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,1131 @@
1
+ """Fluent SignalBuilder for composable signal generation.
2
+
3
+ This module provides the main SignalBuilder class that allows fluent
4
+ composition of signals for test data generation, demos, and protocol testing.
5
+
6
+ Example:
7
+ >>> from oscura import SignalBuilder
8
+ >>> signal = (SignalBuilder()
9
+ ... .sample_rate(10e6)
10
+ ... .duration(0.01)
11
+ ... .add_sine(frequency=1000, amplitude=1.0)
12
+ ... .add_noise(snr_db=40)
13
+ ... .build())
14
+
15
+ The builder supports:
16
+ - Analog waveforms (sine, square, triangle, chirp, multitone)
17
+ - Protocol signals (UART, SPI, I2C, CAN)
18
+ - Noise and impairments (gaussian, pink, jitter, quantization)
19
+ - Multi-channel signals
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import Any, Literal
27
+
28
+ import numpy as np
29
+ from scipy import signal as scipy_signal
30
+
31
+ from oscura.core.types import TraceMetadata, WaveformTrace
32
+
33
+
34
+ @dataclass
35
+ class SignalMetadata:
36
+ """Metadata for generated signals.
37
+
38
+ Attributes:
39
+ sample_rate: Sample rate in Hz.
40
+ duration: Signal duration in seconds.
41
+ channel_names: List of channel names.
42
+ description: Human-readable description.
43
+ generator: Name of generator that created this signal.
44
+ parameters: Dictionary of generation parameters.
45
+ """
46
+
47
+ sample_rate: float
48
+ duration: float
49
+ channel_names: list[str] = field(default_factory=lambda: ["ch1"])
50
+ description: str = ""
51
+ generator: str = "SignalBuilder"
52
+ parameters: dict[str, Any] = field(default_factory=dict)
53
+
54
+
55
+ @dataclass
56
+ class GeneratedSignal:
57
+ """Container for generated signal data.
58
+
59
+ Attributes:
60
+ data: Dictionary mapping channel names to signal arrays.
61
+ metadata: Signal metadata.
62
+ """
63
+
64
+ data: dict[str, np.ndarray[Any, np.dtype[np.float64]]]
65
+ metadata: SignalMetadata
66
+ _time: np.ndarray[Any, np.dtype[np.float64]] | None = field(default=None, repr=False)
67
+
68
+ @property
69
+ def time(self) -> np.ndarray[Any, np.dtype[np.float64]]:
70
+ """Get time array, computing if necessary."""
71
+ if self._time is None:
72
+ n_samples = len(next(iter(self.data.values())))
73
+ self._time = np.arange(n_samples) / self.metadata.sample_rate
74
+ return self._time
75
+
76
+ @property
77
+ def num_channels(self) -> int:
78
+ """Number of channels in signal."""
79
+ return len(self.data)
80
+
81
+ @property
82
+ def num_samples(self) -> int:
83
+ """Number of samples per channel."""
84
+ return len(next(iter(self.data.values())))
85
+
86
+ def get_channel(self, name: str) -> np.ndarray[Any, np.dtype[np.float64]]:
87
+ """Get signal data for a specific channel.
88
+
89
+ Args:
90
+ name: Channel name.
91
+
92
+ Returns:
93
+ Signal array for the channel.
94
+
95
+ Raises:
96
+ KeyError: If channel name not found.
97
+ """
98
+ if name not in self.data:
99
+ available = list(self.data.keys())
100
+ raise KeyError(f"Channel '{name}' not found. Available: {available}")
101
+ return self.data[name]
102
+
103
+ def to_trace(self, channel: str | None = None) -> WaveformTrace:
104
+ """Convert to WaveformTrace for TraceKit analysis.
105
+
106
+ Args:
107
+ channel: Channel name to convert. If None, uses first channel.
108
+
109
+ Returns:
110
+ WaveformTrace instance ready for analysis.
111
+ """
112
+ if channel is None:
113
+ channel = self.metadata.channel_names[0]
114
+
115
+ data = self.get_channel(channel)
116
+ trace_meta = TraceMetadata(
117
+ sample_rate=self.metadata.sample_rate,
118
+ channel_name=channel,
119
+ )
120
+ return WaveformTrace(data=data, metadata=trace_meta)
121
+
122
+ def save_npz(self, path: Path | str) -> None:
123
+ """Save signal to NPZ format.
124
+
125
+ Args:
126
+ path: Output file path.
127
+ """
128
+ path = Path(path)
129
+ path.parent.mkdir(parents=True, exist_ok=True)
130
+
131
+ save_dict: dict[str, Any] = {
132
+ "sample_rate": self.metadata.sample_rate,
133
+ "duration": self.metadata.duration,
134
+ "channel_names": np.array(self.metadata.channel_names),
135
+ "description": self.metadata.description,
136
+ "generator": self.metadata.generator,
137
+ }
138
+
139
+ # Add channel data
140
+ for name, data in self.data.items():
141
+ save_dict[name] = data
142
+
143
+ # Add parameters as JSON-serializable
144
+ for key, value in self.metadata.parameters.items():
145
+ if isinstance(value, (int, float, str, bool)):
146
+ save_dict[f"param_{key}"] = value
147
+
148
+ np.savez_compressed(path, **save_dict)
149
+
150
+ @classmethod
151
+ def load_npz(cls, path: Path | str) -> GeneratedSignal:
152
+ """Load signal from NPZ format.
153
+
154
+ Args:
155
+ path: Input file path.
156
+
157
+ Returns:
158
+ GeneratedSignal instance.
159
+ """
160
+ path = Path(path)
161
+ loaded = np.load(path, allow_pickle=True)
162
+
163
+ sample_rate = float(loaded["sample_rate"])
164
+ duration = float(loaded["duration"])
165
+ channel_names = list(loaded.get("channel_names", ["ch1"]))
166
+ description = str(loaded.get("description", ""))
167
+ generator = str(loaded.get("generator", "unknown"))
168
+
169
+ # Extract channel data
170
+ data = {}
171
+ for name in channel_names:
172
+ if name in loaded:
173
+ data[name] = loaded[name]
174
+
175
+ # Extract parameters
176
+ parameters: dict[str, Any] = {}
177
+ for key in loaded.files:
178
+ if key.startswith("param_"):
179
+ param_name = key[6:] # Remove "param_" prefix
180
+ value = loaded[key]
181
+ parameters[param_name] = value.item() if value.ndim == 0 else value
182
+
183
+ metadata = SignalMetadata(
184
+ sample_rate=sample_rate,
185
+ duration=duration,
186
+ channel_names=channel_names,
187
+ description=description,
188
+ generator=generator,
189
+ parameters=parameters,
190
+ )
191
+
192
+ return cls(data=data, metadata=metadata)
193
+
194
+
195
+ class SignalBuilder:
196
+ """Fluent builder for composable signal generation.
197
+
198
+ This class provides a chainable API for building complex test signals
199
+ by combining basic waveforms, protocol signals, noise, and impairments.
200
+
201
+ Example:
202
+ >>> # Simple sine wave with noise
203
+ >>> signal = (SignalBuilder()
204
+ ... .sample_rate(1e6)
205
+ ... .duration(0.01)
206
+ ... .add_sine(frequency=1000, amplitude=1.0)
207
+ ... .add_noise(snr_db=40)
208
+ ... .build())
209
+ >>>
210
+ >>> # UART signal with realistic characteristics
211
+ >>> uart_signal = (SignalBuilder()
212
+ ... .sample_rate(10e6)
213
+ ... .add_uart(baud_rate=115200, data=b"Hello TraceKit!", config="8N1")
214
+ ... .add_noise(snr_db=30)
215
+ ... .build())
216
+ """
217
+
218
+ def __init__(self, sample_rate: float = 1e6, duration: float = 0.01):
219
+ """Initialize builder with default parameters.
220
+
221
+ Args:
222
+ sample_rate: Sample rate in Hz (default 1 MHz).
223
+ duration: Signal duration in seconds (default 10 ms).
224
+ """
225
+ self._sample_rate = sample_rate
226
+ self._duration = duration
227
+ self._channels: dict[str, np.ndarray[Any, np.dtype[np.float64]]] = {}
228
+ self._description = ""
229
+ self._parameters: dict[str, Any] = {}
230
+
231
+ # ========== Configuration Methods ==========
232
+
233
+ def sample_rate(self, rate: float) -> SignalBuilder:
234
+ """Set sample rate in Hz.
235
+
236
+ Args:
237
+ rate: Sample rate in Hz.
238
+
239
+ Returns:
240
+ Self for chaining.
241
+ """
242
+ self._sample_rate = rate
243
+ return self
244
+
245
+ def duration(self, seconds: float) -> SignalBuilder:
246
+ """Set signal duration.
247
+
248
+ Args:
249
+ seconds: Duration in seconds.
250
+
251
+ Returns:
252
+ Self for chaining.
253
+ """
254
+ self._duration = seconds
255
+ return self
256
+
257
+ def description(self, desc: str) -> SignalBuilder:
258
+ """Set signal description.
259
+
260
+ Args:
261
+ desc: Human-readable description.
262
+
263
+ Returns:
264
+ Self for chaining.
265
+ """
266
+ self._description = desc
267
+ return self
268
+
269
+ # ========== Analog Signal Methods ==========
270
+
271
+ def add_sine(
272
+ self,
273
+ frequency: float = 1e3,
274
+ amplitude: float = 1.0,
275
+ phase: float = 0.0,
276
+ dc_offset: float = 0.0,
277
+ channel: str = "ch1",
278
+ ) -> SignalBuilder:
279
+ """Add sinusoidal component.
280
+
281
+ Args:
282
+ frequency: Signal frequency in Hz.
283
+ amplitude: Signal amplitude.
284
+ phase: Phase offset in radians.
285
+ dc_offset: DC offset.
286
+ channel: Channel name.
287
+
288
+ Returns:
289
+ Self for chaining.
290
+ """
291
+ t = self._get_time()
292
+ signal = amplitude * np.sin(2 * np.pi * frequency * t + phase) + dc_offset
293
+
294
+ self._add_to_channel(channel, signal)
295
+ self._parameters[f"{channel}_sine_freq"] = frequency
296
+ return self
297
+
298
+ def add_harmonics(
299
+ self,
300
+ fundamental: float = 1e3,
301
+ thd_percent: float = 1.0,
302
+ harmonics: list[tuple[int, float]] | None = None,
303
+ channel: str = "ch1",
304
+ ) -> SignalBuilder:
305
+ """Add harmonic distortion.
306
+
307
+ Args:
308
+ fundamental: Fundamental frequency in Hz.
309
+ thd_percent: Total harmonic distortion percentage (if harmonics not specified).
310
+ harmonics: List of (harmonic_number, relative_amplitude) tuples.
311
+ channel: Channel name.
312
+
313
+ Returns:
314
+ Self for chaining.
315
+ """
316
+ t = self._get_time()
317
+
318
+ if harmonics is None:
319
+ # Generate typical harmonic profile
320
+ thd_linear = thd_percent / 100
321
+ harmonics = [
322
+ (2, thd_linear * 0.7), # 2nd harmonic
323
+ (3, thd_linear * 0.5), # 3rd harmonic
324
+ (4, thd_linear * 0.3), # 4th harmonic
325
+ (5, thd_linear * 0.2), # 5th harmonic
326
+ ]
327
+
328
+ signal = np.zeros_like(t)
329
+ for harm_num, rel_amp in harmonics:
330
+ signal += rel_amp * np.sin(2 * np.pi * harm_num * fundamental * t)
331
+
332
+ self._add_to_channel(channel, signal)
333
+ return self
334
+
335
+ def add_square(
336
+ self,
337
+ frequency: float = 1e3,
338
+ amplitude: float = 1.0,
339
+ duty_cycle: float = 0.5,
340
+ rise_time: float | None = None,
341
+ channel: str = "ch1",
342
+ ) -> SignalBuilder:
343
+ """Add square wave with optional edge rate.
344
+
345
+ Args:
346
+ frequency: Signal frequency in Hz.
347
+ amplitude: Signal amplitude.
348
+ duty_cycle: Duty cycle 0-1 (default 0.5 = 50%).
349
+ rise_time: Rise time in seconds (None for ideal edges).
350
+ channel: Channel name.
351
+
352
+ Returns:
353
+ Self for chaining.
354
+ """
355
+ t = self._get_time()
356
+ signal = amplitude * scipy_signal.square(2 * np.pi * frequency * t, duty=duty_cycle)
357
+
358
+ # Apply finite rise time if specified
359
+ if rise_time is not None and rise_time > 0:
360
+ tau = rise_time / 2.2 # 10-90% rise time
361
+ alpha = 1 / (tau * self._sample_rate + 1)
362
+ filtered = np.zeros_like(signal)
363
+ filtered[0] = signal[0]
364
+ for i in range(1, len(signal)):
365
+ filtered[i] = alpha * signal[i] + (1 - alpha) * filtered[i - 1]
366
+ signal = filtered
367
+
368
+ self._add_to_channel(channel, signal)
369
+ self._parameters[f"{channel}_square_freq"] = frequency
370
+ return self
371
+
372
+ def add_triangle(
373
+ self,
374
+ frequency: float = 1e3,
375
+ amplitude: float = 1.0,
376
+ channel: str = "ch1",
377
+ ) -> SignalBuilder:
378
+ """Add triangle wave.
379
+
380
+ Args:
381
+ frequency: Signal frequency in Hz.
382
+ amplitude: Signal amplitude.
383
+ channel: Channel name.
384
+
385
+ Returns:
386
+ Self for chaining.
387
+ """
388
+ t = self._get_time()
389
+ signal = amplitude * scipy_signal.sawtooth(2 * np.pi * frequency * t, width=0.5)
390
+ self._add_to_channel(channel, signal)
391
+ return self
392
+
393
+ def add_sawtooth(
394
+ self,
395
+ frequency: float = 1e3,
396
+ amplitude: float = 1.0,
397
+ channel: str = "ch1",
398
+ ) -> SignalBuilder:
399
+ """Add sawtooth wave.
400
+
401
+ Args:
402
+ frequency: Signal frequency in Hz.
403
+ amplitude: Signal amplitude.
404
+ channel: Channel name.
405
+
406
+ Returns:
407
+ Self for chaining.
408
+ """
409
+ t = self._get_time()
410
+ signal = amplitude * scipy_signal.sawtooth(2 * np.pi * frequency * t)
411
+ self._add_to_channel(channel, signal)
412
+ return self
413
+
414
+ def add_chirp(
415
+ self,
416
+ f0: float = 1e3,
417
+ f1: float = 10e3,
418
+ method: Literal["linear", "quadratic", "logarithmic"] = "linear",
419
+ amplitude: float = 1.0,
420
+ channel: str = "ch1",
421
+ ) -> SignalBuilder:
422
+ """Add chirp (frequency sweep) signal.
423
+
424
+ Args:
425
+ f0: Starting frequency in Hz.
426
+ f1: Ending frequency in Hz.
427
+ method: Sweep type.
428
+ amplitude: Signal amplitude.
429
+ channel: Channel name.
430
+
431
+ Returns:
432
+ Self for chaining.
433
+ """
434
+ t = self._get_time()
435
+ signal = amplitude * scipy_signal.chirp(t, f0, self._duration, f1, method=method)
436
+ self._add_to_channel(channel, signal)
437
+ return self
438
+
439
+ def add_multitone(
440
+ self,
441
+ frequencies: list[float],
442
+ amplitudes: list[float] | None = None,
443
+ channel: str = "ch1",
444
+ ) -> SignalBuilder:
445
+ """Add multi-tone signal.
446
+
447
+ Args:
448
+ frequencies: List of frequencies in Hz.
449
+ amplitudes: List of amplitudes (default: all 1.0).
450
+ channel: Channel name.
451
+
452
+ Returns:
453
+ Self for chaining.
454
+ """
455
+ t = self._get_time()
456
+ if amplitudes is None:
457
+ amplitudes = [1.0] * len(frequencies)
458
+
459
+ signal = np.zeros_like(t)
460
+ for freq, amp in zip(frequencies, amplitudes, strict=False):
461
+ signal += amp * np.sin(2 * np.pi * freq * t)
462
+
463
+ self._add_to_channel(channel, signal)
464
+ return self
465
+
466
+ def add_dc(
467
+ self,
468
+ level: float = 1.0,
469
+ channel: str = "ch1",
470
+ ) -> SignalBuilder:
471
+ """Add DC level.
472
+
473
+ Args:
474
+ level: DC voltage level.
475
+ channel: Channel name.
476
+
477
+ Returns:
478
+ Self for chaining.
479
+ """
480
+ n_samples = self._get_num_samples()
481
+ signal = np.full(n_samples, level)
482
+ self._add_to_channel(channel, signal)
483
+ return self
484
+
485
+ def add_pulse(
486
+ self,
487
+ width: float = 1e-6,
488
+ amplitude: float = 1.0,
489
+ delay: float = 0.0,
490
+ rise_time: float = 0.0,
491
+ fall_time: float = 0.0,
492
+ channel: str = "ch1",
493
+ ) -> SignalBuilder:
494
+ """Add single pulse.
495
+
496
+ Args:
497
+ width: Pulse width in seconds.
498
+ amplitude: Pulse amplitude.
499
+ delay: Delay before pulse in seconds.
500
+ rise_time: Rise time in seconds.
501
+ fall_time: Fall time in seconds.
502
+ channel: Channel name.
503
+
504
+ Returns:
505
+ Self for chaining.
506
+ """
507
+ t = self._get_time()
508
+ signal = np.zeros_like(t)
509
+
510
+ start_idx = int(delay * self._sample_rate)
511
+ width_samples = int(width * self._sample_rate)
512
+ end_idx = min(start_idx + width_samples, len(signal))
513
+
514
+ if start_idx < len(signal):
515
+ signal[start_idx:end_idx] = amplitude
516
+
517
+ # Apply rise/fall times with simple filtering
518
+ if rise_time > 0 or fall_time > 0:
519
+ tau = max(rise_time, fall_time) / 2.2
520
+ if tau > 0:
521
+ alpha = 1 / (tau * self._sample_rate + 1)
522
+ filtered = np.zeros_like(signal)
523
+ filtered[0] = signal[0]
524
+ for i in range(1, len(signal)):
525
+ filtered[i] = alpha * signal[i] + (1 - alpha) * filtered[i - 1]
526
+ signal = filtered
527
+
528
+ self._add_to_channel(channel, signal)
529
+ return self
530
+
531
+ # ========== Noise Methods ==========
532
+
533
+ def add_noise(
534
+ self,
535
+ snr_db: float = 40.0,
536
+ noise_type: Literal["gaussian", "white", "pink"] = "gaussian",
537
+ channel: str = "ch1",
538
+ ) -> SignalBuilder:
539
+ """Add noise at specified SNR.
540
+
541
+ Args:
542
+ snr_db: Target signal-to-noise ratio in dB.
543
+ noise_type: Type of noise.
544
+ channel: Channel name.
545
+
546
+ Returns:
547
+ Self for chaining.
548
+ """
549
+ if channel not in self._channels:
550
+ raise ValueError(f"Channel '{channel}' does not exist. Add a signal first.")
551
+
552
+ signal = self._channels[channel]
553
+ signal_power = np.mean(signal**2)
554
+
555
+ if signal_power == 0:
556
+ signal_power = 1.0
557
+
558
+ noise_power = signal_power / (10 ** (snr_db / 10))
559
+ n_samples = len(signal)
560
+
561
+ if noise_type in ["gaussian", "white"]:
562
+ noise = np.sqrt(noise_power) * np.random.randn(n_samples)
563
+ elif noise_type == "pink":
564
+ white = np.random.randn(n_samples)
565
+ fft_white = np.fft.rfft(white)
566
+ freqs = np.fft.rfftfreq(n_samples)
567
+ freqs[0] = 1 # Avoid division by zero
568
+ pink_filter = 1 / np.sqrt(freqs)
569
+ fft_pink = fft_white * pink_filter
570
+ noise = np.fft.irfft(fft_pink, n=n_samples)
571
+ noise = noise * np.sqrt(noise_power / np.mean(noise**2))
572
+ else:
573
+ raise ValueError(f"Unknown noise type: {noise_type}")
574
+
575
+ self._channels[channel] = signal + noise
576
+ self._parameters[f"{channel}_snr_db"] = snr_db
577
+ return self
578
+
579
+ def add_white_noise(
580
+ self,
581
+ amplitude: float = 1.0,
582
+ channel: str = "ch1",
583
+ ) -> SignalBuilder:
584
+ """Add white noise signal.
585
+
586
+ Args:
587
+ amplitude: Noise amplitude (standard deviation).
588
+ channel: Channel name.
589
+
590
+ Returns:
591
+ Self for chaining.
592
+ """
593
+ n_samples = self._get_num_samples()
594
+ noise = amplitude * np.random.randn(n_samples)
595
+ self._add_to_channel(channel, noise)
596
+ return self
597
+
598
+ # ========== Protocol Signal Methods ==========
599
+
600
+ def add_uart(
601
+ self,
602
+ baud_rate: int = 115200,
603
+ data: bytes = b"Hello",
604
+ config: str = "8N1",
605
+ amplitude: float = 3.3,
606
+ idle_high: bool = True,
607
+ channel: str = "uart",
608
+ ) -> SignalBuilder:
609
+ """Add UART transmission signal.
610
+
611
+ Args:
612
+ baud_rate: UART baud rate.
613
+ data: Data bytes to transmit.
614
+ config: Configuration string "XYZ" where X=data bits, Y=parity, Z=stop bits.
615
+ amplitude: Logic high voltage level.
616
+ idle_high: If True, idle state is high (standard UART).
617
+ channel: Channel name.
618
+
619
+ Returns:
620
+ Self for chaining.
621
+ """
622
+ # Parse config
623
+ data_bits = int(config[0])
624
+ parity = config[1].upper() # N, O, E
625
+ stop_bits = int(config[2])
626
+
627
+ samples_per_bit = int(self._sample_rate / baud_rate)
628
+ bits: list[int] = []
629
+
630
+ # Add initial idle
631
+ idle_level = 1 if idle_high else 0
632
+ bits.extend([idle_level] * (samples_per_bit * 2))
633
+
634
+ for byte_val in data:
635
+ # Start bit (opposite of idle)
636
+ bits.extend([1 - idle_level] * samples_per_bit)
637
+
638
+ # Data bits (LSB first)
639
+ ones_count = 0
640
+ for i in range(data_bits):
641
+ bit = (byte_val >> i) & 1
642
+ ones_count += bit
643
+ bits.extend([bit] * samples_per_bit)
644
+
645
+ # Parity bit
646
+ if parity == "O": # Odd parity
647
+ parity_bit = (ones_count + 1) % 2
648
+ bits.extend([parity_bit] * samples_per_bit)
649
+ elif parity == "E": # Even parity
650
+ parity_bit = ones_count % 2
651
+ bits.extend([parity_bit] * samples_per_bit)
652
+ # N = no parity
653
+
654
+ # Stop bits
655
+ bits.extend([idle_level] * (samples_per_bit * stop_bits))
656
+
657
+ # Inter-frame gap
658
+ bits.extend([idle_level] * samples_per_bit)
659
+
660
+ # Final idle
661
+ bits.extend([idle_level] * (samples_per_bit * 2))
662
+
663
+ signal = np.array(bits, dtype=np.float64) * amplitude
664
+ self._add_to_channel(channel, signal)
665
+ self._parameters["uart_baud_rate"] = baud_rate
666
+ self._parameters["uart_data"] = data.hex()
667
+ return self
668
+
669
+ def add_spi(
670
+ self,
671
+ clock_freq: float = 1e6,
672
+ mode: int = 0,
673
+ data_mosi: bytes = b"\x00",
674
+ data_miso: bytes | None = None,
675
+ amplitude: float = 3.3,
676
+ channels: tuple[str, str, str, str] = ("sck", "mosi", "miso", "cs"),
677
+ ) -> SignalBuilder:
678
+ """Add SPI transaction signals.
679
+
680
+ Args:
681
+ clock_freq: SPI clock frequency in Hz.
682
+ mode: SPI mode (0-3, combines CPOL and CPHA).
683
+ data_mosi: MOSI data bytes.
684
+ data_miso: MISO data bytes (default: same as MOSI).
685
+ amplitude: Logic high voltage level.
686
+ channels: Tuple of channel names (SCK, MOSI, MISO, CS).
687
+
688
+ Returns:
689
+ Self for chaining.
690
+ """
691
+ if data_miso is None:
692
+ data_miso = data_mosi
693
+
694
+ cpol = (mode >> 1) & 1
695
+ cpha = mode & 1
696
+
697
+ samples_per_half_clock = int(self._sample_rate / clock_freq / 2)
698
+ total_bits = len(data_mosi) * 8
699
+ total_samples = samples_per_half_clock * 2 * total_bits + samples_per_half_clock * 4
700
+
701
+ # Initialize signals
702
+ sck = np.full(total_samples, amplitude if cpol else 0.0)
703
+ mosi = np.zeros(total_samples)
704
+ miso = np.zeros(total_samples)
705
+ cs = np.full(total_samples, amplitude) # Active low
706
+
707
+ idx = samples_per_half_clock # Start after idle
708
+ cs[idx:] = 0.0 # Activate CS
709
+
710
+ for byte_idx in range(len(data_mosi)):
711
+ mosi_byte = data_mosi[byte_idx]
712
+ miso_byte = data_miso[byte_idx] if byte_idx < len(data_miso) else 0
713
+
714
+ for bit_idx in range(8):
715
+ mosi_bit = (mosi_byte >> (7 - bit_idx)) & 1
716
+ miso_bit = (miso_byte >> (7 - bit_idx)) & 1
717
+
718
+ if cpha == 0:
719
+ mosi[idx : idx + samples_per_half_clock * 2] = amplitude if mosi_bit else 0.0
720
+ miso[idx : idx + samples_per_half_clock * 2] = amplitude if miso_bit else 0.0
721
+
722
+ # Clock edge
723
+ if cpol == 0:
724
+ sck[idx + samples_per_half_clock : idx + samples_per_half_clock * 2] = amplitude
725
+ else:
726
+ sck[idx + samples_per_half_clock : idx + samples_per_half_clock * 2] = 0.0
727
+
728
+ if cpha == 1:
729
+ mosi[idx + samples_per_half_clock : idx + samples_per_half_clock * 2] = (
730
+ amplitude if mosi_bit else 0.0
731
+ )
732
+ miso[idx + samples_per_half_clock : idx + samples_per_half_clock * 2] = (
733
+ amplitude if miso_bit else 0.0
734
+ )
735
+
736
+ idx += samples_per_half_clock * 2
737
+
738
+ cs[idx:] = amplitude # Deactivate CS
739
+
740
+ self._channels[channels[0]] = sck
741
+ self._channels[channels[1]] = mosi
742
+ self._channels[channels[2]] = miso
743
+ self._channels[channels[3]] = cs
744
+
745
+ self._parameters["spi_clock_freq"] = clock_freq
746
+ self._parameters["spi_mode"] = mode
747
+ return self
748
+
749
+ def add_i2c(
750
+ self,
751
+ clock_freq: float = 100e3,
752
+ address: int = 0x50,
753
+ data: bytes = b"\x00",
754
+ read: bool = False,
755
+ amplitude: float = 3.3,
756
+ channels: tuple[str, str] = ("scl", "sda"),
757
+ ) -> SignalBuilder:
758
+ """Add I2C transaction signals.
759
+
760
+ Args:
761
+ clock_freq: I2C clock frequency in Hz.
762
+ address: 7-bit I2C address.
763
+ data: Data bytes to transmit.
764
+ read: True for read, False for write.
765
+ amplitude: Logic high voltage level.
766
+ channels: Tuple of channel names (SCL, SDA).
767
+
768
+ Returns:
769
+ Self for chaining.
770
+ """
771
+ samples_per_bit = int(self._sample_rate / clock_freq)
772
+ half_bit = samples_per_bit // 2
773
+
774
+ # Calculate total samples
775
+ total_bits = 1 + 8 + 1 + len(data) * 9 + 1
776
+ total_samples = samples_per_bit * total_bits + samples_per_bit * 2
777
+
778
+ scl = np.full(total_samples, amplitude)
779
+ sda = np.full(total_samples, amplitude)
780
+
781
+ idx = samples_per_bit # Start after idle
782
+
783
+ # START: SDA falls while SCL high
784
+ sda[idx : idx + half_bit] = 0.0
785
+ idx += half_bit
786
+
787
+ # Address + R/W bit
788
+ addr_byte = (address << 1) | (1 if read else 0)
789
+
790
+ for bit_idx in range(8):
791
+ bit = (addr_byte >> (7 - bit_idx)) & 1
792
+ scl[idx : idx + half_bit] = 0.0
793
+ sda[idx : idx + samples_per_bit] = amplitude if bit else 0.0
794
+ idx += half_bit
795
+ scl[idx : idx + half_bit] = amplitude
796
+ idx += half_bit
797
+
798
+ # ACK bit
799
+ scl[idx : idx + half_bit] = 0.0
800
+ sda[idx : idx + samples_per_bit] = 0.0 # ACK (low)
801
+ idx += half_bit
802
+ scl[idx : idx + half_bit] = amplitude
803
+ idx += half_bit
804
+
805
+ # Data bytes
806
+ for byte_val in data:
807
+ for bit_idx in range(8):
808
+ bit = (byte_val >> (7 - bit_idx)) & 1
809
+ scl[idx : idx + half_bit] = 0.0
810
+ sda[idx : idx + samples_per_bit] = amplitude if bit else 0.0
811
+ idx += half_bit
812
+ scl[idx : idx + half_bit] = amplitude
813
+ idx += half_bit
814
+
815
+ # ACK
816
+ scl[idx : idx + half_bit] = 0.0
817
+ sda[idx : idx + samples_per_bit] = 0.0
818
+ idx += half_bit
819
+ scl[idx : idx + half_bit] = amplitude
820
+ idx += half_bit
821
+
822
+ # STOP: SDA rises while SCL high
823
+ scl[idx : idx + half_bit] = 0.0
824
+ sda[idx : idx + half_bit] = 0.0
825
+ idx += half_bit
826
+ scl[idx:] = amplitude
827
+ sda[idx:] = amplitude
828
+
829
+ self._channels[channels[0]] = scl[:idx]
830
+ self._channels[channels[1]] = sda[:idx]
831
+
832
+ self._parameters["i2c_clock_freq"] = clock_freq
833
+ self._parameters["i2c_address"] = address
834
+ return self
835
+
836
+ def add_can(
837
+ self,
838
+ bitrate: int = 500000,
839
+ arbitration_id: int = 0x100,
840
+ data: bytes = b"\x00",
841
+ extended: bool = False,
842
+ amplitude: float = 2.5,
843
+ channel: str = "can",
844
+ ) -> SignalBuilder:
845
+ """Add CAN message signal.
846
+
847
+ Args:
848
+ bitrate: CAN bit rate.
849
+ arbitration_id: Message arbitration ID.
850
+ data: Data bytes (max 8).
851
+ extended: True for extended (29-bit) ID.
852
+ amplitude: Logic high voltage level.
853
+ channel: Channel name.
854
+
855
+ Returns:
856
+ Self for chaining.
857
+ """
858
+ samples_per_bit = int(self._sample_rate / bitrate)
859
+ bits: list[int] = []
860
+
861
+ # Start of frame (dominant = 0)
862
+ bits.append(0)
863
+
864
+ # Arbitration ID
865
+ id_bits = 29 if extended else 11
866
+ for i in range(id_bits - 1, -1, -1):
867
+ bits.append((arbitration_id >> i) & 1)
868
+
869
+ # RTR (0 for data frame)
870
+ bits.append(0)
871
+
872
+ # IDE (0 for standard, 1 for extended)
873
+ if not extended:
874
+ bits.append(0)
875
+
876
+ # Reserved bit
877
+ bits.append(0)
878
+
879
+ # DLC (data length code)
880
+ dlc = min(len(data), 8)
881
+ for i in range(3, -1, -1):
882
+ bits.append((dlc >> i) & 1)
883
+
884
+ # Data bytes
885
+ for byte_val in data[:8]:
886
+ for i in range(7, -1, -1):
887
+ bits.append((byte_val >> i) & 1)
888
+
889
+ # CRC (simplified placeholder)
890
+ for _ in range(15):
891
+ bits.append(0)
892
+
893
+ # CRC delimiter
894
+ bits.append(1)
895
+
896
+ # ACK slot and delimiter
897
+ bits.append(0) # ACK
898
+ bits.append(1) # ACK delimiter
899
+
900
+ # End of frame (7 recessive bits)
901
+ bits.extend([1] * 7)
902
+
903
+ # Inter-frame space
904
+ bits.extend([1] * 3)
905
+
906
+ # Convert to signal
907
+ signal_bits: list[float] = []
908
+ for bit in bits:
909
+ level = amplitude if bit else 0.0
910
+ signal_bits.extend([level] * samples_per_bit)
911
+
912
+ signal = np.array(signal_bits, dtype=np.float64)
913
+ self._add_to_channel(channel, signal)
914
+ self._parameters["can_bitrate"] = bitrate
915
+ self._parameters["can_id"] = arbitration_id
916
+ return self
917
+
918
+ def add_digital_pattern(
919
+ self,
920
+ pattern: str = "01010101",
921
+ bit_rate: float = 1e6,
922
+ amplitude: float = 3.3,
923
+ channel: str = "digital",
924
+ ) -> SignalBuilder:
925
+ """Add digital bit pattern.
926
+
927
+ Args:
928
+ pattern: Binary pattern string (e.g., "01010101").
929
+ bit_rate: Bit rate in bps.
930
+ amplitude: Logic high voltage level.
931
+ channel: Channel name.
932
+
933
+ Returns:
934
+ Self for chaining.
935
+ """
936
+ samples_per_bit = int(self._sample_rate / bit_rate)
937
+ bits: list[float] = []
938
+
939
+ for bit_char in pattern:
940
+ level = amplitude if bit_char == "1" else 0.0
941
+ bits.extend([level] * samples_per_bit)
942
+
943
+ signal = np.array(bits, dtype=np.float64)
944
+ self._add_to_channel(channel, signal)
945
+ return self
946
+
947
+ def add_clock(
948
+ self,
949
+ frequency: float = 1e6,
950
+ duty_cycle: float = 0.5,
951
+ amplitude: float = 3.3,
952
+ channel: str = "clk",
953
+ ) -> SignalBuilder:
954
+ """Add clock signal.
955
+
956
+ Args:
957
+ frequency: Clock frequency in Hz.
958
+ duty_cycle: Duty cycle (0-1).
959
+ amplitude: Logic high voltage level.
960
+ channel: Channel name.
961
+
962
+ Returns:
963
+ Self for chaining.
964
+ """
965
+ return self.add_square(
966
+ frequency=frequency,
967
+ amplitude=amplitude,
968
+ duty_cycle=duty_cycle,
969
+ channel=channel,
970
+ )
971
+
972
+ # ========== Impairment Methods ==========
973
+
974
+ def add_jitter(
975
+ self,
976
+ rj_rms: float = 0.0,
977
+ dj_pp: float = 0.0,
978
+ channel: str = "ch1",
979
+ ) -> SignalBuilder:
980
+ """Add jitter to digital signal.
981
+
982
+ Args:
983
+ rj_rms: Random jitter RMS in seconds.
984
+ dj_pp: Deterministic jitter peak-to-peak in seconds.
985
+ channel: Channel name.
986
+
987
+ Returns:
988
+ Self for chaining.
989
+ """
990
+ if channel not in self._channels:
991
+ raise ValueError(f"Channel '{channel}' does not exist.")
992
+
993
+ if rj_rms == 0 and dj_pp == 0:
994
+ return self
995
+
996
+ signal = self._channels[channel]
997
+ threshold = (np.max(signal) + np.min(signal)) / 2
998
+ edges = np.where(np.diff((signal > threshold).astype(int)))[0]
999
+
1000
+ if len(edges) == 0:
1001
+ return self
1002
+
1003
+ t_original = np.arange(len(signal)) / self._sample_rate
1004
+ t_jittered = t_original.copy()
1005
+
1006
+ for edge_idx in edges:
1007
+ jitter = 0.0
1008
+ if rj_rms > 0:
1009
+ jitter += np.random.randn() * rj_rms
1010
+ if dj_pp > 0:
1011
+ jitter += (dj_pp / 2) * np.sin(2 * np.pi * edge_idx / max(len(edges), 1))
1012
+
1013
+ edge_region = slice(max(0, edge_idx - 5), min(len(signal), edge_idx + 6))
1014
+ t_jittered[edge_region] += jitter
1015
+
1016
+ self._channels[channel] = np.interp(t_original, t_jittered, signal)
1017
+ return self
1018
+
1019
+ def add_quantization(
1020
+ self,
1021
+ bits: int = 8,
1022
+ full_scale: float = 2.0,
1023
+ channel: str = "ch1",
1024
+ ) -> SignalBuilder:
1025
+ """Apply ADC quantization effects.
1026
+
1027
+ Args:
1028
+ bits: Number of ADC bits.
1029
+ full_scale: Full scale range.
1030
+ channel: Channel name.
1031
+
1032
+ Returns:
1033
+ Self for chaining.
1034
+ """
1035
+ if channel not in self._channels:
1036
+ raise ValueError(f"Channel '{channel}' does not exist.")
1037
+
1038
+ signal = self._channels[channel]
1039
+ levels = 2**bits
1040
+ lsb = full_scale / levels
1041
+
1042
+ # Quantize
1043
+ quantized = np.round(signal / lsb) * lsb
1044
+ # Clip to full scale
1045
+ quantized = np.clip(quantized, -full_scale / 2, full_scale / 2 - lsb)
1046
+
1047
+ self._channels[channel] = quantized
1048
+ self._parameters[f"{channel}_adc_bits"] = bits
1049
+ return self
1050
+
1051
+ # ========== Build Methods ==========
1052
+
1053
+ def build(self) -> GeneratedSignal:
1054
+ """Build and return signal.
1055
+
1056
+ Returns:
1057
+ GeneratedSignal containing all channels and metadata.
1058
+
1059
+ Raises:
1060
+ ValueError: If no signals have been added.
1061
+ """
1062
+ if not self._channels:
1063
+ raise ValueError("No signals added. Call add_* methods before build().")
1064
+
1065
+ # Ensure all channels have same length (pad if necessary)
1066
+ max_len = max(len(s) for s in self._channels.values())
1067
+ for name, signal in self._channels.items():
1068
+ if len(signal) < max_len:
1069
+ self._channels[name] = np.pad(signal, (0, max_len - len(signal)), mode="edge")
1070
+
1071
+ # Calculate actual duration from signal length
1072
+ actual_duration = max_len / self._sample_rate
1073
+
1074
+ metadata = SignalMetadata(
1075
+ sample_rate=self._sample_rate,
1076
+ duration=actual_duration,
1077
+ channel_names=list(self._channels.keys()),
1078
+ description=self._description,
1079
+ generator="SignalBuilder",
1080
+ parameters=self._parameters,
1081
+ )
1082
+
1083
+ return GeneratedSignal(data=self._channels.copy(), metadata=metadata)
1084
+
1085
+ def build_trace(self, channel: str | None = None) -> WaveformTrace:
1086
+ """Build and return as WaveformTrace for direct use with TraceKit.
1087
+
1088
+ Args:
1089
+ channel: Channel to return as trace. If None, uses first channel.
1090
+
1091
+ Returns:
1092
+ WaveformTrace ready for TraceKit analysis.
1093
+ """
1094
+ signal = self.build()
1095
+ return signal.to_trace(channel)
1096
+
1097
+ def save_npz(self, path: Path | str) -> GeneratedSignal:
1098
+ """Build and save signal to NPZ format.
1099
+
1100
+ Args:
1101
+ path: Output file path.
1102
+
1103
+ Returns:
1104
+ GeneratedSignal that was saved.
1105
+ """
1106
+ signal = self.build()
1107
+ signal.save_npz(path)
1108
+ return signal
1109
+
1110
+ # ========== Internal Methods ==========
1111
+
1112
+ def _get_time(self) -> np.ndarray[Any, np.dtype[np.float64]]:
1113
+ """Get time array based on current settings."""
1114
+ n_samples = self._get_num_samples()
1115
+ return np.arange(n_samples) / self._sample_rate
1116
+
1117
+ def _get_num_samples(self) -> int:
1118
+ """Get number of samples based on current settings."""
1119
+ return int(self._sample_rate * self._duration)
1120
+
1121
+ def _add_to_channel(self, channel: str, signal: np.ndarray[Any, np.dtype[np.float64]]) -> None:
1122
+ """Add signal to channel, summing if channel already exists."""
1123
+ if channel in self._channels:
1124
+ current = self._channels[channel]
1125
+ if len(signal) > len(current):
1126
+ current = np.pad(current, (0, len(signal) - len(current)))
1127
+ elif len(signal) < len(current):
1128
+ signal = np.pad(signal, (0, len(current) - len(signal)))
1129
+ self._channels[channel] = current + signal
1130
+ else:
1131
+ self._channels[channel] = signal