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,338 @@
|
|
|
1
|
+
"""Auto-detection utilities for signal analysis.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for automatic detection of signal
|
|
4
|
+
parameters such as baud rate, logic levels, and protocol types.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.utils.autodetect import detect_baud_rate
|
|
9
|
+
>>> baudrate = detect_baud_rate(trace)
|
|
10
|
+
>>> print(f"Detected baud rate: {baudrate}")
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
Standard baud rates. and UART specifications.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING, Literal
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from numpy.typing import NDArray
|
|
26
|
+
|
|
27
|
+
# Standard baud rates (RS-232, UART, CAN, etc.)
|
|
28
|
+
STANDARD_BAUD_RATES: tuple[int, ...] = (
|
|
29
|
+
300,
|
|
30
|
+
600,
|
|
31
|
+
1200,
|
|
32
|
+
2400,
|
|
33
|
+
4800,
|
|
34
|
+
9600,
|
|
35
|
+
14400,
|
|
36
|
+
19200,
|
|
37
|
+
28800,
|
|
38
|
+
38400,
|
|
39
|
+
57600,
|
|
40
|
+
76800,
|
|
41
|
+
115200,
|
|
42
|
+
230400,
|
|
43
|
+
250000, # CAN common
|
|
44
|
+
460800,
|
|
45
|
+
500000, # CAN common
|
|
46
|
+
576000,
|
|
47
|
+
921600,
|
|
48
|
+
1000000, # 1 Mbps
|
|
49
|
+
1500000,
|
|
50
|
+
2000000,
|
|
51
|
+
3000000,
|
|
52
|
+
4000000,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def detect_baud_rate(
|
|
57
|
+
trace: WaveformTrace | DigitalTrace,
|
|
58
|
+
*,
|
|
59
|
+
threshold: float | Literal["auto"] = "auto",
|
|
60
|
+
method: Literal["pulse_width", "edge_timing", "autocorr"] = "pulse_width",
|
|
61
|
+
tolerance: float = 0.05,
|
|
62
|
+
return_confidence: bool = False,
|
|
63
|
+
) -> int | tuple[int, float]:
|
|
64
|
+
"""Detect baud rate from signal timing.
|
|
65
|
+
|
|
66
|
+
Analyzes pulse widths or edge timing to determine the symbol rate,
|
|
67
|
+
then maps to the nearest standard baud rate.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
trace: Input trace (analog or digital).
|
|
71
|
+
threshold: Threshold for analog to digital conversion.
|
|
72
|
+
method: Detection method:
|
|
73
|
+
- "pulse_width": Minimum pulse width (default)
|
|
74
|
+
- "edge_timing": Edge-to-edge timing analysis
|
|
75
|
+
- "autocorr": Autocorrelation peak detection
|
|
76
|
+
tolerance: Tolerance for matching to standard rate (default 5%).
|
|
77
|
+
return_confidence: If True, also return confidence score.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Detected baud rate (nearest standard), or tuple of (rate, confidence)
|
|
81
|
+
if return_confidence=True.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: If unknown detection method specified.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> baudrate = detect_baud_rate(trace)
|
|
88
|
+
>>> print(f"Detected: {baudrate} bps")
|
|
89
|
+
|
|
90
|
+
>>> baudrate, confidence = detect_baud_rate(trace, return_confidence=True)
|
|
91
|
+
>>> print(f"Detected: {baudrate} bps ({confidence:.0%} confidence)")
|
|
92
|
+
|
|
93
|
+
References:
|
|
94
|
+
RS-232 Standard Baud Rates
|
|
95
|
+
"""
|
|
96
|
+
# Get digital representation
|
|
97
|
+
if isinstance(trace, WaveformTrace):
|
|
98
|
+
from oscura.analyzers.digital.extraction import to_digital
|
|
99
|
+
|
|
100
|
+
digital_trace = to_digital(trace, threshold=threshold)
|
|
101
|
+
data = digital_trace.data
|
|
102
|
+
else:
|
|
103
|
+
data = trace.data
|
|
104
|
+
|
|
105
|
+
sample_rate = trace.metadata.sample_rate
|
|
106
|
+
|
|
107
|
+
if method == "pulse_width":
|
|
108
|
+
bit_period = _detect_via_pulse_width(data, sample_rate)
|
|
109
|
+
elif method == "edge_timing":
|
|
110
|
+
bit_period = _detect_via_edge_timing(data, sample_rate)
|
|
111
|
+
elif method == "autocorr":
|
|
112
|
+
bit_period = _detect_via_autocorrelation(data, sample_rate)
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError(f"Unknown method: {method}")
|
|
115
|
+
|
|
116
|
+
if bit_period <= 0 or np.isnan(bit_period):
|
|
117
|
+
if return_confidence:
|
|
118
|
+
return 0, 0.0
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
# Convert to baud rate
|
|
122
|
+
measured_rate = 1.0 / bit_period
|
|
123
|
+
|
|
124
|
+
# Find nearest standard rate
|
|
125
|
+
best_rate = 0
|
|
126
|
+
best_error = float("inf")
|
|
127
|
+
|
|
128
|
+
for std_rate in STANDARD_BAUD_RATES:
|
|
129
|
+
error = abs(measured_rate - std_rate) / std_rate
|
|
130
|
+
if error < best_error:
|
|
131
|
+
best_error = error
|
|
132
|
+
best_rate = std_rate
|
|
133
|
+
|
|
134
|
+
# Compute confidence
|
|
135
|
+
confidence = max(0.0, 1.0 - best_error / tolerance) if best_error <= tolerance else 0.0
|
|
136
|
+
|
|
137
|
+
if return_confidence:
|
|
138
|
+
return best_rate, confidence
|
|
139
|
+
|
|
140
|
+
return best_rate
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _detect_via_pulse_width(data: NDArray[np.bool_], sample_rate: float) -> float:
|
|
144
|
+
"""Detect bit period from minimum pulse width.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
data: Digital signal data.
|
|
148
|
+
sample_rate: Sample rate in Hz.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Estimated bit period in seconds.
|
|
152
|
+
"""
|
|
153
|
+
# Find pulse widths (runs of consecutive values)
|
|
154
|
+
pulse_widths = []
|
|
155
|
+
|
|
156
|
+
current_value = data[0]
|
|
157
|
+
run_length = 1
|
|
158
|
+
|
|
159
|
+
for i in range(1, len(data)):
|
|
160
|
+
if data[i] == current_value:
|
|
161
|
+
run_length += 1
|
|
162
|
+
else:
|
|
163
|
+
pulse_widths.append(run_length)
|
|
164
|
+
current_value = data[i]
|
|
165
|
+
run_length = 1
|
|
166
|
+
|
|
167
|
+
# Add final run
|
|
168
|
+
pulse_widths.append(run_length)
|
|
169
|
+
|
|
170
|
+
if len(pulse_widths) == 0:
|
|
171
|
+
return 0.0
|
|
172
|
+
|
|
173
|
+
pulse_widths_arr = np.array(pulse_widths, dtype=np.float64)
|
|
174
|
+
|
|
175
|
+
# Filter out very short pulses (noise)
|
|
176
|
+
min_pulse = max(2, np.min(pulse_widths_arr[pulse_widths_arr > 1]))
|
|
177
|
+
|
|
178
|
+
# The minimum pulse width corresponds to a single bit
|
|
179
|
+
# Use the mode of small pulses for robustness
|
|
180
|
+
small_pulses = pulse_widths_arr[pulse_widths_arr <= min_pulse * 1.5]
|
|
181
|
+
|
|
182
|
+
bit_samples = min_pulse if len(small_pulses) == 0 else np.median(small_pulses)
|
|
183
|
+
|
|
184
|
+
return float(bit_samples / sample_rate)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _detect_via_edge_timing(data: NDArray[np.bool_], sample_rate: float) -> float:
|
|
188
|
+
"""Detect bit period from edge-to-edge timing.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
data: Digital signal data.
|
|
192
|
+
sample_rate: Sample rate in Hz.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Estimated bit period in seconds.
|
|
196
|
+
"""
|
|
197
|
+
# Find all edges
|
|
198
|
+
transitions = np.diff(data.astype(np.int8))
|
|
199
|
+
edge_indices = np.where(transitions != 0)[0]
|
|
200
|
+
|
|
201
|
+
if len(edge_indices) < 2:
|
|
202
|
+
return 0.0
|
|
203
|
+
|
|
204
|
+
# Compute edge intervals
|
|
205
|
+
intervals = np.diff(edge_indices).astype(np.float64)
|
|
206
|
+
|
|
207
|
+
if len(intervals) == 0:
|
|
208
|
+
return 0.0
|
|
209
|
+
|
|
210
|
+
# Intervals should be multiples of bit period
|
|
211
|
+
# Find GCD-like value using histogram
|
|
212
|
+
min_interval = np.min(intervals)
|
|
213
|
+
max_check = min(min_interval * 2, np.median(intervals))
|
|
214
|
+
|
|
215
|
+
# The bit period is the smallest common interval
|
|
216
|
+
# Use histogram to find the cluster
|
|
217
|
+
bins = np.arange(1, max_check + 1)
|
|
218
|
+
hist, _ = np.histogram(intervals, bins=bins)
|
|
219
|
+
|
|
220
|
+
if len(hist) == 0 or np.max(hist) == 0:
|
|
221
|
+
bit_samples = min_interval
|
|
222
|
+
else:
|
|
223
|
+
# Find first significant peak
|
|
224
|
+
threshold = np.max(hist) * 0.3
|
|
225
|
+
peaks = np.where(hist >= threshold)[0]
|
|
226
|
+
|
|
227
|
+
if len(peaks) > 0:
|
|
228
|
+
bit_samples = peaks[0] + 1 # +1 for bin offset
|
|
229
|
+
else:
|
|
230
|
+
bit_samples = min_interval
|
|
231
|
+
|
|
232
|
+
return float(bit_samples / sample_rate)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _detect_via_autocorrelation(data: NDArray[np.bool_], sample_rate: float) -> float:
|
|
236
|
+
"""Detect bit period via autocorrelation.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
data: Digital signal data.
|
|
240
|
+
sample_rate: Sample rate in Hz.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Estimated bit period in seconds.
|
|
244
|
+
"""
|
|
245
|
+
# Convert to float for correlation
|
|
246
|
+
signal = data.astype(np.float64) * 2 - 1 # Map to [-1, 1]
|
|
247
|
+
|
|
248
|
+
# Remove DC
|
|
249
|
+
signal = signal - np.mean(signal)
|
|
250
|
+
|
|
251
|
+
# Compute autocorrelation
|
|
252
|
+
n = len(signal)
|
|
253
|
+
max_lag = min(n // 2, int(sample_rate / 300)) # Limit to reasonable range
|
|
254
|
+
|
|
255
|
+
autocorr = np.correlate(signal[: max_lag * 2], signal[: max_lag * 2], mode="full")
|
|
256
|
+
autocorr = autocorr[len(autocorr) // 2 :] # Keep positive lags
|
|
257
|
+
|
|
258
|
+
# Normalize
|
|
259
|
+
autocorr = autocorr / autocorr[0]
|
|
260
|
+
|
|
261
|
+
# Find first significant peak after lag 0
|
|
262
|
+
# Skip initial samples to avoid lag-0 region
|
|
263
|
+
min_lag = max(2, max_lag // 100)
|
|
264
|
+
|
|
265
|
+
# Find local maxima
|
|
266
|
+
peaks = []
|
|
267
|
+
for i in range(min_lag, len(autocorr) - 1):
|
|
268
|
+
if autocorr[i] > autocorr[i - 1] and autocorr[i] > autocorr[i + 1]:
|
|
269
|
+
if autocorr[i] > 0.3: # Significance threshold
|
|
270
|
+
peaks.append((i, autocorr[i]))
|
|
271
|
+
|
|
272
|
+
if len(peaks) == 0:
|
|
273
|
+
return 0.0
|
|
274
|
+
|
|
275
|
+
# First significant peak is likely the bit period
|
|
276
|
+
bit_samples = peaks[0][0]
|
|
277
|
+
|
|
278
|
+
return float(bit_samples / sample_rate)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def detect_logic_family(
|
|
282
|
+
trace: WaveformTrace,
|
|
283
|
+
*,
|
|
284
|
+
return_confidence: bool = False,
|
|
285
|
+
) -> str | tuple[str, float]:
|
|
286
|
+
"""Detect logic family from signal levels.
|
|
287
|
+
|
|
288
|
+
Analyzes voltage levels to identify TTL, CMOS, LVTTL, LVCMOS variants.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
trace: Input analog trace.
|
|
292
|
+
return_confidence: If True, also return confidence score.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Logic family name (e.g., "TTL", "LVCMOS_3V3"), or tuple of
|
|
296
|
+
(family, confidence) if return_confidence=True.
|
|
297
|
+
"""
|
|
298
|
+
from oscura.analyzers.digital.extraction import LOGIC_FAMILIES
|
|
299
|
+
|
|
300
|
+
data = trace.data
|
|
301
|
+
|
|
302
|
+
# Get voltage levels
|
|
303
|
+
v_low = float(np.percentile(data, 10))
|
|
304
|
+
v_high = float(np.percentile(data, 90))
|
|
305
|
+
|
|
306
|
+
# Estimate VCC from high level
|
|
307
|
+
v_cc_est = v_high * 1.1 # Add margin
|
|
308
|
+
|
|
309
|
+
best_family = "TTL"
|
|
310
|
+
best_score = 0.0
|
|
311
|
+
|
|
312
|
+
for family, levels in LOGIC_FAMILIES.items():
|
|
313
|
+
vcc = levels["VCC"]
|
|
314
|
+
vol = levels["VOL_max"]
|
|
315
|
+
voh = levels["VOH_min"]
|
|
316
|
+
|
|
317
|
+
# Score based on how well levels match
|
|
318
|
+
low_match = 1.0 - min(1.0, abs(v_low - vol) / 0.5)
|
|
319
|
+
high_match = 1.0 - min(1.0, abs(v_high - voh) / 0.5)
|
|
320
|
+
vcc_match = 1.0 - min(1.0, abs(v_cc_est - vcc) / vcc)
|
|
321
|
+
|
|
322
|
+
score = (low_match + high_match + vcc_match) / 3
|
|
323
|
+
|
|
324
|
+
if score > best_score:
|
|
325
|
+
best_score = score
|
|
326
|
+
best_family = family
|
|
327
|
+
|
|
328
|
+
if return_confidence:
|
|
329
|
+
return best_family, best_score
|
|
330
|
+
|
|
331
|
+
return best_family
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
__all__ = [
|
|
335
|
+
"STANDARD_BAUD_RATES",
|
|
336
|
+
"detect_baud_rate",
|
|
337
|
+
"detect_logic_family",
|
|
338
|
+
]
|
oscura/utils/buffer.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Buffer utilities for streaming data.
|
|
2
|
+
|
|
3
|
+
This module provides circular buffer implementation for
|
|
4
|
+
streaming data with O(1) operations.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.utils.buffer import CircularBuffer
|
|
9
|
+
>>> buf = CircularBuffer(1000)
|
|
10
|
+
>>> buf.append(value)
|
|
11
|
+
>>> recent = buf.get_last(100)
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
Ring buffer data structure
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from numpy.typing import DTypeLike, NDArray
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CircularBuffer[T]:
|
|
30
|
+
"""Fixed-size circular buffer with O(1) operations.
|
|
31
|
+
|
|
32
|
+
Thread-safe for single producer, single consumer pattern.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
capacity: Maximum buffer size.
|
|
36
|
+
dtype: NumPy dtype for numeric buffers.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
capacity: Buffer capacity.
|
|
40
|
+
count: Current number of items.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> buf = CircularBuffer(1000, dtype=np.float64)
|
|
44
|
+
>>> for value in stream:
|
|
45
|
+
... buf.append(value)
|
|
46
|
+
... if buf.is_full():
|
|
47
|
+
... process(buf.get_all())
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
capacity: int,
|
|
53
|
+
dtype: DTypeLike | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Initialize circular buffer.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
capacity: Maximum buffer size.
|
|
59
|
+
dtype: NumPy dtype. If None, uses object array.
|
|
60
|
+
"""
|
|
61
|
+
self._capacity = capacity
|
|
62
|
+
self._dtype = dtype
|
|
63
|
+
|
|
64
|
+
if dtype is not None:
|
|
65
|
+
self._data: NDArray[Any] = np.zeros(capacity, dtype=dtype)
|
|
66
|
+
else:
|
|
67
|
+
self._data = np.empty(capacity, dtype=object)
|
|
68
|
+
|
|
69
|
+
self._head = 0 # Next write position
|
|
70
|
+
self._count = 0 # Number of valid items
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def capacity(self) -> int:
|
|
74
|
+
"""Get buffer capacity."""
|
|
75
|
+
return self._capacity
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def count(self) -> int:
|
|
79
|
+
"""Get current item count."""
|
|
80
|
+
return self._count
|
|
81
|
+
|
|
82
|
+
def is_empty(self) -> bool:
|
|
83
|
+
"""Check if buffer is empty."""
|
|
84
|
+
return self._count == 0
|
|
85
|
+
|
|
86
|
+
def is_full(self) -> bool:
|
|
87
|
+
"""Check if buffer is full."""
|
|
88
|
+
return self._count == self._capacity
|
|
89
|
+
|
|
90
|
+
def append(self, value: T) -> None:
|
|
91
|
+
"""Append value to buffer.
|
|
92
|
+
|
|
93
|
+
O(1) operation. Overwrites oldest value if full.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
value: Value to append.
|
|
97
|
+
"""
|
|
98
|
+
self._data[self._head] = value
|
|
99
|
+
self._head = (self._head + 1) % self._capacity
|
|
100
|
+
|
|
101
|
+
if self._count < self._capacity:
|
|
102
|
+
self._count += 1
|
|
103
|
+
|
|
104
|
+
def extend(self, values: list[T] | NDArray[Any]) -> None:
|
|
105
|
+
"""Extend buffer with multiple values.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
values: Values to append.
|
|
109
|
+
"""
|
|
110
|
+
for value in values:
|
|
111
|
+
self.append(value)
|
|
112
|
+
|
|
113
|
+
def get_last(self, n: int = 1) -> NDArray[Any]:
|
|
114
|
+
"""Get last n items.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
n: Number of items (default 1).
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Array of last n items (newest first).
|
|
121
|
+
"""
|
|
122
|
+
n = min(n, self._count)
|
|
123
|
+
|
|
124
|
+
if n == 0:
|
|
125
|
+
if self._dtype is not None:
|
|
126
|
+
return np.array([], dtype=self._dtype)
|
|
127
|
+
return np.array([], dtype=object)
|
|
128
|
+
|
|
129
|
+
result = np.empty(n, dtype=self._data.dtype)
|
|
130
|
+
|
|
131
|
+
for i in range(n):
|
|
132
|
+
idx = (self._head - 1 - i) % self._capacity
|
|
133
|
+
result[i] = self._data[idx]
|
|
134
|
+
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
def get_first(self, n: int = 1) -> NDArray[Any]:
|
|
138
|
+
"""Get first n items (oldest).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
n: Number of items.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Array of first n items (oldest first).
|
|
145
|
+
"""
|
|
146
|
+
n = min(n, self._count)
|
|
147
|
+
|
|
148
|
+
if n == 0:
|
|
149
|
+
if self._dtype is not None:
|
|
150
|
+
return np.array([], dtype=self._dtype)
|
|
151
|
+
return np.array([], dtype=object)
|
|
152
|
+
|
|
153
|
+
# Calculate tail position (oldest item)
|
|
154
|
+
tail = (self._head - self._count) % self._capacity
|
|
155
|
+
|
|
156
|
+
result = np.empty(n, dtype=self._data.dtype)
|
|
157
|
+
|
|
158
|
+
for i in range(n):
|
|
159
|
+
idx = (tail + i) % self._capacity
|
|
160
|
+
result[i] = self._data[idx]
|
|
161
|
+
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
def get_all(self) -> NDArray[Any]:
|
|
165
|
+
"""Get all items in order.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Array of all items (oldest first).
|
|
169
|
+
"""
|
|
170
|
+
return self.get_first(self._count)
|
|
171
|
+
|
|
172
|
+
@overload
|
|
173
|
+
def __getitem__(self, index: int) -> T: ...
|
|
174
|
+
|
|
175
|
+
@overload
|
|
176
|
+
def __getitem__(self, index: slice) -> NDArray[Any]: ...
|
|
177
|
+
|
|
178
|
+
def __getitem__(self, index: int | slice) -> T | NDArray[Any]:
|
|
179
|
+
"""Get item(s) by index.
|
|
180
|
+
|
|
181
|
+
Positive indices count from oldest (0 = oldest).
|
|
182
|
+
Negative indices count from newest (-1 = newest).
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
index: Integer index or slice.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Item or array of items.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
IndexError: If index out of range.
|
|
192
|
+
"""
|
|
193
|
+
if isinstance(index, slice):
|
|
194
|
+
# Convert slice to indices
|
|
195
|
+
start, stop, step = index.indices(self._count)
|
|
196
|
+
result = []
|
|
197
|
+
for i in range(start, stop, step):
|
|
198
|
+
result.append(self[i])
|
|
199
|
+
return np.array(result, dtype=self._data.dtype)
|
|
200
|
+
|
|
201
|
+
if index < 0:
|
|
202
|
+
index = self._count + index
|
|
203
|
+
|
|
204
|
+
if index < 0 or index >= self._count:
|
|
205
|
+
raise IndexError(f"Index {index} out of range [0, {self._count})")
|
|
206
|
+
|
|
207
|
+
# Calculate actual position
|
|
208
|
+
tail = (self._head - self._count) % self._capacity
|
|
209
|
+
actual_idx = (tail + index) % self._capacity
|
|
210
|
+
|
|
211
|
+
return self._data[actual_idx] # type: ignore[no-any-return]
|
|
212
|
+
|
|
213
|
+
def __len__(self) -> int:
|
|
214
|
+
"""Get current item count."""
|
|
215
|
+
return self._count
|
|
216
|
+
|
|
217
|
+
def clear(self) -> None:
|
|
218
|
+
"""Clear all items."""
|
|
219
|
+
self._head = 0
|
|
220
|
+
self._count = 0
|
|
221
|
+
|
|
222
|
+
def mean(self) -> float:
|
|
223
|
+
"""Compute mean of numeric buffer.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Mean value, or NaN if empty.
|
|
227
|
+
"""
|
|
228
|
+
if self._count == 0:
|
|
229
|
+
return float("nan")
|
|
230
|
+
|
|
231
|
+
return float(np.mean(self.get_all()))
|
|
232
|
+
|
|
233
|
+
def std(self) -> float:
|
|
234
|
+
"""Compute standard deviation.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Standard deviation, or NaN if empty.
|
|
238
|
+
"""
|
|
239
|
+
if self._count < 2:
|
|
240
|
+
return float("nan")
|
|
241
|
+
|
|
242
|
+
return float(np.std(self.get_all()))
|
|
243
|
+
|
|
244
|
+
def min(self) -> T:
|
|
245
|
+
"""Get minimum value.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Minimum value.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ValueError: If buffer is empty.
|
|
252
|
+
"""
|
|
253
|
+
if self._count == 0:
|
|
254
|
+
raise ValueError("Buffer is empty")
|
|
255
|
+
|
|
256
|
+
return np.min(self.get_all()) # type: ignore[no-any-return]
|
|
257
|
+
|
|
258
|
+
def max(self) -> T:
|
|
259
|
+
"""Get maximum value.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Maximum value.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
ValueError: If buffer is empty.
|
|
266
|
+
"""
|
|
267
|
+
if self._count == 0:
|
|
268
|
+
raise ValueError("Buffer is empty")
|
|
269
|
+
|
|
270
|
+
return np.max(self.get_all()) # type: ignore[no-any-return]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class SlidingWindow:
|
|
274
|
+
"""Sliding window for time-series analysis.
|
|
275
|
+
|
|
276
|
+
Maintains a window of samples based on time or count.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
window_size: Window size in samples or seconds.
|
|
280
|
+
time_based: If True, window_size is in seconds.
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> window = SlidingWindow(1000) # 1000 samples
|
|
284
|
+
>>> for sample, time in stream:
|
|
285
|
+
... window.add(sample, time)
|
|
286
|
+
... if window.is_ready():
|
|
287
|
+
... result = analyze(window.get_data())
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
def __init__(
|
|
291
|
+
self,
|
|
292
|
+
window_size: int | float,
|
|
293
|
+
time_based: bool = False,
|
|
294
|
+
dtype: DTypeLike = np.float64,
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Initialize sliding window.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
window_size: Size in samples or seconds.
|
|
300
|
+
time_based: True for time-based window.
|
|
301
|
+
dtype: Data type for samples.
|
|
302
|
+
"""
|
|
303
|
+
self._window_size = window_size
|
|
304
|
+
self._time_based = time_based
|
|
305
|
+
|
|
306
|
+
if time_based:
|
|
307
|
+
# Use large buffer for time-based
|
|
308
|
+
capacity = 100000
|
|
309
|
+
else:
|
|
310
|
+
capacity = int(window_size)
|
|
311
|
+
|
|
312
|
+
self._data = CircularBuffer(capacity, dtype=dtype) # type: ignore[var-annotated]
|
|
313
|
+
self._times = CircularBuffer(capacity, dtype=np.float64) # type: ignore[var-annotated]
|
|
314
|
+
|
|
315
|
+
def add(self, value: float, timestamp: float | None = None) -> None:
|
|
316
|
+
"""Add sample to window.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
value: Sample value.
|
|
320
|
+
timestamp: Sample timestamp (required for time-based).
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
ValueError: If timestamp is None for time-based window.
|
|
324
|
+
"""
|
|
325
|
+
self._data.append(value)
|
|
326
|
+
|
|
327
|
+
if self._time_based:
|
|
328
|
+
if timestamp is None:
|
|
329
|
+
raise ValueError("Timestamp required for time-based window")
|
|
330
|
+
self._times.append(timestamp)
|
|
331
|
+
|
|
332
|
+
def is_ready(self) -> bool:
|
|
333
|
+
"""Check if window is full."""
|
|
334
|
+
if self._time_based:
|
|
335
|
+
if self._times.count < 2:
|
|
336
|
+
return False
|
|
337
|
+
times = self._times.get_all()
|
|
338
|
+
duration = times[-1] - times[0]
|
|
339
|
+
return duration >= self._window_size # type: ignore[no-any-return]
|
|
340
|
+
else:
|
|
341
|
+
return self._data.count >= self._window_size
|
|
342
|
+
|
|
343
|
+
def get_data(self) -> NDArray[np.float64]:
|
|
344
|
+
"""Get window data.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Array of samples in window.
|
|
348
|
+
"""
|
|
349
|
+
if self._time_based:
|
|
350
|
+
# Get samples within time window
|
|
351
|
+
times = self._times.get_all()
|
|
352
|
+
data = self._data.get_all()
|
|
353
|
+
|
|
354
|
+
if len(times) == 0:
|
|
355
|
+
return np.array([], dtype=np.float64)
|
|
356
|
+
|
|
357
|
+
cutoff = times[-1] - self._window_size
|
|
358
|
+
mask = times >= cutoff
|
|
359
|
+
|
|
360
|
+
result: NDArray[np.float64] = data[mask]
|
|
361
|
+
return result
|
|
362
|
+
else:
|
|
363
|
+
result_all: NDArray[np.float64] = self._data.get_all()
|
|
364
|
+
return result_all
|
|
365
|
+
|
|
366
|
+
def get_times(self) -> NDArray[np.float64]:
|
|
367
|
+
"""Get timestamps for time-based window.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Array of timestamps.
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
ValueError: If not a time-based window.
|
|
374
|
+
"""
|
|
375
|
+
if not self._time_based:
|
|
376
|
+
raise ValueError("Not a time-based window")
|
|
377
|
+
|
|
378
|
+
return self._times.get_all()
|
|
379
|
+
|
|
380
|
+
def clear(self) -> None:
|
|
381
|
+
"""Clear window."""
|
|
382
|
+
self._data.clear()
|
|
383
|
+
self._times.clear()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
__all__ = [
|
|
387
|
+
"CircularBuffer",
|
|
388
|
+
"SlidingWindow",
|
|
389
|
+
]
|