oscura 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
"""Waveform timing and amplitude measurements.
|
|
2
|
+
|
|
3
|
+
This module provides IEEE 181-2011 and IEEE 1057-2017 compliant
|
|
4
|
+
waveform measurements including rise/fall time, period, frequency,
|
|
5
|
+
amplitude, and RMS.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from oscura.analyzers.waveform.measurements import rise_time, measure
|
|
10
|
+
>>> t_rise = rise_time(trace)
|
|
11
|
+
>>> results = measure(trace, parameters=["rise_time", "frequency"])
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
IEEE 181-2011: Standard for Transitional Waveform Definitions
|
|
15
|
+
IEEE 1057-2017: Standard for Digitizing Waveform Recorders
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from numpy import floating as np_floating
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
from oscura.core.types import WaveformTrace
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def rise_time(
|
|
32
|
+
trace: WaveformTrace,
|
|
33
|
+
*,
|
|
34
|
+
ref_levels: tuple[float, float] = (0.1, 0.9),
|
|
35
|
+
) -> float | np_floating[Any]:
|
|
36
|
+
"""Measure rise time between reference levels.
|
|
37
|
+
|
|
38
|
+
Computes the time for a signal to transition from the lower
|
|
39
|
+
reference level to the upper reference level, per IEEE 181-2011.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
trace: Input waveform trace.
|
|
43
|
+
ref_levels: Reference levels as fractions (0.0 to 1.0).
|
|
44
|
+
Default (0.1, 0.9) for 10%-90% rise time.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Rise time in seconds, or np.nan if no valid rising edge found.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> t_rise = rise_time(trace)
|
|
51
|
+
>>> print(f"Rise time: {t_rise * 1e9:.2f} ns")
|
|
52
|
+
|
|
53
|
+
References:
|
|
54
|
+
IEEE 181-2011 Section 5.2
|
|
55
|
+
"""
|
|
56
|
+
if len(trace.data) < 3:
|
|
57
|
+
return np.nan
|
|
58
|
+
|
|
59
|
+
data = trace.data
|
|
60
|
+
low, high = _find_levels(data)
|
|
61
|
+
amplitude = high - low
|
|
62
|
+
|
|
63
|
+
if amplitude <= 0:
|
|
64
|
+
return np.nan
|
|
65
|
+
|
|
66
|
+
# Calculate reference voltages
|
|
67
|
+
low_ref = low + ref_levels[0] * amplitude
|
|
68
|
+
high_ref = low + ref_levels[1] * amplitude
|
|
69
|
+
|
|
70
|
+
# Find rising edge: where signal crosses from below low_ref to above high_ref
|
|
71
|
+
sample_period = trace.metadata.time_base
|
|
72
|
+
|
|
73
|
+
# Find first crossing of low reference (going up)
|
|
74
|
+
below_low = data < low_ref
|
|
75
|
+
above_low = data >= low_ref
|
|
76
|
+
|
|
77
|
+
# Find transitions from below to above low_ref
|
|
78
|
+
transitions = np.where(below_low[:-1] & above_low[1:])[0]
|
|
79
|
+
|
|
80
|
+
if len(transitions) == 0:
|
|
81
|
+
return np.nan
|
|
82
|
+
|
|
83
|
+
best_rise_time: float | np_floating[Any] = np.nan
|
|
84
|
+
|
|
85
|
+
for start_idx in transitions:
|
|
86
|
+
# Find where signal crosses high reference
|
|
87
|
+
remaining = data[start_idx:]
|
|
88
|
+
above_high = remaining >= high_ref
|
|
89
|
+
|
|
90
|
+
if not np.any(above_high):
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
end_offset = np.argmax(above_high)
|
|
94
|
+
end_idx = start_idx + end_offset
|
|
95
|
+
|
|
96
|
+
# Ensure monotonic rise (no dips)
|
|
97
|
+
segment = data[start_idx : end_idx + 1]
|
|
98
|
+
if len(segment) < 2:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# Interpolate for sub-sample accuracy
|
|
102
|
+
t_low = _interpolate_crossing_time(data, start_idx, low_ref, sample_period, rising=True)
|
|
103
|
+
t_high = _interpolate_crossing_time(data, end_idx - 1, high_ref, sample_period, rising=True)
|
|
104
|
+
|
|
105
|
+
if t_low is not None and t_high is not None:
|
|
106
|
+
rt = t_high - t_low
|
|
107
|
+
if rt > 0 and (np.isnan(best_rise_time) or rt < best_rise_time):
|
|
108
|
+
best_rise_time = rt
|
|
109
|
+
|
|
110
|
+
return best_rise_time
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def fall_time(
|
|
114
|
+
trace: WaveformTrace,
|
|
115
|
+
*,
|
|
116
|
+
ref_levels: tuple[float, float] = (0.9, 0.1),
|
|
117
|
+
) -> float | np_floating[Any]:
|
|
118
|
+
"""Measure fall time between reference levels.
|
|
119
|
+
|
|
120
|
+
Computes the time for a signal to transition from the upper
|
|
121
|
+
reference level to the lower reference level, per IEEE 181-2011.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
trace: Input waveform trace.
|
|
125
|
+
ref_levels: Reference levels as fractions (0.0 to 1.0).
|
|
126
|
+
Default (0.9, 0.1) for 90%-10% fall time.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Fall time in seconds, or np.nan if no valid falling edge found.
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
>>> t_fall = fall_time(trace)
|
|
133
|
+
>>> print(f"Fall time: {t_fall * 1e9:.2f} ns")
|
|
134
|
+
|
|
135
|
+
References:
|
|
136
|
+
IEEE 181-2011 Section 5.2
|
|
137
|
+
"""
|
|
138
|
+
if len(trace.data) < 3:
|
|
139
|
+
return np.nan
|
|
140
|
+
|
|
141
|
+
data = trace.data
|
|
142
|
+
low, high = _find_levels(data)
|
|
143
|
+
amplitude = high - low
|
|
144
|
+
|
|
145
|
+
if amplitude <= 0:
|
|
146
|
+
return np.nan
|
|
147
|
+
|
|
148
|
+
# Calculate reference voltages (note: ref_levels[0] is the higher one for fall)
|
|
149
|
+
high_ref = low + ref_levels[0] * amplitude
|
|
150
|
+
low_ref = low + ref_levels[1] * amplitude
|
|
151
|
+
|
|
152
|
+
sample_period = trace.metadata.time_base
|
|
153
|
+
|
|
154
|
+
# Find where signal is above high reference
|
|
155
|
+
above_high = data >= high_ref
|
|
156
|
+
below_high = data < high_ref
|
|
157
|
+
|
|
158
|
+
# Find transitions from above to below high_ref
|
|
159
|
+
transitions = np.where(above_high[:-1] & below_high[1:])[0]
|
|
160
|
+
|
|
161
|
+
if len(transitions) == 0:
|
|
162
|
+
return np.nan
|
|
163
|
+
|
|
164
|
+
best_fall_time: float | np_floating[Any] = np.nan
|
|
165
|
+
|
|
166
|
+
for start_idx in transitions:
|
|
167
|
+
# Find where signal crosses low reference
|
|
168
|
+
remaining = data[start_idx:]
|
|
169
|
+
below_low = remaining <= low_ref
|
|
170
|
+
|
|
171
|
+
if not np.any(below_low):
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
end_offset = np.argmax(below_low)
|
|
175
|
+
end_idx = start_idx + end_offset
|
|
176
|
+
|
|
177
|
+
segment = data[start_idx : end_idx + 1]
|
|
178
|
+
if len(segment) < 2:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Interpolate for sub-sample accuracy
|
|
182
|
+
t_high = _interpolate_crossing_time(data, start_idx, high_ref, sample_period, rising=False)
|
|
183
|
+
t_low = _interpolate_crossing_time(data, end_idx - 1, low_ref, sample_period, rising=False)
|
|
184
|
+
|
|
185
|
+
if t_high is not None and t_low is not None:
|
|
186
|
+
ft = t_low - t_high
|
|
187
|
+
if ft > 0 and (np.isnan(best_fall_time) or ft < best_fall_time):
|
|
188
|
+
best_fall_time = ft
|
|
189
|
+
|
|
190
|
+
return best_fall_time
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@overload
|
|
194
|
+
def period(
|
|
195
|
+
trace: WaveformTrace,
|
|
196
|
+
*,
|
|
197
|
+
edge_type: Literal["rising", "falling"] = "rising",
|
|
198
|
+
return_all: Literal[False] = False,
|
|
199
|
+
) -> float | np_floating[Any]: ...
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@overload
|
|
203
|
+
def period(
|
|
204
|
+
trace: WaveformTrace,
|
|
205
|
+
*,
|
|
206
|
+
edge_type: Literal["rising", "falling"] = "rising",
|
|
207
|
+
return_all: Literal[True],
|
|
208
|
+
) -> NDArray[np.float64]: ...
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def period(
|
|
212
|
+
trace: WaveformTrace,
|
|
213
|
+
*,
|
|
214
|
+
edge_type: Literal["rising", "falling"] = "rising",
|
|
215
|
+
return_all: bool = False,
|
|
216
|
+
) -> float | np_floating[Any] | NDArray[np.float64]:
|
|
217
|
+
"""Measure signal period between consecutive edges.
|
|
218
|
+
|
|
219
|
+
Computes the time between consecutive rising or falling edges.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
trace: Input waveform trace.
|
|
223
|
+
edge_type: Type of edges to use ("rising" or "falling").
|
|
224
|
+
return_all: If True, return array of all periods. If False, return mean.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Period in seconds (mean if return_all=False), or array of periods.
|
|
228
|
+
|
|
229
|
+
Example:
|
|
230
|
+
>>> T = period(trace)
|
|
231
|
+
>>> print(f"Period: {T * 1e6:.2f} us")
|
|
232
|
+
|
|
233
|
+
References:
|
|
234
|
+
IEEE 181-2011 Section 5.3
|
|
235
|
+
"""
|
|
236
|
+
edges = _find_edges(trace, edge_type)
|
|
237
|
+
|
|
238
|
+
if len(edges) < 2:
|
|
239
|
+
if return_all:
|
|
240
|
+
return np.array([], dtype=np.float64)
|
|
241
|
+
return np.nan
|
|
242
|
+
|
|
243
|
+
periods = np.diff(edges)
|
|
244
|
+
|
|
245
|
+
if return_all:
|
|
246
|
+
return periods
|
|
247
|
+
return float(np.mean(periods))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def frequency(
|
|
251
|
+
trace: WaveformTrace,
|
|
252
|
+
*,
|
|
253
|
+
method: Literal["edge", "fft"] = "edge",
|
|
254
|
+
) -> float | np_floating[Any]:
|
|
255
|
+
"""Measure signal frequency.
|
|
256
|
+
|
|
257
|
+
Computes frequency either from edge-to-edge period or using FFT.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
trace: Input waveform trace.
|
|
261
|
+
method: Measurement method:
|
|
262
|
+
- "edge": 1/period from edge timing (default, more accurate)
|
|
263
|
+
- "fft": Peak of FFT magnitude spectrum
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Frequency in Hz, or np.nan if measurement not possible.
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
ValueError: If method is not one of the supported types.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
>>> f = frequency(trace)
|
|
273
|
+
>>> print(f"Frequency: {f / 1e6:.3f} MHz")
|
|
274
|
+
|
|
275
|
+
References:
|
|
276
|
+
IEEE 181-2011 Section 5.3
|
|
277
|
+
"""
|
|
278
|
+
if method == "edge":
|
|
279
|
+
T = period(trace, edge_type="rising", return_all=False)
|
|
280
|
+
if np.isnan(T) or T <= 0:
|
|
281
|
+
return np.nan
|
|
282
|
+
return 1.0 / T
|
|
283
|
+
|
|
284
|
+
elif method == "fft":
|
|
285
|
+
if len(trace.data) < 16:
|
|
286
|
+
return np.nan
|
|
287
|
+
|
|
288
|
+
data = trace.data - np.mean(trace.data) # Remove DC
|
|
289
|
+
n = len(data)
|
|
290
|
+
fft_mag = np.abs(np.fft.rfft(data))
|
|
291
|
+
|
|
292
|
+
# Find peak (skip DC component)
|
|
293
|
+
peak_idx = np.argmax(fft_mag[1:]) + 1
|
|
294
|
+
|
|
295
|
+
# Calculate frequency
|
|
296
|
+
freq_resolution = trace.metadata.sample_rate / n
|
|
297
|
+
return float(peak_idx * freq_resolution)
|
|
298
|
+
|
|
299
|
+
else:
|
|
300
|
+
raise ValueError(f"Unknown method: {method}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def duty_cycle(
|
|
304
|
+
trace: WaveformTrace,
|
|
305
|
+
*,
|
|
306
|
+
percentage: bool = False,
|
|
307
|
+
) -> float | np_floating[Any]:
|
|
308
|
+
"""Measure duty cycle.
|
|
309
|
+
|
|
310
|
+
Computes duty cycle as the ratio of positive pulse width to period.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
trace: Input waveform trace.
|
|
314
|
+
percentage: If True, return as percentage (0-100). If False, return ratio (0-1).
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Duty cycle as ratio or percentage.
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
>>> dc = duty_cycle(trace, percentage=True)
|
|
321
|
+
>>> print(f"Duty cycle: {dc:.1f}%")
|
|
322
|
+
|
|
323
|
+
References:
|
|
324
|
+
IEEE 181-2011 Section 5.4
|
|
325
|
+
"""
|
|
326
|
+
pw_pos = pulse_width(trace, polarity="positive", return_all=False)
|
|
327
|
+
T = period(trace, edge_type="rising", return_all=False)
|
|
328
|
+
|
|
329
|
+
if np.isnan(pw_pos) or np.isnan(T) or T <= 0:
|
|
330
|
+
return np.nan
|
|
331
|
+
|
|
332
|
+
dc = pw_pos / T
|
|
333
|
+
|
|
334
|
+
if percentage:
|
|
335
|
+
return dc * 100
|
|
336
|
+
return dc
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@overload
|
|
340
|
+
def pulse_width(
|
|
341
|
+
trace: WaveformTrace,
|
|
342
|
+
*,
|
|
343
|
+
polarity: Literal["positive", "negative"] = "positive",
|
|
344
|
+
ref_level: float = 0.5,
|
|
345
|
+
return_all: Literal[False] = False,
|
|
346
|
+
) -> float | np_floating[Any]: ...
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@overload
|
|
350
|
+
def pulse_width(
|
|
351
|
+
trace: WaveformTrace,
|
|
352
|
+
*,
|
|
353
|
+
polarity: Literal["positive", "negative"] = "positive",
|
|
354
|
+
ref_level: float = 0.5,
|
|
355
|
+
return_all: Literal[True],
|
|
356
|
+
) -> NDArray[np.float64]: ...
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def pulse_width(
|
|
360
|
+
trace: WaveformTrace,
|
|
361
|
+
*,
|
|
362
|
+
polarity: Literal["positive", "negative"] = "positive",
|
|
363
|
+
ref_level: float = 0.5,
|
|
364
|
+
return_all: bool = False,
|
|
365
|
+
) -> float | np_floating[Any] | NDArray[np.float64]:
|
|
366
|
+
"""Measure pulse width.
|
|
367
|
+
|
|
368
|
+
Computes positive or negative pulse width at the specified reference level.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
trace: Input waveform trace.
|
|
372
|
+
polarity: "positive" for high pulses, "negative" for low pulses.
|
|
373
|
+
ref_level: Reference level as fraction (0.0 to 1.0). Default 0.5 (50%).
|
|
374
|
+
return_all: If True, return array of all widths. If False, return mean.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Pulse width in seconds.
|
|
378
|
+
|
|
379
|
+
Example:
|
|
380
|
+
>>> pw = pulse_width(trace, polarity="positive")
|
|
381
|
+
>>> print(f"Pulse width: {pw * 1e6:.2f} us")
|
|
382
|
+
|
|
383
|
+
References:
|
|
384
|
+
IEEE 181-2011 Section 5.4
|
|
385
|
+
"""
|
|
386
|
+
rising_edges = _find_edges(trace, "rising", ref_level)
|
|
387
|
+
falling_edges = _find_edges(trace, "falling", ref_level)
|
|
388
|
+
|
|
389
|
+
if len(rising_edges) == 0 or len(falling_edges) == 0:
|
|
390
|
+
if return_all:
|
|
391
|
+
return np.array([], dtype=np.float64)
|
|
392
|
+
return np.nan
|
|
393
|
+
|
|
394
|
+
widths: list[float] = []
|
|
395
|
+
|
|
396
|
+
if polarity == "positive":
|
|
397
|
+
# Rising to falling
|
|
398
|
+
for r in rising_edges:
|
|
399
|
+
# Find next falling edge after this rising edge
|
|
400
|
+
next_falling = falling_edges[falling_edges > r]
|
|
401
|
+
if len(next_falling) > 0:
|
|
402
|
+
widths.append(next_falling[0] - r)
|
|
403
|
+
else:
|
|
404
|
+
# Falling to rising
|
|
405
|
+
for f in falling_edges:
|
|
406
|
+
# Find next rising edge after this falling edge
|
|
407
|
+
next_rising = rising_edges[rising_edges > f]
|
|
408
|
+
if len(next_rising) > 0:
|
|
409
|
+
widths.append(next_rising[0] - f)
|
|
410
|
+
|
|
411
|
+
if len(widths) == 0:
|
|
412
|
+
if return_all:
|
|
413
|
+
return np.array([], dtype=np.float64)
|
|
414
|
+
return np.nan
|
|
415
|
+
|
|
416
|
+
widths_arr = np.array(widths, dtype=np.float64)
|
|
417
|
+
|
|
418
|
+
if return_all:
|
|
419
|
+
return widths_arr
|
|
420
|
+
return float(np.mean(widths_arr))
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def overshoot(trace: WaveformTrace) -> float | np_floating[Any]:
|
|
424
|
+
"""Measure overshoot percentage.
|
|
425
|
+
|
|
426
|
+
Computes overshoot as (max - high) / amplitude * 100%.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
trace: Input waveform trace.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Overshoot as percentage, or np.nan if not applicable.
|
|
433
|
+
|
|
434
|
+
Example:
|
|
435
|
+
>>> os = overshoot(trace)
|
|
436
|
+
>>> print(f"Overshoot: {os:.1f}%")
|
|
437
|
+
|
|
438
|
+
References:
|
|
439
|
+
IEEE 181-2011 Section 5.5
|
|
440
|
+
"""
|
|
441
|
+
if len(trace.data) < 3:
|
|
442
|
+
return np.nan
|
|
443
|
+
|
|
444
|
+
data = trace.data
|
|
445
|
+
low, high = _find_levels(data)
|
|
446
|
+
amplitude = high - low
|
|
447
|
+
|
|
448
|
+
if amplitude <= 0:
|
|
449
|
+
return np.nan
|
|
450
|
+
|
|
451
|
+
max_val = np.max(data)
|
|
452
|
+
|
|
453
|
+
if max_val <= high:
|
|
454
|
+
return 0.0
|
|
455
|
+
|
|
456
|
+
return float((max_val - high) / amplitude * 100)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def undershoot(trace: WaveformTrace) -> float | np_floating[Any]:
|
|
460
|
+
"""Measure undershoot percentage.
|
|
461
|
+
|
|
462
|
+
Computes undershoot as (low - min) / amplitude * 100%.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
trace: Input waveform trace.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Undershoot as percentage, or np.nan if not applicable.
|
|
469
|
+
|
|
470
|
+
Example:
|
|
471
|
+
>>> us = undershoot(trace)
|
|
472
|
+
>>> print(f"Undershoot: {us:.1f}%")
|
|
473
|
+
|
|
474
|
+
References:
|
|
475
|
+
IEEE 181-2011 Section 5.5
|
|
476
|
+
"""
|
|
477
|
+
if len(trace.data) < 3:
|
|
478
|
+
return np.nan
|
|
479
|
+
|
|
480
|
+
data = trace.data
|
|
481
|
+
low, high = _find_levels(data)
|
|
482
|
+
amplitude = high - low
|
|
483
|
+
|
|
484
|
+
if amplitude <= 0:
|
|
485
|
+
return np.nan
|
|
486
|
+
|
|
487
|
+
min_val = np.min(data)
|
|
488
|
+
|
|
489
|
+
if min_val >= low:
|
|
490
|
+
return 0.0
|
|
491
|
+
|
|
492
|
+
return float((low - min_val) / amplitude * 100)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def preshoot(
|
|
496
|
+
trace: WaveformTrace,
|
|
497
|
+
*,
|
|
498
|
+
edge_type: Literal["rising", "falling"] = "rising",
|
|
499
|
+
) -> float | np_floating[Any]:
|
|
500
|
+
"""Measure preshoot percentage.
|
|
501
|
+
|
|
502
|
+
Computes preshoot before transitions as percentage of amplitude.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
trace: Input waveform trace.
|
|
506
|
+
edge_type: Type of edge to analyze ("rising" or "falling").
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Preshoot as percentage, or np.nan if not applicable.
|
|
510
|
+
|
|
511
|
+
Example:
|
|
512
|
+
>>> ps = preshoot(trace)
|
|
513
|
+
>>> print(f"Preshoot: {ps:.1f}%")
|
|
514
|
+
|
|
515
|
+
References:
|
|
516
|
+
IEEE 181-2011 Section 5.5
|
|
517
|
+
"""
|
|
518
|
+
if len(trace.data) < 10:
|
|
519
|
+
return np.nan
|
|
520
|
+
|
|
521
|
+
# Convert memoryview to ndarray if needed
|
|
522
|
+
data = np.asarray(trace.data)
|
|
523
|
+
low, high = _find_levels(data)
|
|
524
|
+
amplitude = high - low
|
|
525
|
+
|
|
526
|
+
if amplitude <= 0:
|
|
527
|
+
return np.nan
|
|
528
|
+
|
|
529
|
+
# Find edge crossings at 50%
|
|
530
|
+
mid = (low + high) / 2
|
|
531
|
+
|
|
532
|
+
if edge_type == "rising":
|
|
533
|
+
# Look for minimum before rising edge that goes below low level
|
|
534
|
+
crossings = np.where((data[:-1] < mid) & (data[1:] >= mid))[0]
|
|
535
|
+
if len(crossings) == 0:
|
|
536
|
+
return np.nan
|
|
537
|
+
|
|
538
|
+
max_preshoot = 0.0
|
|
539
|
+
for idx in crossings:
|
|
540
|
+
# Look at samples before crossing
|
|
541
|
+
pre_samples = max(0, idx - 10)
|
|
542
|
+
pre_region = data[pre_samples:idx]
|
|
543
|
+
if len(pre_region) > 0:
|
|
544
|
+
min_pre = np.min(pre_region)
|
|
545
|
+
if min_pre < low:
|
|
546
|
+
preshoot_val = (low - min_pre) / amplitude * 100
|
|
547
|
+
max_preshoot = max(max_preshoot, preshoot_val)
|
|
548
|
+
|
|
549
|
+
return max_preshoot
|
|
550
|
+
|
|
551
|
+
else: # falling
|
|
552
|
+
crossings = np.where((data[:-1] >= mid) & (data[1:] < mid))[0]
|
|
553
|
+
if len(crossings) == 0:
|
|
554
|
+
return np.nan
|
|
555
|
+
|
|
556
|
+
max_preshoot = 0.0
|
|
557
|
+
for idx in crossings:
|
|
558
|
+
pre_samples = max(0, idx - 10)
|
|
559
|
+
pre_region = data[pre_samples:idx]
|
|
560
|
+
if len(pre_region) > 0:
|
|
561
|
+
max_pre = np.max(pre_region)
|
|
562
|
+
if max_pre > high:
|
|
563
|
+
preshoot_val = (max_pre - high) / amplitude * 100
|
|
564
|
+
max_preshoot = max(max_preshoot, preshoot_val)
|
|
565
|
+
|
|
566
|
+
return max_preshoot
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def amplitude(trace: WaveformTrace) -> float | np_floating[Any]:
|
|
570
|
+
"""Measure peak-to-peak amplitude.
|
|
571
|
+
|
|
572
|
+
Computes Vpp as the difference between histogram-based high and low levels.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
trace: Input waveform trace.
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
Amplitude in volts (or input units).
|
|
579
|
+
|
|
580
|
+
Example:
|
|
581
|
+
>>> vpp = amplitude(trace)
|
|
582
|
+
>>> print(f"Amplitude: {vpp:.3f} V")
|
|
583
|
+
|
|
584
|
+
References:
|
|
585
|
+
IEEE 1057-2017 Section 4.2
|
|
586
|
+
"""
|
|
587
|
+
if len(trace.data) < 2:
|
|
588
|
+
return np.nan
|
|
589
|
+
|
|
590
|
+
low, high = _find_levels(trace.data)
|
|
591
|
+
return high - low
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def rms(
|
|
595
|
+
trace: WaveformTrace,
|
|
596
|
+
*,
|
|
597
|
+
ac_coupled: bool = False,
|
|
598
|
+
nan_policy: Literal["propagate", "omit", "raise"] = "propagate",
|
|
599
|
+
) -> float | np_floating[Any]:
|
|
600
|
+
"""Compute RMS voltage.
|
|
601
|
+
|
|
602
|
+
Calculates root-mean-square voltage of the waveform.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
trace: Input waveform trace.
|
|
606
|
+
ac_coupled: If True, remove DC offset before computing RMS.
|
|
607
|
+
nan_policy: How to handle NaN values:
|
|
608
|
+
- "propagate": Return NaN if any NaN present (default, NumPy behavior)
|
|
609
|
+
- "omit": Ignore NaN values in calculation
|
|
610
|
+
- "raise": Raise ValueError if any NaN present
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
RMS voltage in volts (or input units).
|
|
614
|
+
|
|
615
|
+
Raises:
|
|
616
|
+
ValueError: If nan_policy="raise" and data contains NaN.
|
|
617
|
+
|
|
618
|
+
Example:
|
|
619
|
+
>>> v_rms = rms(trace)
|
|
620
|
+
>>> print(f"RMS: {v_rms:.3f} V")
|
|
621
|
+
|
|
622
|
+
>>> # Handle traces with NaN values
|
|
623
|
+
>>> v_rms = rms(trace, nan_policy="omit")
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
References:
|
|
627
|
+
IEEE 1057-2017 Section 4.3
|
|
628
|
+
"""
|
|
629
|
+
if len(trace.data) == 0:
|
|
630
|
+
return np.nan
|
|
631
|
+
|
|
632
|
+
# Convert memoryview to ndarray if needed
|
|
633
|
+
data = np.asarray(trace.data)
|
|
634
|
+
|
|
635
|
+
# Handle NaN based on policy
|
|
636
|
+
if nan_policy == "raise":
|
|
637
|
+
if np.any(np.isnan(data)):
|
|
638
|
+
raise ValueError("Input data contains NaN values")
|
|
639
|
+
elif nan_policy == "omit":
|
|
640
|
+
# Use nanmean and nansum for NaN-safe calculation
|
|
641
|
+
if ac_coupled:
|
|
642
|
+
data = data - np.nanmean(data)
|
|
643
|
+
return float(np.sqrt(np.nanmean(data**2)))
|
|
644
|
+
# else propagate - default NumPy behavior
|
|
645
|
+
|
|
646
|
+
if ac_coupled:
|
|
647
|
+
data = data - np.mean(data)
|
|
648
|
+
|
|
649
|
+
return float(np.sqrt(np.mean(data**2)))
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def mean(
|
|
653
|
+
trace: WaveformTrace,
|
|
654
|
+
*,
|
|
655
|
+
nan_policy: Literal["propagate", "omit", "raise"] = "propagate",
|
|
656
|
+
) -> float | np_floating[Any]:
|
|
657
|
+
"""Compute mean (DC) voltage.
|
|
658
|
+
|
|
659
|
+
Calculates arithmetic mean of the waveform.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
trace: Input waveform trace.
|
|
663
|
+
nan_policy: How to handle NaN values:
|
|
664
|
+
- "propagate": Return NaN if any NaN present (default, NumPy behavior)
|
|
665
|
+
- "omit": Ignore NaN values in calculation
|
|
666
|
+
- "raise": Raise ValueError if any NaN present
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Mean voltage in volts (or input units).
|
|
670
|
+
|
|
671
|
+
Raises:
|
|
672
|
+
ValueError: If nan_policy="raise" and data contains NaN.
|
|
673
|
+
|
|
674
|
+
Example:
|
|
675
|
+
>>> v_dc = mean(trace)
|
|
676
|
+
>>> print(f"DC: {v_dc:.3f} V")
|
|
677
|
+
|
|
678
|
+
>>> # Handle traces with NaN values
|
|
679
|
+
>>> v_dc = mean(trace, nan_policy="omit")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
References:
|
|
683
|
+
IEEE 1057-2017 Section 4.3
|
|
684
|
+
"""
|
|
685
|
+
if len(trace.data) == 0:
|
|
686
|
+
return np.nan
|
|
687
|
+
|
|
688
|
+
# Convert memoryview to ndarray if needed
|
|
689
|
+
data = np.asarray(trace.data)
|
|
690
|
+
|
|
691
|
+
# Handle NaN based on policy
|
|
692
|
+
if nan_policy == "raise":
|
|
693
|
+
if np.any(np.isnan(data)):
|
|
694
|
+
raise ValueError("Input data contains NaN values")
|
|
695
|
+
return float(np.mean(data))
|
|
696
|
+
elif nan_policy == "omit":
|
|
697
|
+
return float(np.nanmean(data))
|
|
698
|
+
else: # propagate
|
|
699
|
+
return float(np.mean(data))
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def measure(
|
|
703
|
+
trace: WaveformTrace,
|
|
704
|
+
*,
|
|
705
|
+
parameters: list[str] | None = None,
|
|
706
|
+
include_units: bool = True,
|
|
707
|
+
) -> dict[str, Any]:
|
|
708
|
+
"""Compute multiple waveform measurements.
|
|
709
|
+
|
|
710
|
+
Unified function for computing all or selected waveform measurements.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
trace: Input waveform trace.
|
|
714
|
+
parameters: List of measurement names to compute. If None, compute all.
|
|
715
|
+
Valid names: rise_time, fall_time, period, frequency, duty_cycle,
|
|
716
|
+
amplitude, rms, mean, overshoot, undershoot, preshoot
|
|
717
|
+
include_units: If True, include units in output.
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
Dictionary mapping measurement names to values (and units if requested).
|
|
721
|
+
|
|
722
|
+
Example:
|
|
723
|
+
>>> results = measure(trace)
|
|
724
|
+
>>> print(f"Rise time: {results['rise_time']['value']} {results['rise_time']['unit']}")
|
|
725
|
+
|
|
726
|
+
>>> results = measure(trace, parameters=["frequency", "amplitude"])
|
|
727
|
+
|
|
728
|
+
References:
|
|
729
|
+
IEEE 181-2011, IEEE 1057-2017
|
|
730
|
+
"""
|
|
731
|
+
all_measurements = {
|
|
732
|
+
"rise_time": (rise_time, "s"),
|
|
733
|
+
"fall_time": (fall_time, "s"),
|
|
734
|
+
"period": (lambda t: period(t, return_all=False), "s"),
|
|
735
|
+
"frequency": (frequency, "Hz"),
|
|
736
|
+
"duty_cycle": (lambda t: duty_cycle(t, percentage=True), "%"),
|
|
737
|
+
"pulse_width_pos": (
|
|
738
|
+
lambda t: pulse_width(t, polarity="positive", return_all=False),
|
|
739
|
+
"s",
|
|
740
|
+
),
|
|
741
|
+
"pulse_width_neg": (
|
|
742
|
+
lambda t: pulse_width(t, polarity="negative", return_all=False),
|
|
743
|
+
"s",
|
|
744
|
+
),
|
|
745
|
+
"amplitude": (amplitude, "V"),
|
|
746
|
+
"rms": (rms, "V"),
|
|
747
|
+
"mean": (mean, "V"),
|
|
748
|
+
"overshoot": (overshoot, "%"),
|
|
749
|
+
"undershoot": (undershoot, "%"),
|
|
750
|
+
"preshoot": (preshoot, "%"),
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if parameters is None:
|
|
754
|
+
selected = all_measurements
|
|
755
|
+
else:
|
|
756
|
+
selected = {k: v for k, v in all_measurements.items() if k in parameters}
|
|
757
|
+
|
|
758
|
+
results: dict[str, Any] = {}
|
|
759
|
+
|
|
760
|
+
for name, (func, unit) in selected.items():
|
|
761
|
+
try:
|
|
762
|
+
value = func(trace) # type: ignore[operator]
|
|
763
|
+
except Exception:
|
|
764
|
+
value = np.nan
|
|
765
|
+
|
|
766
|
+
if include_units:
|
|
767
|
+
results[name] = {"value": value, "unit": unit}
|
|
768
|
+
else:
|
|
769
|
+
results[name] = value
|
|
770
|
+
|
|
771
|
+
return results
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
# =============================================================================
|
|
775
|
+
# Helper Functions
|
|
776
|
+
# =============================================================================
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _find_levels(data: NDArray[np_floating[Any]]) -> tuple[float, float]:
|
|
780
|
+
"""Find low and high levels using histogram method.
|
|
781
|
+
|
|
782
|
+
Args:
|
|
783
|
+
data: Waveform data array.
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
Tuple of (low_level, high_level).
|
|
787
|
+
"""
|
|
788
|
+
# Convert boolean data to float if needed (for digital signals)
|
|
789
|
+
if data.dtype == np.bool_:
|
|
790
|
+
data = data.astype(np.float64)
|
|
791
|
+
|
|
792
|
+
# Use percentiles for robust level detection
|
|
793
|
+
p10, p90 = np.percentile(data, [10, 90])
|
|
794
|
+
|
|
795
|
+
# Refine using histogram peaks
|
|
796
|
+
hist, bin_edges = np.histogram(data, bins=50)
|
|
797
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
798
|
+
|
|
799
|
+
# Find peaks in lower and upper halves
|
|
800
|
+
mid_idx = len(hist) // 2
|
|
801
|
+
low_idx = np.argmax(hist[:mid_idx])
|
|
802
|
+
high_idx = mid_idx + np.argmax(hist[mid_idx:])
|
|
803
|
+
|
|
804
|
+
low = bin_centers[low_idx]
|
|
805
|
+
high = bin_centers[high_idx]
|
|
806
|
+
|
|
807
|
+
# Sanity check
|
|
808
|
+
if high <= low:
|
|
809
|
+
return float(p10), float(p90)
|
|
810
|
+
|
|
811
|
+
return float(low), float(high)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _find_edges(
|
|
815
|
+
trace: WaveformTrace,
|
|
816
|
+
edge_type: Literal["rising", "falling"],
|
|
817
|
+
ref_level: float = 0.5,
|
|
818
|
+
) -> NDArray[np.float64]:
|
|
819
|
+
"""Find edge timestamps in a waveform.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
trace: Input waveform.
|
|
823
|
+
edge_type: Type of edges to find.
|
|
824
|
+
ref_level: Reference level as fraction (0.0 to 1.0). Default 0.5 (50%).
|
|
825
|
+
|
|
826
|
+
Returns:
|
|
827
|
+
Array of edge timestamps in seconds.
|
|
828
|
+
"""
|
|
829
|
+
data = trace.data
|
|
830
|
+
sample_period = trace.metadata.time_base
|
|
831
|
+
|
|
832
|
+
if len(data) < 3:
|
|
833
|
+
return np.array([], dtype=np.float64)
|
|
834
|
+
|
|
835
|
+
# Convert boolean data to float for arithmetic (NumPy 2.0+ compatibility)
|
|
836
|
+
if data.dtype == bool:
|
|
837
|
+
data = data.astype(np.float64)
|
|
838
|
+
|
|
839
|
+
low, high = _find_levels(data)
|
|
840
|
+
# Use ref_level parameter to compute threshold
|
|
841
|
+
mid = low + ref_level * (high - low)
|
|
842
|
+
|
|
843
|
+
if edge_type == "rising":
|
|
844
|
+
crossings = np.where((data[:-1] < mid) & (data[1:] >= mid))[0]
|
|
845
|
+
else:
|
|
846
|
+
crossings = np.where((data[:-1] >= mid) & (data[1:] < mid))[0]
|
|
847
|
+
|
|
848
|
+
# Convert to timestamps with interpolation
|
|
849
|
+
timestamps = np.zeros(len(crossings), dtype=np.float64)
|
|
850
|
+
|
|
851
|
+
for i, idx in enumerate(crossings):
|
|
852
|
+
base_time = idx * sample_period
|
|
853
|
+
|
|
854
|
+
# Linear interpolation
|
|
855
|
+
if idx < len(data) - 1:
|
|
856
|
+
v1, v2 = data[idx], data[idx + 1]
|
|
857
|
+
if abs(v2 - v1) > 1e-12:
|
|
858
|
+
t_offset = (mid - v1) / (v2 - v1) * sample_period
|
|
859
|
+
t_offset = max(0, min(sample_period, t_offset))
|
|
860
|
+
timestamps[i] = base_time + t_offset
|
|
861
|
+
else:
|
|
862
|
+
timestamps[i] = base_time + sample_period / 2
|
|
863
|
+
else:
|
|
864
|
+
timestamps[i] = base_time
|
|
865
|
+
|
|
866
|
+
return timestamps
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _interpolate_crossing_time(
|
|
870
|
+
data: NDArray[np_floating[Any]],
|
|
871
|
+
idx: int,
|
|
872
|
+
threshold: float,
|
|
873
|
+
sample_period: float,
|
|
874
|
+
rising: bool,
|
|
875
|
+
) -> float | None:
|
|
876
|
+
"""Interpolate threshold crossing time.
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
data: Waveform data.
|
|
880
|
+
idx: Sample index near crossing.
|
|
881
|
+
threshold: Threshold level.
|
|
882
|
+
sample_period: Time between samples.
|
|
883
|
+
rising: True for rising edge, False for falling.
|
|
884
|
+
|
|
885
|
+
Returns:
|
|
886
|
+
Time of crossing in seconds, or None if not found.
|
|
887
|
+
"""
|
|
888
|
+
if idx < 0 or idx >= len(data) - 1:
|
|
889
|
+
return None
|
|
890
|
+
|
|
891
|
+
v1, v2 = data[idx], data[idx + 1]
|
|
892
|
+
|
|
893
|
+
# Check direction
|
|
894
|
+
if rising and not (v1 < threshold <= v2):
|
|
895
|
+
# Search nearby
|
|
896
|
+
for offset in range(-2, 3):
|
|
897
|
+
check_idx = idx + offset
|
|
898
|
+
if 0 <= check_idx < len(data) - 1:
|
|
899
|
+
v1, v2 = data[check_idx], data[check_idx + 1]
|
|
900
|
+
if v1 < threshold <= v2:
|
|
901
|
+
idx = check_idx
|
|
902
|
+
break
|
|
903
|
+
else:
|
|
904
|
+
return None
|
|
905
|
+
|
|
906
|
+
if not rising and not (v1 >= threshold > v2):
|
|
907
|
+
for offset in range(-2, 3):
|
|
908
|
+
check_idx = idx + offset
|
|
909
|
+
if 0 <= check_idx < len(data) - 1:
|
|
910
|
+
v1, v2 = data[check_idx], data[check_idx + 1]
|
|
911
|
+
if v1 >= threshold > v2:
|
|
912
|
+
idx = check_idx
|
|
913
|
+
break
|
|
914
|
+
else:
|
|
915
|
+
return None
|
|
916
|
+
|
|
917
|
+
v1, v2 = data[idx], data[idx + 1]
|
|
918
|
+
dv = v2 - v1
|
|
919
|
+
|
|
920
|
+
if abs(dv) < 1e-12:
|
|
921
|
+
t_offset = sample_period / 2
|
|
922
|
+
else:
|
|
923
|
+
t_offset = (threshold - v1) / dv * sample_period
|
|
924
|
+
t_offset = max(0, min(sample_period, t_offset))
|
|
925
|
+
|
|
926
|
+
return idx * sample_period + t_offset
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
__all__ = [
|
|
930
|
+
"amplitude",
|
|
931
|
+
"duty_cycle",
|
|
932
|
+
"fall_time",
|
|
933
|
+
"frequency",
|
|
934
|
+
"mean",
|
|
935
|
+
"measure",
|
|
936
|
+
"overshoot",
|
|
937
|
+
"period",
|
|
938
|
+
"preshoot",
|
|
939
|
+
"pulse_width",
|
|
940
|
+
"rise_time",
|
|
941
|
+
"rms",
|
|
942
|
+
"undershoot",
|
|
943
|
+
]
|