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,622 @@
|
|
|
1
|
+
"""Real-time streaming APIs for live data acquisition and processing.
|
|
2
|
+
|
|
3
|
+
This module provides interfaces for real-time data capture, buffering, and
|
|
4
|
+
on-the-fly analysis of streaming waveforms. Supports pluggable input sources,
|
|
5
|
+
configurable sample buffers, and streaming statistics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from collections import deque
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from ..core.types import TraceMetadata, WaveformTrace
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Generator
|
|
23
|
+
|
|
24
|
+
from numpy.typing import NDArray
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class RealtimeConfig:
|
|
29
|
+
"""Configuration for real-time streaming."""
|
|
30
|
+
|
|
31
|
+
sample_rate: float
|
|
32
|
+
"""Sample rate in Hz."""
|
|
33
|
+
buffer_size: int = 10000
|
|
34
|
+
"""Size of the circular buffer in samples."""
|
|
35
|
+
chunk_size: int = 1000
|
|
36
|
+
"""Number of samples to yield per chunk."""
|
|
37
|
+
timeout: float = 10.0
|
|
38
|
+
"""Timeout in seconds for buffer operations."""
|
|
39
|
+
window_size: int | None = None
|
|
40
|
+
"""Window size for rolling statistics. If None, uses buffer_size."""
|
|
41
|
+
enable_validation: bool = True
|
|
42
|
+
"""Enable input validation."""
|
|
43
|
+
|
|
44
|
+
def validate(self) -> None:
|
|
45
|
+
"""Validate configuration parameters.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If configuration is invalid.
|
|
49
|
+
"""
|
|
50
|
+
if self.sample_rate <= 0:
|
|
51
|
+
raise ValueError("sample_rate must be positive")
|
|
52
|
+
|
|
53
|
+
if self.buffer_size <= 0:
|
|
54
|
+
raise ValueError("buffer_size must be positive")
|
|
55
|
+
|
|
56
|
+
if self.chunk_size <= 0:
|
|
57
|
+
raise ValueError("chunk_size must be positive")
|
|
58
|
+
|
|
59
|
+
if self.chunk_size > self.buffer_size:
|
|
60
|
+
raise ValueError("chunk_size cannot exceed buffer_size")
|
|
61
|
+
|
|
62
|
+
if self.timeout <= 0:
|
|
63
|
+
raise ValueError("timeout must be positive")
|
|
64
|
+
|
|
65
|
+
if self.window_size is not None and self.window_size <= 0:
|
|
66
|
+
raise ValueError("window_size must be positive")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RealtimeBuffer:
|
|
70
|
+
"""Thread-safe circular buffer for real-time streaming.
|
|
71
|
+
|
|
72
|
+
Maintains a fixed-size buffer of the most recent samples with
|
|
73
|
+
thread-safe read/write operations and overflow handling.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> buffer = RealtimeBuffer(config)
|
|
77
|
+
>>> buffer.write(samples)
|
|
78
|
+
>>> chunk = buffer.read(chunk_size)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, config: RealtimeConfig) -> None:
|
|
82
|
+
"""Initialize real-time buffer.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
config: Realtime configuration.
|
|
86
|
+
"""
|
|
87
|
+
config.validate()
|
|
88
|
+
self.config = config
|
|
89
|
+
|
|
90
|
+
self._buffer: deque[float] = deque(maxlen=config.buffer_size)
|
|
91
|
+
self._lock = threading.RLock()
|
|
92
|
+
self._not_empty = threading.Condition(self._lock)
|
|
93
|
+
self._total_samples = 0
|
|
94
|
+
self._overflow_count = 0
|
|
95
|
+
|
|
96
|
+
def write(self, data: NDArray[np.floating[Any]]) -> int:
|
|
97
|
+
"""Write samples to buffer.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
data: Array of samples to write.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Number of samples written.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
TypeError: If data is not numeric array.
|
|
107
|
+
"""
|
|
108
|
+
if not isinstance(data, np.ndarray):
|
|
109
|
+
raise TypeError("data must be numpy array")
|
|
110
|
+
|
|
111
|
+
if data.dtype.kind not in "fc": # float or complex
|
|
112
|
+
raise TypeError("data must be float or complex array")
|
|
113
|
+
|
|
114
|
+
with self._not_empty:
|
|
115
|
+
initial_len = len(self._buffer)
|
|
116
|
+
|
|
117
|
+
# Write samples to buffer
|
|
118
|
+
for sample in data.flat:
|
|
119
|
+
self._buffer.append(float(sample))
|
|
120
|
+
|
|
121
|
+
# Track overflow
|
|
122
|
+
if len(self._buffer) == self.config.buffer_size:
|
|
123
|
+
written = self.config.buffer_size - initial_len
|
|
124
|
+
if written < len(data):
|
|
125
|
+
self._overflow_count += len(data) - written
|
|
126
|
+
else:
|
|
127
|
+
written = len(data)
|
|
128
|
+
|
|
129
|
+
self._total_samples += len(data)
|
|
130
|
+
self._not_empty.notify_all()
|
|
131
|
+
|
|
132
|
+
return written
|
|
133
|
+
|
|
134
|
+
def read(self, n_samples: int, timeout: float | None = None) -> NDArray[np.float64]:
|
|
135
|
+
"""Read samples from buffer (blocking).
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
n_samples: Number of samples to read.
|
|
139
|
+
timeout: Timeout in seconds (None = use config timeout).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Array of samples, may be shorter if timeout occurs.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If n_samples is invalid.
|
|
146
|
+
TimeoutError: If timeout occurs without sufficient data.
|
|
147
|
+
"""
|
|
148
|
+
if n_samples <= 0:
|
|
149
|
+
raise ValueError("n_samples must be positive")
|
|
150
|
+
|
|
151
|
+
if timeout is None:
|
|
152
|
+
timeout = self.config.timeout
|
|
153
|
+
|
|
154
|
+
with self._not_empty:
|
|
155
|
+
# Wait for data with timeout
|
|
156
|
+
if not self._wait_for_data(n_samples, timeout):
|
|
157
|
+
if len(self._buffer) == 0:
|
|
158
|
+
raise TimeoutError("No data available")
|
|
159
|
+
|
|
160
|
+
# Read available samples
|
|
161
|
+
n_read = min(n_samples, len(self._buffer))
|
|
162
|
+
data = np.array(list(self._buffer)[:n_read], dtype=np.float64)
|
|
163
|
+
return data
|
|
164
|
+
|
|
165
|
+
def _wait_for_data(self, n_samples: int, timeout: float) -> bool:
|
|
166
|
+
"""Wait for minimum data in buffer.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
n_samples: Minimum samples to wait for.
|
|
170
|
+
timeout: Timeout in seconds.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if sufficient data available, False on timeout.
|
|
174
|
+
"""
|
|
175
|
+
deadline = time.time() + timeout
|
|
176
|
+
while len(self._buffer) < n_samples:
|
|
177
|
+
remaining = deadline - time.time()
|
|
178
|
+
if remaining <= 0:
|
|
179
|
+
return False
|
|
180
|
+
if not self._not_empty.wait(timeout=min(remaining, 0.1)):
|
|
181
|
+
continue
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
def get_available(self) -> int:
|
|
185
|
+
"""Get number of available samples in buffer.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Number of samples currently in buffer.
|
|
189
|
+
"""
|
|
190
|
+
with self._lock:
|
|
191
|
+
return len(self._buffer)
|
|
192
|
+
|
|
193
|
+
def get_stats(self) -> dict[str, int]:
|
|
194
|
+
"""Get buffer statistics.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dictionary with buffer stats (total_samples, overflow_count, available).
|
|
198
|
+
"""
|
|
199
|
+
with self._lock:
|
|
200
|
+
return {
|
|
201
|
+
"total_samples": self._total_samples,
|
|
202
|
+
"overflow_count": self._overflow_count,
|
|
203
|
+
"available": len(self._buffer),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
def clear(self) -> None:
|
|
207
|
+
"""Clear buffer contents.
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> buffer.clear()
|
|
211
|
+
"""
|
|
212
|
+
with self._lock:
|
|
213
|
+
self._buffer.clear()
|
|
214
|
+
self._total_samples = 0
|
|
215
|
+
self._overflow_count = 0
|
|
216
|
+
|
|
217
|
+
def close(self) -> None:
|
|
218
|
+
"""Close buffer and release resources.
|
|
219
|
+
|
|
220
|
+
Example:
|
|
221
|
+
>>> buffer.close()
|
|
222
|
+
"""
|
|
223
|
+
self.clear()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class RealtimeSource:
|
|
227
|
+
"""Base class for real-time data sources.
|
|
228
|
+
|
|
229
|
+
Subclass to implement custom data sources that feed the real-time
|
|
230
|
+
buffer. Must implement the acquire method.
|
|
231
|
+
|
|
232
|
+
Example:
|
|
233
|
+
>>> class CustomSource(RealtimeSource):
|
|
234
|
+
... def acquire(self) -> np.ndarray:
|
|
235
|
+
... # Get data from hardware
|
|
236
|
+
... return np.array([...])
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def acquire(self) -> NDArray[np.floating[Any]]:
|
|
240
|
+
"""Acquire samples from source.
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
NotImplementedError: Subclasses must implement.
|
|
244
|
+
"""
|
|
245
|
+
raise NotImplementedError("Subclasses must implement acquire()")
|
|
246
|
+
|
|
247
|
+
def start(self) -> None:
|
|
248
|
+
"""Start acquisition (optional).
|
|
249
|
+
|
|
250
|
+
Default implementation does nothing.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def stop(self) -> None:
|
|
254
|
+
"""Stop acquisition (optional).
|
|
255
|
+
|
|
256
|
+
Default implementation does nothing.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class SimulatedSource(RealtimeSource):
|
|
261
|
+
"""Simulated data source for testing and examples.
|
|
262
|
+
|
|
263
|
+
Generates synthetic waveforms (sine, square, noise) for real-time
|
|
264
|
+
streaming without requiring hardware.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
signal_type: Type of signal ("sine", "square", "noise", "mixed").
|
|
268
|
+
frequency: Signal frequency in Hz (for periodic signals).
|
|
269
|
+
amplitude: Signal amplitude.
|
|
270
|
+
sample_rate: Sample rate in Hz.
|
|
271
|
+
chunk_size: Number of samples per acquire() call.
|
|
272
|
+
noise_level: Noise amplitude (0-1 relative to signal).
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
>>> source = SimulatedSource("sine", frequency=1000, sample_rate=48000)
|
|
276
|
+
>>> data = source.acquire() # Get one chunk
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(
|
|
280
|
+
self,
|
|
281
|
+
signal_type: str = "sine",
|
|
282
|
+
*,
|
|
283
|
+
frequency: float = 1000.0,
|
|
284
|
+
amplitude: float = 1.0,
|
|
285
|
+
sample_rate: float = 48000.0,
|
|
286
|
+
chunk_size: int = 1024,
|
|
287
|
+
noise_level: float = 0.0,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Initialize simulated source."""
|
|
290
|
+
self.signal_type = signal_type
|
|
291
|
+
self.frequency = frequency
|
|
292
|
+
self.amplitude = amplitude
|
|
293
|
+
self.sample_rate = sample_rate
|
|
294
|
+
self.chunk_size = chunk_size
|
|
295
|
+
self.noise_level = noise_level
|
|
296
|
+
self._phase = 0.0
|
|
297
|
+
self._running = False
|
|
298
|
+
|
|
299
|
+
def acquire(self) -> NDArray[np.floating[Any]]:
|
|
300
|
+
"""Acquire one chunk of simulated data.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Array of simulated samples.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
ValueError: If signal_type is not recognized.
|
|
307
|
+
"""
|
|
308
|
+
t = np.arange(self.chunk_size) / self.sample_rate
|
|
309
|
+
t += self._phase / (2 * np.pi * self.frequency)
|
|
310
|
+
|
|
311
|
+
# Generate base signal
|
|
312
|
+
if self.signal_type == "sine":
|
|
313
|
+
signal = self.amplitude * np.sin(2 * np.pi * self.frequency * t)
|
|
314
|
+
elif self.signal_type == "square":
|
|
315
|
+
signal = self.amplitude * np.sign(np.sin(2 * np.pi * self.frequency * t))
|
|
316
|
+
elif self.signal_type == "noise":
|
|
317
|
+
signal = self.amplitude * np.random.randn(self.chunk_size)
|
|
318
|
+
elif self.signal_type == "mixed":
|
|
319
|
+
# Mix sine at frequency and sine at 3*frequency
|
|
320
|
+
signal = self.amplitude * (
|
|
321
|
+
np.sin(2 * np.pi * self.frequency * t)
|
|
322
|
+
+ 0.3 * np.sin(2 * np.pi * 3 * self.frequency * t)
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
raise ValueError(f"Unknown signal type: {self.signal_type}")
|
|
326
|
+
|
|
327
|
+
# Add noise if requested
|
|
328
|
+
if self.noise_level > 0:
|
|
329
|
+
noise = self.noise_level * self.amplitude * np.random.randn(self.chunk_size)
|
|
330
|
+
signal = signal + noise
|
|
331
|
+
|
|
332
|
+
# Update phase for continuity
|
|
333
|
+
self._phase = (
|
|
334
|
+
self._phase + 2 * np.pi * self.frequency * self.chunk_size / self.sample_rate
|
|
335
|
+
) % (2 * np.pi)
|
|
336
|
+
|
|
337
|
+
# Cast to the expected return type
|
|
338
|
+
return signal.astype(np.float64) # type: ignore[return-value,no-any-return]
|
|
339
|
+
|
|
340
|
+
def start(self) -> None:
|
|
341
|
+
"""Start acquisition."""
|
|
342
|
+
self._running = True
|
|
343
|
+
self._phase = 0.0
|
|
344
|
+
|
|
345
|
+
def stop(self) -> None:
|
|
346
|
+
"""Stop acquisition."""
|
|
347
|
+
self._running = False
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class RealtimeAnalyzer:
|
|
351
|
+
"""Analyzer for real-time streaming data.
|
|
352
|
+
|
|
353
|
+
Maintains rolling statistics and runs configurable analysis on
|
|
354
|
+
incoming data chunks.
|
|
355
|
+
|
|
356
|
+
Example:
|
|
357
|
+
>>> config = RealtimeConfig(sample_rate=1e6)
|
|
358
|
+
>>> analyzer = RealtimeAnalyzer(config)
|
|
359
|
+
>>> analyzer.accumulate(chunk)
|
|
360
|
+
>>> stats = analyzer.get_statistics()
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
def __init__(self, config: RealtimeConfig) -> None:
|
|
364
|
+
"""Initialize real-time analyzer.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
config: Realtime configuration.
|
|
368
|
+
"""
|
|
369
|
+
config.validate()
|
|
370
|
+
self.config = config
|
|
371
|
+
|
|
372
|
+
self._window_size = config.window_size or config.buffer_size
|
|
373
|
+
self._samples: deque[float] = deque(maxlen=self._window_size)
|
|
374
|
+
self._sum = 0.0
|
|
375
|
+
self._sum_sq = 0.0
|
|
376
|
+
self._min = float("inf")
|
|
377
|
+
self._max = float("-inf")
|
|
378
|
+
self._update_count = 0
|
|
379
|
+
|
|
380
|
+
def accumulate(self, data: NDArray[np.floating[Any]]) -> None:
|
|
381
|
+
"""Accumulate statistics from data chunk.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
data: Array of samples to process.
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
TypeError: If data is not numeric array.
|
|
388
|
+
"""
|
|
389
|
+
if not isinstance(data, np.ndarray):
|
|
390
|
+
raise TypeError("data must be numpy array")
|
|
391
|
+
|
|
392
|
+
if data.dtype.kind not in "fc":
|
|
393
|
+
raise TypeError("data must be float or complex array")
|
|
394
|
+
|
|
395
|
+
for sample in data.flat:
|
|
396
|
+
sample_float = float(sample)
|
|
397
|
+
|
|
398
|
+
# Remove oldest sample from stats if window full
|
|
399
|
+
if len(self._samples) == self._window_size:
|
|
400
|
+
old_sample = self._samples[0]
|
|
401
|
+
self._sum -= old_sample
|
|
402
|
+
self._sum_sq -= old_sample**2
|
|
403
|
+
|
|
404
|
+
# Add new sample
|
|
405
|
+
self._samples.append(sample_float)
|
|
406
|
+
self._sum += sample_float
|
|
407
|
+
self._sum_sq += sample_float**2
|
|
408
|
+
self._min = min(self._min, sample_float)
|
|
409
|
+
self._max = max(self._max, sample_float)
|
|
410
|
+
self._update_count += 1
|
|
411
|
+
|
|
412
|
+
def get_statistics(self) -> dict[str, float]:
|
|
413
|
+
"""Get current rolling statistics.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Dictionary with mean, std, min, max, peak_to_peak.
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
ValueError: If no data accumulated.
|
|
420
|
+
"""
|
|
421
|
+
if len(self._samples) == 0:
|
|
422
|
+
raise ValueError("No data accumulated yet")
|
|
423
|
+
|
|
424
|
+
n = len(self._samples)
|
|
425
|
+
mean = self._sum / n
|
|
426
|
+
variance = (self._sum_sq / n) - (mean**2)
|
|
427
|
+
std = np.sqrt(max(0, variance))
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
"mean": mean,
|
|
431
|
+
"std": std,
|
|
432
|
+
"min": self._min,
|
|
433
|
+
"max": self._max,
|
|
434
|
+
"peak_to_peak": self._max - self._min,
|
|
435
|
+
"n_samples": n,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
def reset(self) -> None:
|
|
439
|
+
"""Reset accumulated statistics.
|
|
440
|
+
|
|
441
|
+
Example:
|
|
442
|
+
>>> analyzer.reset()
|
|
443
|
+
"""
|
|
444
|
+
self._samples.clear()
|
|
445
|
+
self._sum = 0.0
|
|
446
|
+
self._sum_sq = 0.0
|
|
447
|
+
self._min = float("inf")
|
|
448
|
+
self._max = float("-inf")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class RealtimeStream:
|
|
452
|
+
"""High-level API for real-time data streaming and analysis.
|
|
453
|
+
|
|
454
|
+
Manages a data source, circular buffer, and analyzer for streaming
|
|
455
|
+
waveform processing.
|
|
456
|
+
|
|
457
|
+
Example:
|
|
458
|
+
>>> config = RealtimeConfig(sample_rate=1e6)
|
|
459
|
+
>>> source = CustomSource()
|
|
460
|
+
>>> stream = RealtimeStream(config, source)
|
|
461
|
+
>>> stream.start()
|
|
462
|
+
>>> for chunk in stream.iter_chunks(chunk_size=1000):
|
|
463
|
+
... print(chunk.data.mean())
|
|
464
|
+
>>> stream.stop()
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
def __init__(
|
|
468
|
+
self,
|
|
469
|
+
config: RealtimeConfig,
|
|
470
|
+
source: RealtimeSource,
|
|
471
|
+
on_chunk: Callable[[WaveformTrace], None] | None = None,
|
|
472
|
+
) -> None:
|
|
473
|
+
"""Initialize real-time stream.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
config: Realtime configuration.
|
|
477
|
+
source: Data source for acquisition.
|
|
478
|
+
on_chunk: Optional callback for each chunk acquired.
|
|
479
|
+
"""
|
|
480
|
+
config.validate()
|
|
481
|
+
self.config = config
|
|
482
|
+
self.source = source
|
|
483
|
+
self._on_chunk = on_chunk
|
|
484
|
+
|
|
485
|
+
self._buffer = RealtimeBuffer(config)
|
|
486
|
+
self._analyzer = RealtimeAnalyzer(config)
|
|
487
|
+
self._is_running = False
|
|
488
|
+
self._acquire_thread: threading.Thread | None = None
|
|
489
|
+
self._chunk_count = 0
|
|
490
|
+
|
|
491
|
+
def start(self) -> None:
|
|
492
|
+
"""Start acquisition thread.
|
|
493
|
+
|
|
494
|
+
Example:
|
|
495
|
+
>>> stream.start()
|
|
496
|
+
"""
|
|
497
|
+
if self._is_running:
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
self._is_running = True
|
|
501
|
+
self.source.start()
|
|
502
|
+
|
|
503
|
+
self._acquire_thread = threading.Thread(target=self._acquire_loop, daemon=True)
|
|
504
|
+
self._acquire_thread.start()
|
|
505
|
+
|
|
506
|
+
def stop(self) -> None:
|
|
507
|
+
"""Stop acquisition thread.
|
|
508
|
+
|
|
509
|
+
Example:
|
|
510
|
+
>>> stream.stop()
|
|
511
|
+
"""
|
|
512
|
+
if not self._is_running:
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
self._is_running = False
|
|
516
|
+
self.source.stop()
|
|
517
|
+
|
|
518
|
+
if self._acquire_thread is not None:
|
|
519
|
+
self._acquire_thread.join(timeout=5.0)
|
|
520
|
+
|
|
521
|
+
def iter_chunks(self) -> Generator[WaveformTrace, None, None]:
|
|
522
|
+
"""Iterate over data chunks as they arrive.
|
|
523
|
+
|
|
524
|
+
Yields chunks of configured size as data becomes available.
|
|
525
|
+
|
|
526
|
+
Yields:
|
|
527
|
+
WaveformTrace chunks.
|
|
528
|
+
|
|
529
|
+
Raises:
|
|
530
|
+
RuntimeError: If stream not started.
|
|
531
|
+
|
|
532
|
+
Example:
|
|
533
|
+
>>> for chunk in stream.iter_chunks():
|
|
534
|
+
... print(f"Chunk {chunk.metadata.start_index} has {len(chunk.data)} samples")
|
|
535
|
+
"""
|
|
536
|
+
if not self._is_running:
|
|
537
|
+
raise RuntimeError("Stream not started")
|
|
538
|
+
|
|
539
|
+
sample_index = 0
|
|
540
|
+
|
|
541
|
+
while self._is_running:
|
|
542
|
+
try:
|
|
543
|
+
data = self._buffer.read(self.config.chunk_size)
|
|
544
|
+
|
|
545
|
+
if len(data) > 0:
|
|
546
|
+
# Accumulate statistics
|
|
547
|
+
self._analyzer.accumulate(data)
|
|
548
|
+
|
|
549
|
+
# Create trace chunk
|
|
550
|
+
metadata = TraceMetadata(
|
|
551
|
+
sample_rate=self.config.sample_rate,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
chunk = WaveformTrace(data=data, metadata=metadata)
|
|
555
|
+
sample_index += len(data)
|
|
556
|
+
self._chunk_count += 1
|
|
557
|
+
|
|
558
|
+
# Call callback if provided
|
|
559
|
+
if self._on_chunk is not None:
|
|
560
|
+
self._on_chunk(chunk)
|
|
561
|
+
|
|
562
|
+
yield chunk
|
|
563
|
+
|
|
564
|
+
except TimeoutError:
|
|
565
|
+
# Check if stopped during timeout
|
|
566
|
+
# Note: _is_running may change asynchronously in another thread
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
def get_statistics(self) -> dict[str, float]:
|
|
570
|
+
"""Get current statistics.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Dictionary with stream statistics.
|
|
574
|
+
"""
|
|
575
|
+
try:
|
|
576
|
+
return self._analyzer.get_statistics()
|
|
577
|
+
except ValueError:
|
|
578
|
+
return {
|
|
579
|
+
"mean": 0.0,
|
|
580
|
+
"std": 0.0,
|
|
581
|
+
"min": 0.0,
|
|
582
|
+
"max": 0.0,
|
|
583
|
+
"peak_to_peak": 0.0,
|
|
584
|
+
"n_samples": 0,
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
def get_buffer_stats(self) -> dict[str, int]:
|
|
588
|
+
"""Get buffer statistics.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Dictionary with buffer stats.
|
|
592
|
+
"""
|
|
593
|
+
return self._buffer.get_stats()
|
|
594
|
+
|
|
595
|
+
def get_chunk_count(self) -> int:
|
|
596
|
+
"""Get total number of chunks acquired.
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
Number of chunks yielded so far.
|
|
600
|
+
"""
|
|
601
|
+
return self._chunk_count
|
|
602
|
+
|
|
603
|
+
def _acquire_loop(self) -> None:
|
|
604
|
+
"""Background thread that acquires data from source."""
|
|
605
|
+
while self._is_running:
|
|
606
|
+
try:
|
|
607
|
+
data = self.source.acquire()
|
|
608
|
+
if data is not None and len(data) > 0:
|
|
609
|
+
self._buffer.write(data)
|
|
610
|
+
except Exception:
|
|
611
|
+
if self._is_running:
|
|
612
|
+
time.sleep(0.001)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
__all__ = [
|
|
616
|
+
"RealtimeAnalyzer",
|
|
617
|
+
"RealtimeBuffer",
|
|
618
|
+
"RealtimeConfig",
|
|
619
|
+
"RealtimeSource",
|
|
620
|
+
"RealtimeStream",
|
|
621
|
+
"SimulatedSource",
|
|
622
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Testing utilities for Oscura.
|
|
2
|
+
|
|
3
|
+
This module provides synthetic test data generation with known ground truth
|
|
4
|
+
for validation and testing purposes.
|
|
5
|
+
|
|
6
|
+
Signal Generators
|
|
7
|
+
-----------------
|
|
8
|
+
These functions return WaveformTrace objects ready for use:
|
|
9
|
+
|
|
10
|
+
- generate_sine_wave: Pure sine wave
|
|
11
|
+
- generate_square_wave: Square wave with configurable duty cycle
|
|
12
|
+
- generate_dc: DC (constant) signal
|
|
13
|
+
- generate_multi_tone: Sum of multiple sine waves
|
|
14
|
+
- generate_pulse: Single pulse with configurable rise/fall times
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> from oscura.testing import generate_sine_wave, generate_square_wave
|
|
18
|
+
>>> sine = generate_sine_wave(frequency=1e6, amplitude=1.0)
|
|
19
|
+
>>> square = generate_square_wave(frequency=500e3, duty_cycle=0.3)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from oscura.testing.synthetic import (
|
|
23
|
+
GroundTruth,
|
|
24
|
+
SyntheticDataGenerator,
|
|
25
|
+
SyntheticMessageConfig,
|
|
26
|
+
SyntheticPacketConfig,
|
|
27
|
+
SyntheticSignalConfig,
|
|
28
|
+
generate_dc,
|
|
29
|
+
generate_digital_signal,
|
|
30
|
+
generate_multi_tone,
|
|
31
|
+
generate_packets,
|
|
32
|
+
generate_protocol_messages,
|
|
33
|
+
generate_pulse,
|
|
34
|
+
generate_sine_wave,
|
|
35
|
+
generate_square_wave,
|
|
36
|
+
generate_test_dataset,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"GroundTruth",
|
|
41
|
+
"SyntheticDataGenerator",
|
|
42
|
+
"SyntheticMessageConfig",
|
|
43
|
+
"SyntheticPacketConfig",
|
|
44
|
+
"SyntheticSignalConfig",
|
|
45
|
+
"generate_dc",
|
|
46
|
+
"generate_digital_signal",
|
|
47
|
+
"generate_multi_tone",
|
|
48
|
+
"generate_packets",
|
|
49
|
+
"generate_protocol_messages",
|
|
50
|
+
"generate_pulse",
|
|
51
|
+
"generate_sine_wave",
|
|
52
|
+
"generate_square_wave",
|
|
53
|
+
"generate_test_dataset",
|
|
54
|
+
]
|