oscura 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.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