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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.1.dist-info/METADATA +300 -0
  460. oscura-0.1.1.dist-info/RECORD +463 -0
  461. oscura-0.1.1.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.1.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.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,381 @@
1
+ """Plot style presets for different output contexts.
2
+
3
+ This module provides comprehensive style presets for publication-quality,
4
+ presentation, screen viewing, and print output.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.styles import apply_style_preset
9
+ >>> with apply_style_preset("publication"):
10
+ ... plot_waveform(signal)
11
+
12
+ References:
13
+ matplotlib rcParams customization
14
+ Publication and presentation best practices
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from contextlib import contextmanager
20
+ from dataclasses import dataclass, field
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Iterator
25
+
26
+ try:
27
+ import matplotlib.pyplot as plt
28
+
29
+ HAS_MATPLOTLIB = True
30
+ except ImportError:
31
+ HAS_MATPLOTLIB = False
32
+
33
+
34
+ @dataclass
35
+ class StylePreset:
36
+ """Style preset configuration for plots.
37
+
38
+ Attributes:
39
+ name: Preset name
40
+ dpi: Target DPI (dots per inch)
41
+ font_family: Font family (serif, sans-serif, monospace)
42
+ font_size: Base font size in points
43
+ line_width: Default line width in points
44
+ marker_size: Default marker size
45
+ figure_facecolor: Figure background color
46
+ axes_facecolor: Axes background color
47
+ axes_edgecolor: Axes edge color
48
+ grid_color: Grid line color
49
+ grid_alpha: Grid line transparency
50
+ grid_linestyle: Grid line style
51
+ use_latex: Use LaTeX for text rendering
52
+ tight_layout: Use tight layout
53
+ rcparams: Additional matplotlib rcParams
54
+ """
55
+
56
+ name: str
57
+ dpi: int = 96
58
+ font_family: str = "sans-serif"
59
+ font_size: int = 10
60
+ line_width: float = 1.0
61
+ marker_size: float = 6.0
62
+ figure_facecolor: str = "white"
63
+ axes_facecolor: str = "white"
64
+ axes_edgecolor: str = "black"
65
+ grid_color: str = "#B0B0B0"
66
+ grid_alpha: float = 0.3
67
+ grid_linestyle: str = "-"
68
+ use_latex: bool = False
69
+ tight_layout: bool = True
70
+ rcparams: dict[str, Any] = field(default_factory=dict)
71
+
72
+
73
+ # Predefined style presets
74
+
75
+ PUBLICATION_PRESET = StylePreset(
76
+ name="publication",
77
+ dpi=600,
78
+ font_family="serif",
79
+ font_size=10,
80
+ line_width=0.8,
81
+ marker_size=4.0,
82
+ figure_facecolor="white",
83
+ axes_facecolor="white",
84
+ axes_edgecolor="black",
85
+ grid_color="#808080",
86
+ grid_alpha=0.3,
87
+ grid_linestyle=":",
88
+ use_latex=False, # LaTeX optional - requires system install
89
+ tight_layout=True,
90
+ rcparams={
91
+ "axes.linewidth": 0.8,
92
+ "xtick.major.width": 0.8,
93
+ "ytick.major.width": 0.8,
94
+ "xtick.minor.width": 0.6,
95
+ "ytick.minor.width": 0.6,
96
+ "lines.antialiased": True,
97
+ "patch.antialiased": True,
98
+ "savefig.dpi": 600,
99
+ "savefig.format": "pdf",
100
+ "savefig.bbox": "tight",
101
+ },
102
+ )
103
+
104
+ PRESENTATION_PRESET = StylePreset(
105
+ name="presentation",
106
+ dpi=96,
107
+ font_family="sans-serif",
108
+ font_size=18,
109
+ line_width=2.5,
110
+ marker_size=10.0,
111
+ figure_facecolor="white",
112
+ axes_facecolor="white",
113
+ axes_edgecolor="black",
114
+ grid_color="#CCCCCC",
115
+ grid_alpha=0.5,
116
+ grid_linestyle="-",
117
+ use_latex=False,
118
+ tight_layout=True,
119
+ rcparams={
120
+ "axes.linewidth": 2.0,
121
+ "xtick.major.width": 2.0,
122
+ "ytick.major.width": 2.0,
123
+ "xtick.major.size": 8,
124
+ "ytick.major.size": 8,
125
+ "lines.antialiased": True,
126
+ "savefig.dpi": 150,
127
+ },
128
+ )
129
+
130
+ SCREEN_PRESET = StylePreset(
131
+ name="screen",
132
+ dpi=96,
133
+ font_family="sans-serif",
134
+ font_size=10,
135
+ line_width=1.2,
136
+ marker_size=6.0,
137
+ figure_facecolor="white",
138
+ axes_facecolor="white",
139
+ axes_edgecolor="#333333",
140
+ grid_color="#B0B0B0",
141
+ grid_alpha=0.3,
142
+ grid_linestyle="-",
143
+ use_latex=False,
144
+ tight_layout=True,
145
+ rcparams={
146
+ "axes.linewidth": 1.0,
147
+ "lines.antialiased": True,
148
+ "patch.antialiased": True,
149
+ },
150
+ )
151
+
152
+ PRINT_PRESET = StylePreset(
153
+ name="print",
154
+ dpi=300,
155
+ font_family="serif",
156
+ font_size=11,
157
+ line_width=1.2,
158
+ marker_size=5.0,
159
+ figure_facecolor="white",
160
+ axes_facecolor="white",
161
+ axes_edgecolor="black",
162
+ grid_color="#707070",
163
+ grid_alpha=0.3,
164
+ grid_linestyle=":",
165
+ use_latex=False,
166
+ tight_layout=True,
167
+ rcparams={
168
+ "axes.linewidth": 1.0,
169
+ "xtick.major.width": 1.0,
170
+ "ytick.major.width": 1.0,
171
+ "lines.antialiased": False, # Sharper lines for print
172
+ "patch.antialiased": False,
173
+ "savefig.dpi": 300,
174
+ "savefig.format": "pdf",
175
+ },
176
+ )
177
+
178
+ # Registry of available presets
179
+ PRESETS: dict[str, StylePreset] = {
180
+ "publication": PUBLICATION_PRESET,
181
+ "presentation": PRESENTATION_PRESET,
182
+ "screen": SCREEN_PRESET,
183
+ "print": PRINT_PRESET,
184
+ }
185
+
186
+
187
+ @contextmanager
188
+ def apply_style_preset(
189
+ preset: str | StylePreset,
190
+ *,
191
+ overrides: dict[str, Any] | None = None,
192
+ ) -> Iterator[None]:
193
+ """Apply style preset as context manager.
194
+
195
+ : Provide comprehensive style presets for common use cases
196
+ with support for custom overrides.
197
+
198
+ Args:
199
+ preset: Preset name or StylePreset object
200
+ overrides: Dictionary of rcParams to override
201
+
202
+ Yields:
203
+ None (use as context manager)
204
+
205
+ Raises:
206
+ ValueError: If preset name is unknown
207
+ ImportError: If matplotlib is not available
208
+
209
+ Example:
210
+ >>> with apply_style_preset("publication"):
211
+ ... fig, ax = plt.subplots()
212
+ ... ax.plot(x, y)
213
+ ... plt.savefig("figure.pdf")
214
+
215
+ >>> # With overrides
216
+ >>> with apply_style_preset("screen", overrides={"font.size": 14}):
217
+ ... plot_waveform(signal)
218
+
219
+ References:
220
+ VIS-024: Plot Style Presets
221
+ matplotlib style sheets and rcParams
222
+ """
223
+ if not HAS_MATPLOTLIB:
224
+ raise ImportError("matplotlib is required for style presets")
225
+
226
+ # Get preset object
227
+ if isinstance(preset, str):
228
+ if preset not in PRESETS:
229
+ raise ValueError(f"Unknown preset: {preset}. Available: {list(PRESETS.keys())}")
230
+ preset_obj = PRESETS[preset]
231
+ else:
232
+ preset_obj = preset
233
+
234
+ # Build rcParams dictionary
235
+ rc_dict = _preset_to_rcparams(preset_obj)
236
+
237
+ # Apply overrides
238
+ if overrides:
239
+ rc_dict.update(overrides)
240
+
241
+ # Apply as context
242
+ with plt.rc_context(rc_dict):
243
+ yield
244
+
245
+
246
+ def _preset_to_rcparams(preset: StylePreset) -> dict[str, Any]:
247
+ """Convert StylePreset to matplotlib rcParams dictionary.
248
+
249
+ Args:
250
+ preset: StylePreset object
251
+
252
+ Returns:
253
+ Dictionary of rcParams
254
+ """
255
+ rc = {
256
+ "figure.dpi": preset.dpi,
257
+ "font.family": preset.font_family,
258
+ "font.size": preset.font_size,
259
+ "lines.linewidth": preset.line_width,
260
+ "lines.markersize": preset.marker_size,
261
+ "figure.facecolor": preset.figure_facecolor,
262
+ "axes.facecolor": preset.axes_facecolor,
263
+ "axes.edgecolor": preset.axes_edgecolor,
264
+ "grid.color": preset.grid_color,
265
+ "grid.alpha": preset.grid_alpha,
266
+ "grid.linestyle": preset.grid_linestyle,
267
+ "figure.autolayout": preset.tight_layout,
268
+ }
269
+
270
+ # LaTeX rendering
271
+ if preset.use_latex:
272
+ rc["text.usetex"] = True
273
+
274
+ # Merge with additional rcparams
275
+ rc.update(preset.rcparams)
276
+
277
+ return rc
278
+
279
+
280
+ def create_custom_preset(
281
+ name: str,
282
+ base_preset: str = "screen",
283
+ **kwargs: Any,
284
+ ) -> StylePreset:
285
+ """Create custom preset by inheriting from base preset.
286
+
287
+ : Support custom presets with inheritance and override.
288
+
289
+ Args:
290
+ name: Name for custom preset
291
+ base_preset: Base preset to inherit from
292
+ **kwargs: Attributes to override
293
+
294
+ Returns:
295
+ Custom StylePreset object
296
+
297
+ Raises:
298
+ ValueError: If base_preset is unknown
299
+
300
+ Example:
301
+ >>> custom = create_custom_preset(
302
+ ... "my_style",
303
+ ... base_preset="publication",
304
+ ... font_size=12,
305
+ ... line_width=1.5
306
+ ... )
307
+ >>> with apply_style_preset(custom):
308
+ ... plot_data()
309
+
310
+ References:
311
+ VIS-024: Plot Style Presets with inheritance
312
+ """
313
+ if base_preset not in PRESETS:
314
+ raise ValueError(f"Unknown base_preset: {base_preset}")
315
+
316
+ # Get base preset
317
+ base = PRESETS[base_preset]
318
+
319
+ # Create copy with overrides
320
+ preset_dict = {
321
+ "name": name,
322
+ "dpi": kwargs.get("dpi", base.dpi),
323
+ "font_family": kwargs.get("font_family", base.font_family),
324
+ "font_size": kwargs.get("font_size", base.font_size),
325
+ "line_width": kwargs.get("line_width", base.line_width),
326
+ "marker_size": kwargs.get("marker_size", base.marker_size),
327
+ "figure_facecolor": kwargs.get("figure_facecolor", base.figure_facecolor),
328
+ "axes_facecolor": kwargs.get("axes_facecolor", base.axes_facecolor),
329
+ "axes_edgecolor": kwargs.get("axes_edgecolor", base.axes_edgecolor),
330
+ "grid_color": kwargs.get("grid_color", base.grid_color),
331
+ "grid_alpha": kwargs.get("grid_alpha", base.grid_alpha),
332
+ "grid_linestyle": kwargs.get("grid_linestyle", base.grid_linestyle),
333
+ "use_latex": kwargs.get("use_latex", base.use_latex),
334
+ "tight_layout": kwargs.get("tight_layout", base.tight_layout),
335
+ "rcparams": kwargs.get("rcparams", base.rcparams.copy()),
336
+ }
337
+
338
+ return StylePreset(**preset_dict)
339
+
340
+
341
+ def register_preset(preset: StylePreset) -> None:
342
+ """Register custom preset in global registry.
343
+
344
+ Args:
345
+ preset: StylePreset to register
346
+
347
+ Example:
348
+ >>> custom = create_custom_preset("my_style", base_preset="publication")
349
+ >>> register_preset(custom)
350
+ >>> with apply_style_preset("my_style"):
351
+ ... plot_data()
352
+ """
353
+ PRESETS[preset.name] = preset
354
+
355
+
356
+ def list_presets() -> list[str]:
357
+ """Get list of available preset names.
358
+
359
+ Returns:
360
+ List of preset names
361
+
362
+ Example:
363
+ >>> presets = list_presets()
364
+ >>> print(presets)
365
+ ['publication', 'presentation', 'screen', 'print']
366
+ """
367
+ return list(PRESETS.keys())
368
+
369
+
370
+ __all__ = [
371
+ "PRESENTATION_PRESET",
372
+ "PRESETS",
373
+ "PRINT_PRESET",
374
+ "PUBLICATION_PRESET",
375
+ "SCREEN_PRESET",
376
+ "StylePreset",
377
+ "apply_style_preset",
378
+ "create_custom_preset",
379
+ "list_presets",
380
+ "register_preset",
381
+ ]
@@ -0,0 +1,311 @@
1
+ """Thumbnail rendering for fast signal previews.
2
+
3
+ This module provides fast preview rendering with reduced detail
4
+ for gallery and browser contexts.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.thumbnails import render_thumbnail
9
+ >>> fig = render_thumbnail(signal, sample_rate, size=(400, 300))
10
+
11
+ References:
12
+ Aggressive decimation for performance
13
+ Simplified rendering without expensive features
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING
19
+
20
+ import numpy as np
21
+
22
+ if TYPE_CHECKING:
23
+ from matplotlib.figure import Figure
24
+ from numpy.typing import NDArray
25
+
26
+ try:
27
+ import matplotlib # noqa: F401
28
+ import matplotlib.pyplot as plt
29
+
30
+ HAS_MATPLOTLIB = True
31
+ except ImportError:
32
+ HAS_MATPLOTLIB = False
33
+
34
+
35
+ def render_thumbnail(
36
+ signal: NDArray[np.float64],
37
+ sample_rate: float | None = None,
38
+ *,
39
+ size: tuple[int, int] = (400, 300),
40
+ width: int | None = None,
41
+ height: int | None = None,
42
+ max_samples: int = 1000,
43
+ time_unit: str = "auto",
44
+ title: str | None = None,
45
+ dpi: int = 72,
46
+ ) -> Figure:
47
+ """Render fast preview thumbnail of signal.
48
+
49
+ : Fast preview rendering mode with reduced detail,
50
+ simplified styles, and lower resolution for quick plot generation.
51
+
52
+ Target performance: <100ms for typical signals (goal: 50ms)
53
+
54
+ Args:
55
+ signal: Input signal array
56
+ sample_rate: Sample rate in Hz. If None, uses 1.0 (sample indices as x-axis).
57
+ size: Thumbnail size in pixels (width, height), default (400, 300)
58
+ width: Width in pixels (alternative to size). If specified, height defaults to 3/4 of width.
59
+ height: Height in pixels (alternative to size).
60
+ max_samples: Maximum samples to plot (default: 1000, aggressive decimation)
61
+ time_unit: Time unit for x-axis ("s", "ms", "us", "ns", "auto")
62
+ title: Optional title
63
+ dpi: DPI for rendering (default: 72)
64
+
65
+ Returns:
66
+ Matplotlib Figure object configured for fast rendering
67
+
68
+ Raises:
69
+ ValueError: If signal is empty or sample_rate is invalid
70
+ ImportError: If matplotlib is not available
71
+
72
+ Example:
73
+ >>> signal = np.sin(2*np.pi*1000*np.arange(0, 0.01, 1/1e6))
74
+ >>> fig = render_thumbnail(signal, 1e6, size=(400, 300))
75
+ >>> fig.savefig("preview.png")
76
+ >>> # Without sample rate
77
+ >>> fig = render_thumbnail(data, width=100, height=50)
78
+
79
+ References:
80
+ VIS-018: Thumbnail Mode
81
+ Fixed-count decimation for uniform sampling
82
+ """
83
+ if not HAS_MATPLOTLIB:
84
+ raise ImportError("matplotlib is required for visualization")
85
+
86
+ # Default sample rate if not provided
87
+ if sample_rate is None:
88
+ sample_rate = 1.0
89
+
90
+ if len(signal) == 0:
91
+ raise ValueError("Signal cannot be empty")
92
+ if sample_rate <= 0:
93
+ raise ValueError("Sample rate must be positive")
94
+ if max_samples < 10:
95
+ raise ValueError("max_samples must be >= 10")
96
+
97
+ # Handle width/height as alternative to size
98
+ if width is not None:
99
+ h = height if height is not None else int(width * 0.75)
100
+ size = (width, h)
101
+ elif height is not None:
102
+ size = (int(height * 4 / 3), height)
103
+
104
+ # Configure matplotlib for fast rendering (no anti-aliasing, etc.)
105
+ with plt.rc_context(
106
+ {
107
+ "path.simplify": True,
108
+ "path.simplify_threshold": 1.0,
109
+ "agg.path.chunksize": 1000,
110
+ "lines.antialiased": False,
111
+ "patch.antialiased": False,
112
+ "text.antialiased": False,
113
+ }
114
+ ):
115
+ # Calculate figure size in inches
116
+ width_inches = size[0] / dpi
117
+ height_inches = size[1] / dpi
118
+
119
+ # Create figure with no fancy features
120
+ fig, ax = plt.subplots(figsize=(width_inches, height_inches), dpi=dpi)
121
+
122
+ # Decimate signal to max_samples
123
+ decimated_signal = _decimate_uniform(signal, max_samples)
124
+
125
+ # Create time vector for decimated signal
126
+ total_time = len(signal) / sample_rate
127
+ time = np.linspace(0, total_time, len(decimated_signal))
128
+
129
+ # Auto-select time unit
130
+ if time_unit == "auto":
131
+ if total_time < 1e-6:
132
+ time_unit = "ns"
133
+ elif total_time < 1e-3:
134
+ time_unit = "us"
135
+ elif total_time < 1:
136
+ time_unit = "ms"
137
+ else:
138
+ time_unit = "s"
139
+
140
+ time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
141
+ multiplier = time_multipliers.get(time_unit, 1.0)
142
+ time_scaled = time * multiplier
143
+
144
+ # Plot with simplified style
145
+ ax.plot(time_scaled, decimated_signal, "b-", linewidth=0.5, antialiased=False)
146
+
147
+ # Minimal labels (no grid, no fancy formatting)
148
+ ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
149
+ ax.set_ylabel("Amplitude", fontsize=8)
150
+
151
+ if title:
152
+ ax.set_title(title, fontsize=9)
153
+
154
+ # Reduce tick label size
155
+ ax.tick_params(labelsize=7)
156
+
157
+ # Tight layout to maximize plot area
158
+ fig.tight_layout(pad=0.5)
159
+
160
+ return fig
161
+
162
+
163
+ def _decimate_uniform(signal: NDArray[np.float64], target_samples: int) -> NDArray[np.float64]:
164
+ """Decimate signal to exactly target_samples using uniform stride.
165
+
166
+ Args:
167
+ signal: Input signal
168
+ target_samples: Target number of samples
169
+
170
+ Returns:
171
+ Decimated signal with exactly target_samples
172
+ """
173
+ if len(signal) <= target_samples:
174
+ return signal
175
+
176
+ # Calculate uniform stride
177
+ stride = len(signal) // target_samples
178
+
179
+ # Sample at uniform intervals
180
+ indices = np.arange(0, len(signal), stride)[:target_samples]
181
+
182
+ decimated: NDArray[np.float64] = signal[indices]
183
+ return decimated
184
+
185
+
186
+ def render_thumbnail_multichannel(
187
+ signals: list[NDArray[np.float64]],
188
+ sample_rate: float,
189
+ *,
190
+ size: tuple[int, int] = (400, 300),
191
+ max_samples: int = 1000,
192
+ time_unit: str = "auto",
193
+ channel_names: list[str] | None = None,
194
+ dpi: int = 72,
195
+ ) -> Figure:
196
+ """Render fast preview thumbnail of multiple channels.
197
+
198
+ : Fast multi-channel preview rendering.
199
+
200
+ Args:
201
+ signals: List of signal arrays
202
+ sample_rate: Sample rate in Hz
203
+ size: Thumbnail size in pixels (width, height)
204
+ max_samples: Maximum samples per channel
205
+ time_unit: Time unit for x-axis
206
+ channel_names: Optional channel names
207
+ dpi: DPI for rendering
208
+
209
+ Returns:
210
+ Matplotlib Figure object
211
+
212
+ Raises:
213
+ ValueError: If inputs are invalid
214
+ ImportError: If matplotlib is not available
215
+
216
+ Example:
217
+ >>> signals = [ch1_data, ch2_data, ch3_data]
218
+ >>> fig = render_thumbnail_multichannel(signals, 1e6)
219
+
220
+ References:
221
+ VIS-018: Thumbnail Mode
222
+ """
223
+ if not HAS_MATPLOTLIB:
224
+ raise ImportError("matplotlib is required for visualization")
225
+
226
+ if len(signals) == 0:
227
+ raise ValueError("Must provide at least one signal")
228
+ if sample_rate <= 0:
229
+ raise ValueError("Sample rate must be positive")
230
+
231
+ n_channels = len(signals)
232
+
233
+ if channel_names is None:
234
+ channel_names = [f"CH{i + 1}" for i in range(n_channels)]
235
+
236
+ # Configure matplotlib for fast rendering
237
+ with plt.rc_context(
238
+ {
239
+ "path.simplify": True,
240
+ "path.simplify_threshold": 1.0,
241
+ "agg.path.chunksize": 1000,
242
+ "lines.antialiased": False,
243
+ "patch.antialiased": False,
244
+ "text.antialiased": False,
245
+ }
246
+ ):
247
+ # Calculate figure size
248
+ width_inches = size[0] / dpi
249
+ height_inches = size[1] / dpi
250
+
251
+ fig, axes = plt.subplots(
252
+ n_channels,
253
+ 1,
254
+ figsize=(width_inches, height_inches),
255
+ dpi=dpi,
256
+ sharex=True,
257
+ )
258
+
259
+ if n_channels == 1:
260
+ axes = [axes]
261
+
262
+ # Get time unit from first signal
263
+ if len(signals[0]) > 0:
264
+ total_time = len(signals[0]) / sample_rate
265
+ if time_unit == "auto":
266
+ if total_time < 1e-6:
267
+ time_unit = "ns"
268
+ elif total_time < 1e-3:
269
+ time_unit = "us"
270
+ elif total_time < 1:
271
+ time_unit = "ms"
272
+ else:
273
+ time_unit = "s"
274
+ else:
275
+ time_unit = "s"
276
+
277
+ time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
278
+ multiplier = time_multipliers.get(time_unit, 1.0)
279
+
280
+ # Plot each channel
281
+ for i, (sig, name, ax) in enumerate(zip(signals, channel_names, axes, strict=False)):
282
+ if len(sig) == 0:
283
+ continue
284
+
285
+ # Decimate signal
286
+ decimated = _decimate_uniform(sig, max_samples)
287
+
288
+ # Time vector
289
+ total_time = len(sig) / sample_rate
290
+ time = np.linspace(0, total_time, len(decimated)) * multiplier
291
+
292
+ # Plot
293
+ ax.plot(time, decimated, "b-", linewidth=0.5, antialiased=False)
294
+
295
+ # Channel label
296
+ ax.set_ylabel(name, fontsize=7, rotation=0, ha="right", va="center")
297
+ ax.tick_params(labelsize=6)
298
+
299
+ # Only x-label on bottom
300
+ if i == n_channels - 1:
301
+ ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
302
+
303
+ fig.tight_layout(pad=0.3)
304
+
305
+ return fig
306
+
307
+
308
+ __all__ = [
309
+ "render_thumbnail",
310
+ "render_thumbnail_multichannel",
311
+ ]