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,808 @@
|
|
|
1
|
+
"""Synthetic test data generation with known ground truth.
|
|
2
|
+
|
|
3
|
+
Provides utilities for generating synthetic test data with known properties
|
|
4
|
+
for validation and testing purposes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import struct
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal, cast
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from numpy.typing import NDArray
|
|
14
|
+
|
|
15
|
+
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SyntheticPacketConfig:
|
|
20
|
+
"""Configuration for synthetic packet generation."""
|
|
21
|
+
|
|
22
|
+
packet_size: int = 1024
|
|
23
|
+
header_size: int = 16
|
|
24
|
+
sync_pattern: bytes = b"\xaa\x55"
|
|
25
|
+
include_sequence: bool = True
|
|
26
|
+
include_timestamp: bool = True
|
|
27
|
+
include_checksum: bool = True
|
|
28
|
+
checksum_algorithm: str = "crc16"
|
|
29
|
+
noise_level: float = 0.0 # 0-1, fraction of corrupted packets
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class SyntheticSignalConfig:
|
|
34
|
+
"""Configuration for synthetic digital signal."""
|
|
35
|
+
|
|
36
|
+
pattern_type: Literal["square", "uart", "spi", "i2c", "random"] = "square"
|
|
37
|
+
sample_rate: float = 100e6
|
|
38
|
+
duration_samples: int = 10000
|
|
39
|
+
frequency: float = 1e6 # For clock/square wave
|
|
40
|
+
noise_snr_db: float = 40 # Signal-to-noise ratio
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class SyntheticMessageConfig:
|
|
45
|
+
"""Configuration for synthetic protocol messages."""
|
|
46
|
+
|
|
47
|
+
message_size: int = 64
|
|
48
|
+
num_fields: int = 5
|
|
49
|
+
include_header: bool = True
|
|
50
|
+
include_length: bool = True
|
|
51
|
+
include_checksum: bool = True
|
|
52
|
+
variation: float = 0.1 # Fraction of variable bytes
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class GroundTruth:
|
|
57
|
+
"""Ground truth data for validation."""
|
|
58
|
+
|
|
59
|
+
field_boundaries: list[int] = field(default_factory=list)
|
|
60
|
+
field_types: list[str] = field(default_factory=list)
|
|
61
|
+
sequence_numbers: list[int] = field(default_factory=list)
|
|
62
|
+
pattern_period: int | None = None
|
|
63
|
+
cluster_labels: list[int] = field(default_factory=list)
|
|
64
|
+
checksum_offsets: list[int] = field(default_factory=list)
|
|
65
|
+
decoded_bytes: list[int] = field(default_factory=list)
|
|
66
|
+
edge_positions: list[int] = field(default_factory=list)
|
|
67
|
+
frequency_hz: float | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SyntheticDataGenerator:
|
|
71
|
+
"""Generate synthetic test data with known ground truth."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, seed: int = 42):
|
|
74
|
+
"""Initialize with random seed for reproducibility.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
seed: Random seed for reproducible generation.
|
|
78
|
+
"""
|
|
79
|
+
self.rng = np.random.default_rng(seed)
|
|
80
|
+
|
|
81
|
+
def generate_packets(
|
|
82
|
+
self, config: SyntheticPacketConfig, count: int = 100
|
|
83
|
+
) -> tuple[bytes, GroundTruth]:
|
|
84
|
+
"""Generate synthetic binary packets.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config: Packet generation configuration.
|
|
88
|
+
count: Number of packets to generate.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Tuple of (binary_data, ground_truth).
|
|
92
|
+
"""
|
|
93
|
+
packets = bytearray()
|
|
94
|
+
ground_truth = GroundTruth()
|
|
95
|
+
|
|
96
|
+
for i in range(count):
|
|
97
|
+
packet = bytearray()
|
|
98
|
+
|
|
99
|
+
# Sync pattern
|
|
100
|
+
packet.extend(config.sync_pattern)
|
|
101
|
+
|
|
102
|
+
# Sequence number (2 bytes)
|
|
103
|
+
if config.include_sequence:
|
|
104
|
+
packet.extend(struct.pack("<H", i))
|
|
105
|
+
ground_truth.sequence_numbers.append(i)
|
|
106
|
+
|
|
107
|
+
# Timestamp (4 bytes)
|
|
108
|
+
if config.include_timestamp:
|
|
109
|
+
timestamp = i * 1000 # Incrementing by 1000 microseconds
|
|
110
|
+
packet.extend(struct.pack("<I", timestamp))
|
|
111
|
+
|
|
112
|
+
# Padding to header size
|
|
113
|
+
while len(packet) < config.header_size:
|
|
114
|
+
packet.append(0x00)
|
|
115
|
+
|
|
116
|
+
# Sample data
|
|
117
|
+
sample_data_size = config.packet_size - config.header_size
|
|
118
|
+
if config.include_checksum:
|
|
119
|
+
sample_data_size -= 2
|
|
120
|
+
|
|
121
|
+
# Counter pattern for samples (easier to validate)
|
|
122
|
+
for j in range(sample_data_size // 2):
|
|
123
|
+
packet.extend(struct.pack("<H", j))
|
|
124
|
+
|
|
125
|
+
# Checksum (CRC-16)
|
|
126
|
+
if config.include_checksum:
|
|
127
|
+
checksum = self._calculate_crc16(packet)
|
|
128
|
+
checksum_offset = len(packets) + len(packet)
|
|
129
|
+
ground_truth.checksum_offsets.append(checksum_offset)
|
|
130
|
+
packet.extend(struct.pack("<H", checksum))
|
|
131
|
+
|
|
132
|
+
packets.extend(packet)
|
|
133
|
+
|
|
134
|
+
# Apply noise (corrupt random packets)
|
|
135
|
+
if config.noise_level > 0:
|
|
136
|
+
packets = bytearray(
|
|
137
|
+
self.corrupt_packets(bytes(packets), config.packet_size, config.noise_level)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return bytes(packets), ground_truth
|
|
141
|
+
|
|
142
|
+
def generate_digital_signal(
|
|
143
|
+
self, config: SyntheticSignalConfig
|
|
144
|
+
) -> tuple[NDArray[np.float64], GroundTruth]:
|
|
145
|
+
"""Generate synthetic digital signal.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
config: Signal generation configuration.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Tuple of (signal_array, ground_truth).
|
|
152
|
+
"""
|
|
153
|
+
ground_truth = GroundTruth()
|
|
154
|
+
signal: NDArray[np.float64]
|
|
155
|
+
|
|
156
|
+
if config.pattern_type == "square":
|
|
157
|
+
# Generate square wave
|
|
158
|
+
period_samples = int(config.sample_rate / config.frequency)
|
|
159
|
+
ground_truth.pattern_period = period_samples
|
|
160
|
+
ground_truth.frequency_hz = config.frequency
|
|
161
|
+
|
|
162
|
+
t = np.arange(config.duration_samples)
|
|
163
|
+
signal = (np.sin(2 * np.pi * config.frequency * t / config.sample_rate) > 0).astype(
|
|
164
|
+
np.float64
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Track edge positions
|
|
168
|
+
edges = np.where(np.diff(signal) != 0)[0] + 1
|
|
169
|
+
ground_truth.edge_positions = edges.tolist()
|
|
170
|
+
|
|
171
|
+
elif config.pattern_type == "uart":
|
|
172
|
+
# Generate UART signal (8N1)
|
|
173
|
+
signal, uart_truth = self._generate_uart_signal(config)
|
|
174
|
+
ground_truth.decoded_bytes = uart_truth["bytes"]
|
|
175
|
+
ground_truth.edge_positions = uart_truth["edges"]
|
|
176
|
+
|
|
177
|
+
elif config.pattern_type == "random":
|
|
178
|
+
# Random digital signal
|
|
179
|
+
signal = self.rng.choice([0.0, 1.0], size=config.duration_samples)
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
# Default to simple pattern
|
|
183
|
+
pattern = np.array([1, 1, 0, 1, 0, 0, 1, 0], dtype=np.float64)
|
|
184
|
+
signal = np.tile(pattern, config.duration_samples // len(pattern) + 1)[
|
|
185
|
+
: config.duration_samples
|
|
186
|
+
]
|
|
187
|
+
ground_truth.pattern_period = len(pattern)
|
|
188
|
+
|
|
189
|
+
# Scale to 3.3V logic levels
|
|
190
|
+
signal = signal * 3.3
|
|
191
|
+
|
|
192
|
+
# Add noise
|
|
193
|
+
if config.noise_snr_db < np.inf:
|
|
194
|
+
noisy_signal = self.add_noise(signal, config.noise_snr_db)
|
|
195
|
+
assert isinstance(noisy_signal, np.ndarray), (
|
|
196
|
+
"add_noise should return ndarray for ndarray input"
|
|
197
|
+
)
|
|
198
|
+
signal = noisy_signal
|
|
199
|
+
|
|
200
|
+
return signal, ground_truth
|
|
201
|
+
|
|
202
|
+
def generate_protocol_messages(
|
|
203
|
+
self, config: SyntheticMessageConfig, count: int = 100
|
|
204
|
+
) -> tuple[list[bytes], GroundTruth]:
|
|
205
|
+
"""Generate synthetic protocol messages.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
config: Message generation configuration.
|
|
209
|
+
count: Number of messages to generate.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Tuple of (message_list, ground_truth).
|
|
213
|
+
"""
|
|
214
|
+
messages = []
|
|
215
|
+
ground_truth = GroundTruth()
|
|
216
|
+
|
|
217
|
+
# Define field structure
|
|
218
|
+
field_boundaries = [0]
|
|
219
|
+
field_types = []
|
|
220
|
+
|
|
221
|
+
current_offset = 0
|
|
222
|
+
|
|
223
|
+
if config.include_header:
|
|
224
|
+
# 2-byte sync pattern
|
|
225
|
+
field_boundaries.append(current_offset + 2)
|
|
226
|
+
field_types.append("constant")
|
|
227
|
+
current_offset += 2
|
|
228
|
+
|
|
229
|
+
if config.include_length:
|
|
230
|
+
# 2-byte length field
|
|
231
|
+
field_boundaries.append(current_offset + 2)
|
|
232
|
+
field_types.append("length")
|
|
233
|
+
current_offset += 2
|
|
234
|
+
|
|
235
|
+
# Sequence number (2 bytes)
|
|
236
|
+
field_boundaries.append(current_offset + 2)
|
|
237
|
+
field_types.append("sequence")
|
|
238
|
+
current_offset += 2
|
|
239
|
+
|
|
240
|
+
# Timestamp (4 bytes)
|
|
241
|
+
field_boundaries.append(current_offset + 4)
|
|
242
|
+
field_types.append("timestamp")
|
|
243
|
+
current_offset += 4
|
|
244
|
+
|
|
245
|
+
# Payload (variable size)
|
|
246
|
+
payload_size = config.message_size - current_offset
|
|
247
|
+
if config.include_checksum:
|
|
248
|
+
payload_size -= 2
|
|
249
|
+
|
|
250
|
+
field_boundaries.append(current_offset + payload_size)
|
|
251
|
+
field_types.append("data")
|
|
252
|
+
current_offset += payload_size
|
|
253
|
+
|
|
254
|
+
if config.include_checksum:
|
|
255
|
+
field_boundaries.append(current_offset + 2)
|
|
256
|
+
field_types.append("checksum")
|
|
257
|
+
|
|
258
|
+
ground_truth.field_boundaries = field_boundaries
|
|
259
|
+
ground_truth.field_types = field_types
|
|
260
|
+
|
|
261
|
+
# Generate messages
|
|
262
|
+
for i in range(count):
|
|
263
|
+
message = bytearray()
|
|
264
|
+
|
|
265
|
+
if config.include_header:
|
|
266
|
+
message.extend(b"\xaa\x55")
|
|
267
|
+
|
|
268
|
+
if config.include_length:
|
|
269
|
+
message.extend(struct.pack("<H", config.message_size))
|
|
270
|
+
|
|
271
|
+
# Sequence number
|
|
272
|
+
message.extend(struct.pack("<H", i))
|
|
273
|
+
ground_truth.sequence_numbers.append(i)
|
|
274
|
+
|
|
275
|
+
# Timestamp
|
|
276
|
+
timestamp = i * 100 # Incrementing
|
|
277
|
+
message.extend(struct.pack("<I", timestamp))
|
|
278
|
+
|
|
279
|
+
# Payload (partially random, partially constant based on variation)
|
|
280
|
+
for _ in range(payload_size):
|
|
281
|
+
if self.rng.random() < config.variation:
|
|
282
|
+
message.append(self.rng.integers(0, 256))
|
|
283
|
+
else:
|
|
284
|
+
message.append(0x42) # Constant byte
|
|
285
|
+
|
|
286
|
+
# Checksum
|
|
287
|
+
if config.include_checksum:
|
|
288
|
+
checksum = self._calculate_crc16(message)
|
|
289
|
+
message.extend(struct.pack("<H", checksum))
|
|
290
|
+
|
|
291
|
+
messages.append(bytes(message))
|
|
292
|
+
|
|
293
|
+
return messages, ground_truth
|
|
294
|
+
|
|
295
|
+
def add_noise(
|
|
296
|
+
self, data: bytes | NDArray[np.float64], snr_db: float = 20
|
|
297
|
+
) -> bytes | NDArray[np.float64]:
|
|
298
|
+
"""Add noise to data.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
data: Input data (bytes or numpy array).
|
|
302
|
+
snr_db: Signal-to-noise ratio in dB.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Noisy data (same type as input).
|
|
306
|
+
"""
|
|
307
|
+
if isinstance(data, bytes):
|
|
308
|
+
# For bytes, add random bit flips
|
|
309
|
+
data_array = np.frombuffer(data, dtype=np.uint8)
|
|
310
|
+
noise_rate = 10 ** (-snr_db / 10)
|
|
311
|
+
mask = self.rng.random(len(data_array)) < noise_rate
|
|
312
|
+
noisy = data_array.copy()
|
|
313
|
+
noisy[mask] ^= self.rng.integers(1, 256, size=int(np.sum(mask)), dtype=np.uint8)
|
|
314
|
+
return noisy.tobytes()
|
|
315
|
+
else:
|
|
316
|
+
# For arrays, add Gaussian noise
|
|
317
|
+
signal_power = np.mean(data**2)
|
|
318
|
+
noise_power = signal_power / (10 ** (snr_db / 10))
|
|
319
|
+
noise = self.rng.normal(0, np.sqrt(noise_power), len(data))
|
|
320
|
+
return data + noise
|
|
321
|
+
|
|
322
|
+
def corrupt_packets(
|
|
323
|
+
self, packets: bytes, packet_size: int, corruption_rate: float = 0.01
|
|
324
|
+
) -> bytes:
|
|
325
|
+
"""Corrupt random packets for testing error handling.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
packets: Binary packet data.
|
|
329
|
+
packet_size: Size of each packet in bytes.
|
|
330
|
+
corruption_rate: Fraction of packets to corrupt (0-1).
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Corrupted packet data.
|
|
334
|
+
"""
|
|
335
|
+
packets_array = bytearray(packets)
|
|
336
|
+
num_packets = len(packets) // packet_size
|
|
337
|
+
|
|
338
|
+
for i in range(num_packets):
|
|
339
|
+
if self.rng.random() < corruption_rate:
|
|
340
|
+
# Corrupt sync marker
|
|
341
|
+
offset = i * packet_size
|
|
342
|
+
packets_array[offset] ^= 0xFF
|
|
343
|
+
|
|
344
|
+
return bytes(packets_array)
|
|
345
|
+
|
|
346
|
+
def _calculate_crc16(self, data: bytes | bytearray) -> int:
|
|
347
|
+
"""Calculate CRC-16 checksum.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
data: Input data.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
CRC-16 checksum value.
|
|
354
|
+
"""
|
|
355
|
+
crc = 0xFFFF
|
|
356
|
+
for byte in data:
|
|
357
|
+
crc ^= byte
|
|
358
|
+
for _ in range(8):
|
|
359
|
+
if crc & 0x0001:
|
|
360
|
+
crc = (crc >> 1) ^ 0xA001
|
|
361
|
+
else:
|
|
362
|
+
crc >>= 1
|
|
363
|
+
return crc & 0xFFFF
|
|
364
|
+
|
|
365
|
+
def _generate_uart_signal(
|
|
366
|
+
self, config: SyntheticSignalConfig
|
|
367
|
+
) -> tuple[NDArray[np.float64], dict[str, Any]]:
|
|
368
|
+
"""Generate UART signal encoding a test message.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
config: Signal configuration.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Tuple of (signal, metadata_dict).
|
|
375
|
+
"""
|
|
376
|
+
# UART parameters
|
|
377
|
+
baud_rate = 9600
|
|
378
|
+
samples_per_bit = int(config.sample_rate / baud_rate)
|
|
379
|
+
|
|
380
|
+
# Test message
|
|
381
|
+
message = b"Hello, World!"
|
|
382
|
+
|
|
383
|
+
# Encode with start/stop bits
|
|
384
|
+
bits = []
|
|
385
|
+
edges = []
|
|
386
|
+
|
|
387
|
+
for byte_val in message:
|
|
388
|
+
# Start bit (0)
|
|
389
|
+
bits.extend([0] * samples_per_bit)
|
|
390
|
+
edges.append(len(bits))
|
|
391
|
+
|
|
392
|
+
# Data bits (LSB first)
|
|
393
|
+
for i in range(8):
|
|
394
|
+
bit = (byte_val >> i) & 1
|
|
395
|
+
bits.extend([bit] * samples_per_bit)
|
|
396
|
+
if i > 0 and bits[-1] != bits[-samples_per_bit - 1]:
|
|
397
|
+
edges.append(len(bits) - samples_per_bit)
|
|
398
|
+
|
|
399
|
+
# Stop bit (1)
|
|
400
|
+
bits.extend([1] * samples_per_bit)
|
|
401
|
+
if bits[-1] != bits[-samples_per_bit - 1]:
|
|
402
|
+
edges.append(len(bits) - samples_per_bit)
|
|
403
|
+
|
|
404
|
+
# Pad to duration
|
|
405
|
+
signal = np.array(bits[: config.duration_samples], dtype=np.float64)
|
|
406
|
+
if len(signal) < config.duration_samples:
|
|
407
|
+
padding = np.ones(config.duration_samples - len(signal), dtype=np.float64)
|
|
408
|
+
signal = np.concatenate([signal, padding])
|
|
409
|
+
|
|
410
|
+
metadata = {"bytes": list(message), "edges": edges[: len(signal)]}
|
|
411
|
+
|
|
412
|
+
return signal, metadata
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# =============================================================================
|
|
416
|
+
# Convenience functions for generating WaveformTrace objects
|
|
417
|
+
# =============================================================================
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def generate_sine_wave(
|
|
421
|
+
frequency: float = 1e6,
|
|
422
|
+
amplitude: float = 1.0,
|
|
423
|
+
sample_rate: float = 100e6,
|
|
424
|
+
duration: float = 10e-6,
|
|
425
|
+
offset: float = 0.0,
|
|
426
|
+
phase: float = 0.0,
|
|
427
|
+
noise_level: float = 0.0,
|
|
428
|
+
) -> WaveformTrace:
|
|
429
|
+
"""Generate a sine wave WaveformTrace.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
frequency: Signal frequency in Hz.
|
|
433
|
+
amplitude: Peak amplitude (will produce 2*amplitude peak-to-peak).
|
|
434
|
+
sample_rate: Sample rate in Hz.
|
|
435
|
+
duration: Duration in seconds.
|
|
436
|
+
offset: DC offset.
|
|
437
|
+
phase: Initial phase in radians.
|
|
438
|
+
noise_level: RMS noise level to add (0 for clean signal).
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
WaveformTrace containing the sine wave.
|
|
442
|
+
|
|
443
|
+
Example:
|
|
444
|
+
>>> from oscura.testing import generate_sine_wave
|
|
445
|
+
>>> trace = generate_sine_wave(frequency=1e6, amplitude=1.0)
|
|
446
|
+
>>> print(f"Samples: {len(trace.data)}")
|
|
447
|
+
"""
|
|
448
|
+
num_samples = int(sample_rate * duration)
|
|
449
|
+
t = np.arange(num_samples) / sample_rate
|
|
450
|
+
data = amplitude * np.sin(2 * np.pi * frequency * t + phase) + offset
|
|
451
|
+
|
|
452
|
+
if noise_level > 0:
|
|
453
|
+
rng = np.random.default_rng(42)
|
|
454
|
+
data = data + rng.normal(0, noise_level, num_samples)
|
|
455
|
+
|
|
456
|
+
metadata = TraceMetadata(sample_rate=sample_rate)
|
|
457
|
+
return WaveformTrace(data=data.astype(np.float64), metadata=metadata)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def generate_square_wave(
|
|
461
|
+
frequency: float = 1e6,
|
|
462
|
+
duty_cycle: float = 0.5,
|
|
463
|
+
sample_rate: float = 100e6,
|
|
464
|
+
duration: float = 10e-6,
|
|
465
|
+
low: float = 0.0,
|
|
466
|
+
high: float = 1.0,
|
|
467
|
+
noise_level: float = 0.0,
|
|
468
|
+
) -> WaveformTrace:
|
|
469
|
+
"""Generate a square wave WaveformTrace.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
frequency: Signal frequency in Hz.
|
|
473
|
+
duty_cycle: Duty cycle (0.0 to 1.0).
|
|
474
|
+
sample_rate: Sample rate in Hz.
|
|
475
|
+
duration: Duration in seconds.
|
|
476
|
+
low: Low voltage level.
|
|
477
|
+
high: High voltage level.
|
|
478
|
+
noise_level: RMS noise level to add.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
WaveformTrace containing the square wave.
|
|
482
|
+
|
|
483
|
+
Example:
|
|
484
|
+
>>> from oscura.testing import generate_square_wave
|
|
485
|
+
>>> trace = generate_square_wave(frequency=500e3, duty_cycle=0.3)
|
|
486
|
+
"""
|
|
487
|
+
num_samples = int(sample_rate * duration)
|
|
488
|
+
t = np.arange(num_samples) / sample_rate
|
|
489
|
+
period = 1.0 / frequency
|
|
490
|
+
|
|
491
|
+
# Create square wave using modulo operation
|
|
492
|
+
phase = (t % period) / period
|
|
493
|
+
data = np.where(phase < duty_cycle, high, low).astype(np.float64)
|
|
494
|
+
|
|
495
|
+
if noise_level > 0:
|
|
496
|
+
rng = np.random.default_rng(42)
|
|
497
|
+
data = data + rng.normal(0, noise_level, num_samples)
|
|
498
|
+
|
|
499
|
+
metadata = TraceMetadata(sample_rate=sample_rate)
|
|
500
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def generate_dc(
|
|
504
|
+
level: float = 1.0,
|
|
505
|
+
sample_rate: float = 100e6,
|
|
506
|
+
duration: float = 10e-6,
|
|
507
|
+
noise_level: float = 0.0,
|
|
508
|
+
) -> WaveformTrace:
|
|
509
|
+
"""Generate a DC (constant) signal WaveformTrace.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
level: DC voltage level.
|
|
513
|
+
sample_rate: Sample rate in Hz.
|
|
514
|
+
duration: Duration in seconds.
|
|
515
|
+
noise_level: RMS noise level to add.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
WaveformTrace containing the DC signal.
|
|
519
|
+
|
|
520
|
+
Example:
|
|
521
|
+
>>> from oscura.testing import generate_dc
|
|
522
|
+
>>> trace = generate_dc(level=1.5, duration=10e-6)
|
|
523
|
+
"""
|
|
524
|
+
num_samples = int(sample_rate * duration)
|
|
525
|
+
data = np.full(num_samples, level, dtype=np.float64)
|
|
526
|
+
|
|
527
|
+
if noise_level > 0:
|
|
528
|
+
rng = np.random.default_rng(42)
|
|
529
|
+
data = data + rng.normal(0, noise_level, num_samples)
|
|
530
|
+
|
|
531
|
+
metadata = TraceMetadata(sample_rate=sample_rate)
|
|
532
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def generate_multi_tone(
|
|
536
|
+
frequencies: list[float],
|
|
537
|
+
amplitudes: list[float] | None = None,
|
|
538
|
+
phases: list[float] | None = None,
|
|
539
|
+
sample_rate: float = 100e6,
|
|
540
|
+
duration: float = 100e-6,
|
|
541
|
+
noise_level: float = 0.0,
|
|
542
|
+
) -> WaveformTrace:
|
|
543
|
+
"""Generate a multi-tone (sum of sine waves) WaveformTrace.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
frequencies: List of frequencies in Hz.
|
|
547
|
+
amplitudes: List of amplitudes for each tone. If None, all 1.0.
|
|
548
|
+
phases: List of phases in radians. If None, all 0.0.
|
|
549
|
+
sample_rate: Sample rate in Hz.
|
|
550
|
+
duration: Duration in seconds.
|
|
551
|
+
noise_level: RMS noise level to add.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
WaveformTrace containing the multi-tone signal.
|
|
555
|
+
|
|
556
|
+
Raises:
|
|
557
|
+
ValueError: If frequencies, amplitudes, and phases have different lengths.
|
|
558
|
+
|
|
559
|
+
Example:
|
|
560
|
+
>>> from oscura.testing import generate_multi_tone
|
|
561
|
+
>>> trace = generate_multi_tone(
|
|
562
|
+
... frequencies=[1e6, 2.5e6, 4e6],
|
|
563
|
+
... amplitudes=[1.0, 0.5, 0.25]
|
|
564
|
+
... )
|
|
565
|
+
"""
|
|
566
|
+
if amplitudes is None:
|
|
567
|
+
amplitudes = [1.0] * len(frequencies)
|
|
568
|
+
if phases is None:
|
|
569
|
+
phases = [0.0] * len(frequencies)
|
|
570
|
+
|
|
571
|
+
if len(frequencies) != len(amplitudes) or len(frequencies) != len(phases):
|
|
572
|
+
raise ValueError("frequencies, amplitudes, and phases must have same length")
|
|
573
|
+
|
|
574
|
+
num_samples = int(sample_rate * duration)
|
|
575
|
+
t = np.arange(num_samples) / sample_rate
|
|
576
|
+
data = np.zeros(num_samples, dtype=np.float64)
|
|
577
|
+
|
|
578
|
+
for freq, amp, phase in zip(frequencies, amplitudes, phases, strict=True):
|
|
579
|
+
data += amp * np.sin(2 * np.pi * freq * t + phase)
|
|
580
|
+
|
|
581
|
+
if noise_level > 0:
|
|
582
|
+
rng = np.random.default_rng(42)
|
|
583
|
+
data = data + rng.normal(0, noise_level, num_samples)
|
|
584
|
+
|
|
585
|
+
metadata = TraceMetadata(sample_rate=sample_rate)
|
|
586
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def generate_pulse(
|
|
590
|
+
width: float = 1e-6,
|
|
591
|
+
rise_time: float = 10e-9,
|
|
592
|
+
fall_time: float = 10e-9,
|
|
593
|
+
sample_rate: float = 1e9,
|
|
594
|
+
duration: float = 10e-6,
|
|
595
|
+
low: float = 0.0,
|
|
596
|
+
high: float = 1.0,
|
|
597
|
+
pulse_position: float = 0.5,
|
|
598
|
+
overshoot: float = 0.0,
|
|
599
|
+
) -> WaveformTrace:
|
|
600
|
+
"""Generate a pulse WaveformTrace with configurable rise/fall times.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
width: Pulse width in seconds.
|
|
604
|
+
rise_time: Rise time (10%-90%) in seconds.
|
|
605
|
+
fall_time: Fall time (90%-10%) in seconds.
|
|
606
|
+
sample_rate: Sample rate in Hz.
|
|
607
|
+
duration: Duration in seconds.
|
|
608
|
+
low: Low voltage level.
|
|
609
|
+
high: High voltage level.
|
|
610
|
+
pulse_position: Position of pulse center as fraction of duration.
|
|
611
|
+
overshoot: Overshoot as fraction of amplitude (0 for none).
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
WaveformTrace containing the pulse.
|
|
615
|
+
|
|
616
|
+
Example:
|
|
617
|
+
>>> from oscura.testing import generate_pulse
|
|
618
|
+
>>> trace = generate_pulse(width=1e-6, rise_time=10e-9)
|
|
619
|
+
"""
|
|
620
|
+
num_samples = int(sample_rate * duration)
|
|
621
|
+
t = np.arange(num_samples) / sample_rate
|
|
622
|
+
data = np.full(num_samples, low, dtype=np.float64)
|
|
623
|
+
|
|
624
|
+
# Pulse timing
|
|
625
|
+
center = duration * pulse_position
|
|
626
|
+
start = center - width / 2
|
|
627
|
+
end = center + width / 2
|
|
628
|
+
|
|
629
|
+
amplitude = high - low
|
|
630
|
+
|
|
631
|
+
for i, time in enumerate(t):
|
|
632
|
+
if time < start:
|
|
633
|
+
data[i] = low
|
|
634
|
+
elif time < start + rise_time:
|
|
635
|
+
# Rising edge (exponential approach)
|
|
636
|
+
progress = (time - start) / rise_time
|
|
637
|
+
data[i] = low + amplitude * progress
|
|
638
|
+
if overshoot > 0 and progress > 0.9:
|
|
639
|
+
data[i] += amplitude * overshoot * np.sin(np.pi * (progress - 0.9) / 0.1)
|
|
640
|
+
elif time < end:
|
|
641
|
+
data[i] = high
|
|
642
|
+
elif time < end + fall_time:
|
|
643
|
+
# Falling edge
|
|
644
|
+
progress = (time - end) / fall_time
|
|
645
|
+
data[i] = high - amplitude * progress
|
|
646
|
+
else:
|
|
647
|
+
data[i] = low
|
|
648
|
+
|
|
649
|
+
metadata = TraceMetadata(sample_rate=sample_rate)
|
|
650
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
# =============================================================================
|
|
654
|
+
# Legacy convenience functions (kept for backward compatibility)
|
|
655
|
+
# =============================================================================
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def generate_packets(count: int = 100, **kwargs: Any) -> tuple[bytes, GroundTruth]:
|
|
659
|
+
"""Generate synthetic packets with defaults.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
count: Number of packets to generate.
|
|
663
|
+
**kwargs: Additional configuration parameters.
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
Tuple of (binary_data, ground_truth).
|
|
667
|
+
"""
|
|
668
|
+
config = SyntheticPacketConfig(**kwargs)
|
|
669
|
+
generator = SyntheticDataGenerator()
|
|
670
|
+
return generator.generate_packets(config, count)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def generate_digital_signal(
|
|
674
|
+
pattern: str = "square", **kwargs: Any
|
|
675
|
+
) -> tuple[NDArray[np.float64], GroundTruth]:
|
|
676
|
+
"""Generate synthetic signal with defaults.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
pattern: Pattern type ('square', 'uart', 'random', etc.).
|
|
680
|
+
**kwargs: Additional configuration parameters.
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Tuple of (signal_array, ground_truth).
|
|
684
|
+
"""
|
|
685
|
+
# Determine pattern type
|
|
686
|
+
valid_patterns = ["square", "uart", "spi", "i2c", "random"]
|
|
687
|
+
pattern_type = pattern if pattern in valid_patterns else "square"
|
|
688
|
+
|
|
689
|
+
# Filter out pattern_type from kwargs to avoid duplicate argument error
|
|
690
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k != "pattern_type"}
|
|
691
|
+
|
|
692
|
+
config = SyntheticSignalConfig(
|
|
693
|
+
pattern_type=cast("Literal['square', 'uart', 'spi', 'i2c', 'random']", pattern_type),
|
|
694
|
+
**filtered_kwargs,
|
|
695
|
+
)
|
|
696
|
+
generator = SyntheticDataGenerator()
|
|
697
|
+
return generator.generate_digital_signal(config)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def generate_protocol_messages(count: int = 100, **kwargs: Any) -> tuple[list[bytes], GroundTruth]:
|
|
701
|
+
"""Generate synthetic messages with defaults.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
count: Number of messages to generate.
|
|
705
|
+
**kwargs: Additional configuration parameters.
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
Tuple of (message_list, ground_truth).
|
|
709
|
+
"""
|
|
710
|
+
config = SyntheticMessageConfig(**kwargs)
|
|
711
|
+
generator = SyntheticDataGenerator()
|
|
712
|
+
return generator.generate_protocol_messages(config, count)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def generate_test_dataset(
|
|
716
|
+
output_dir: str,
|
|
717
|
+
num_packets: int = 1000,
|
|
718
|
+
num_signals: int = 10,
|
|
719
|
+
num_messages: int = 500,
|
|
720
|
+
) -> dict[str, Any]:
|
|
721
|
+
"""Generate complete test dataset with ground truth.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
output_dir: Directory to save test data.
|
|
725
|
+
num_packets: Number of packets to generate.
|
|
726
|
+
num_signals: Number of signals to generate.
|
|
727
|
+
num_messages: Number of messages to generate.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
Dictionary with dataset metadata and file paths.
|
|
731
|
+
"""
|
|
732
|
+
output_path = Path(output_dir)
|
|
733
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
734
|
+
|
|
735
|
+
generator = SyntheticDataGenerator()
|
|
736
|
+
metadata: dict[str, Any] = {
|
|
737
|
+
"dataset_type": "synthetic_test_data",
|
|
738
|
+
"generated_files": [],
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
# Generate packets
|
|
742
|
+
packet_config = SyntheticPacketConfig()
|
|
743
|
+
packets, packet_truth = generator.generate_packets(packet_config, num_packets)
|
|
744
|
+
packet_file = output_path / "test_packets.bin"
|
|
745
|
+
packet_file.write_bytes(packets)
|
|
746
|
+
metadata["generated_files"].append(
|
|
747
|
+
{
|
|
748
|
+
"path": str(packet_file),
|
|
749
|
+
"type": "packets",
|
|
750
|
+
"count": num_packets,
|
|
751
|
+
"ground_truth": {
|
|
752
|
+
"sequence_numbers": packet_truth.sequence_numbers[:10], # First 10
|
|
753
|
+
"checksum_offsets": packet_truth.checksum_offsets[:10],
|
|
754
|
+
},
|
|
755
|
+
}
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
# Generate signals
|
|
759
|
+
for i in range(num_signals):
|
|
760
|
+
signal_config = SyntheticSignalConfig(
|
|
761
|
+
pattern_type="square" if i % 2 == 0 else "uart",
|
|
762
|
+
frequency=1e6 * (i + 1),
|
|
763
|
+
)
|
|
764
|
+
signal, signal_truth = generator.generate_digital_signal(signal_config)
|
|
765
|
+
signal_file = output_path / f"test_signal_{i:03d}.npy"
|
|
766
|
+
np.save(signal_file, signal)
|
|
767
|
+
metadata["generated_files"].append(
|
|
768
|
+
{
|
|
769
|
+
"path": str(signal_file),
|
|
770
|
+
"type": "signal",
|
|
771
|
+
"pattern": signal_config.pattern_type,
|
|
772
|
+
"ground_truth": {
|
|
773
|
+
"frequency_hz": signal_truth.frequency_hz,
|
|
774
|
+
"period_samples": signal_truth.pattern_period,
|
|
775
|
+
},
|
|
776
|
+
}
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# Generate protocol messages
|
|
780
|
+
message_config = SyntheticMessageConfig()
|
|
781
|
+
messages, message_truth = generator.generate_protocol_messages(message_config, num_messages)
|
|
782
|
+
messages_file = output_path / "test_messages.bin"
|
|
783
|
+
with messages_file.open("wb") as f:
|
|
784
|
+
for msg in messages:
|
|
785
|
+
f.write(msg)
|
|
786
|
+
metadata["generated_files"].append(
|
|
787
|
+
{
|
|
788
|
+
"path": str(messages_file),
|
|
789
|
+
"type": "messages",
|
|
790
|
+
"count": num_messages,
|
|
791
|
+
"ground_truth": {
|
|
792
|
+
"field_boundaries": message_truth.field_boundaries,
|
|
793
|
+
"field_types": message_truth.field_types,
|
|
794
|
+
"message_size": message_config.message_size,
|
|
795
|
+
},
|
|
796
|
+
}
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
# Save metadata
|
|
800
|
+
import json
|
|
801
|
+
|
|
802
|
+
metadata_file = output_path / "dataset_metadata.json"
|
|
803
|
+
with metadata_file.open("w") as f:
|
|
804
|
+
json.dump(metadata, f, indent=2)
|
|
805
|
+
|
|
806
|
+
metadata["metadata_file"] = str(metadata_file)
|
|
807
|
+
|
|
808
|
+
return metadata
|