oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -14,12 +14,12 @@ These functions return WaveformTrace objects ready for use:
14
14
  - generate_pulse: Single pulse with configurable rise/fall times
15
15
 
16
16
  Example:
17
- >>> from oscura.testing import generate_sine_wave, generate_square_wave
17
+ >>> from oscura.validation.testing import generate_sine_wave, generate_square_wave
18
18
  >>> sine = generate_sine_wave(frequency=1e6, amplitude=1.0)
19
19
  >>> square = generate_square_wave(frequency=500e3, duty_cycle=0.3)
20
20
  """
21
21
 
22
- from oscura.testing.synthetic import (
22
+ from oscura.validation.testing.synthetic import (
23
23
  GroundTruth,
24
24
  SyntheticDataGenerator,
25
25
  SyntheticMessageConfig,
@@ -443,7 +443,7 @@ def generate_sine_wave(
443
443
  WaveformTrace containing the sine wave.
444
444
 
445
445
  Example:
446
- >>> from oscura.testing import generate_sine_wave
446
+ >>> from oscura.validation.testing import generate_sine_wave
447
447
  >>> trace = generate_sine_wave(frequency=1e6, amplitude=1.0)
448
448
  >>> print(f"Samples: {len(trace.data)}")
449
449
  """
@@ -483,7 +483,7 @@ def generate_square_wave(
483
483
  WaveformTrace containing the square wave.
484
484
 
485
485
  Example:
486
- >>> from oscura.testing import generate_square_wave
486
+ >>> from oscura.validation.testing import generate_square_wave
487
487
  >>> trace = generate_square_wave(frequency=500e3, duty_cycle=0.3)
488
488
  """
489
489
  num_samples = int(sample_rate * duration)
@@ -520,7 +520,7 @@ def generate_dc(
520
520
  WaveformTrace containing the DC signal.
521
521
 
522
522
  Example:
523
- >>> from oscura.testing import generate_dc
523
+ >>> from oscura.validation.testing import generate_dc
524
524
  >>> trace = generate_dc(level=1.5, duration=10e-6)
525
525
  """
526
526
  num_samples = int(sample_rate * duration)
@@ -559,7 +559,7 @@ def generate_multi_tone(
559
559
  ValueError: If frequencies, amplitudes, and phases have different lengths.
560
560
 
561
561
  Example:
562
- >>> from oscura.testing import generate_multi_tone
562
+ >>> from oscura.validation.testing import generate_multi_tone
563
563
  >>> trace = generate_multi_tone(
564
564
  ... frequencies=[1e6, 2.5e6, 4e6],
565
565
  ... amplitudes=[1.0, 0.5, 0.25]
@@ -616,7 +616,7 @@ def generate_pulse(
616
616
  WaveformTrace containing the pulse.
617
617
 
618
618
  Example:
619
- >>> from oscura.testing import generate_pulse
619
+ >>> from oscura.validation.testing import generate_pulse
620
620
  >>> trace = generate_pulse(width=1e-6, rise_time=10e-9)
621
621
  """
622
622
  num_samples = int(sample_rate * duration)
@@ -3,6 +3,11 @@
3
3
  Provides plotting functions for waveforms, spectra, and other signal data,
4
4
  plus optimization utilities, style presets, and intelligent rendering.
5
5
 
6
+ **Requires matplotlib:**
7
+ This module requires matplotlib to be installed. Install with:
8
+ pip install oscura[visualization] # Just visualization
9
+ pip install oscura[standard] # Recommended
10
+ pip install oscura[all] # Everything
6
11
 
7
12
  Example:
8
13
  >>> from oscura.visualization import plot_waveform, plot_spectrum
@@ -15,6 +20,10 @@ Example:
15
20
  ... plt.savefig("figure.pdf")
16
21
  """
17
22
 
23
+ # NOTE: Matplotlib is optional - individual functions will check and raise
24
+ # helpful errors if matplotlib is not installed when they're called.
25
+ # The module itself can be imported without matplotlib.
26
+
18
27
  # Import plot module as namespace for DSL compatibility
19
28
  from oscura.visualization import plot
20
29
  from oscura.visualization.accessibility import (
@@ -111,7 +111,7 @@ def get_multi_line_styles(n_lines: int) -> list[tuple[tuple[float, float, float,
111
111
  for i in range(n_lines):
112
112
  linestyle = LINE_STYLES[i % len(LINE_STYLES)]
113
113
  # Colors from colormap are RGBA tuples
114
- rgba_color = tuple(colors[i]) # type: ignore[arg-type]
114
+ rgba_color = tuple(colors[i])
115
115
  styles.append((rgba_color, linestyle)) # type: ignore[arg-type]
116
116
 
117
117
  return styles
@@ -20,6 +20,8 @@ from dataclasses import dataclass
20
20
 
21
21
  import numpy as np
22
22
 
23
+ from oscura.utils.geometry import generate_leader_line
24
+
23
25
 
24
26
  @dataclass
25
27
  class Annotation:
@@ -106,57 +108,84 @@ def place_annotations(
106
108
  if len(annotations) == 0:
107
109
  return []
108
110
 
109
- # Filter by viewport if specified
110
- if viewport is not None:
111
- x_min, x_max = viewport
112
- visible_annots = [a for a in annotations if x_min <= a.x <= x_max]
113
- else:
114
- visible_annots = annotations
115
-
116
- # Apply density limiting - keep only top priority annotations
117
- if len(visible_annots) > density_limit:
118
- # Sort by priority (descending)
119
- sorted_annots = sorted(
120
- visible_annots,
121
- key=lambda a: a.priority,
122
- reverse=True,
123
- )
124
- visible_annots = sorted_annots[:density_limit]
125
-
126
- # Initialize placed annotations at anchor points
127
- placed = []
128
- for annot in visible_annots:
129
- placed.append(
130
- PlacedAnnotation(
131
- annotation=annot,
132
- display_x=annot.x,
133
- display_y=annot.y,
134
- visible=True,
135
- needs_leader=False,
136
- )
111
+ # Filter and limit annotations
112
+ visible_annots = _filter_by_viewport(annotations, viewport)
113
+ visible_annots = _apply_density_limit(visible_annots, density_limit)
114
+
115
+ # Initialize placement
116
+ placed = _initialize_placements(visible_annots)
117
+
118
+ # Resolve collisions
119
+ _resolve_collisions(placed, collision_threshold, max_iterations)
120
+
121
+ # Add leader lines where needed
122
+ _add_leader_lines(placed, leader_threshold=30.0)
123
+
124
+ return placed
125
+
126
+
127
+ def _filter_by_viewport(
128
+ annotations: list[Annotation],
129
+ viewport: tuple[float, float] | None,
130
+ ) -> list[Annotation]:
131
+ """Filter annotations by viewport range."""
132
+ if viewport is None:
133
+ return annotations
134
+
135
+ x_min, x_max = viewport
136
+ return [a for a in annotations if x_min <= a.x <= x_max]
137
+
138
+
139
+ def _apply_density_limit(
140
+ annotations: list[Annotation],
141
+ density_limit: int,
142
+ ) -> list[Annotation]:
143
+ """Apply density limiting by keeping top priority annotations."""
144
+ if len(annotations) <= density_limit:
145
+ return annotations
146
+
147
+ sorted_annots = sorted(annotations, key=lambda a: a.priority, reverse=True)
148
+ return sorted_annots[:density_limit]
149
+
150
+
151
+ def _initialize_placements(annotations: list[Annotation]) -> list[PlacedAnnotation]:
152
+ """Initialize placed annotations at anchor points."""
153
+ return [
154
+ PlacedAnnotation(
155
+ annotation=annot,
156
+ display_x=annot.x,
157
+ display_y=annot.y,
158
+ visible=True,
159
+ needs_leader=False,
137
160
  )
161
+ for annot in annotations
162
+ ]
138
163
 
139
- # Resolve collisions using iterative adjustment
164
+
165
+ def _resolve_collisions(
166
+ placed: list[PlacedAnnotation],
167
+ collision_threshold: float,
168
+ max_iterations: int,
169
+ ) -> None:
170
+ """Resolve collisions using iterative adjustment."""
140
171
  for _iteration in range(max_iterations):
141
172
  moved = False
142
173
 
143
- # Check all pairs for collisions
144
174
  for i in range(len(placed)):
145
175
  for j in range(i + 1, len(placed)):
146
176
  if _check_collision(placed[i], placed[j], collision_threshold):
147
- # Resolve collision by moving lower-priority annotation
177
+ # Move lower-priority annotation
148
178
  if placed[i].annotation.priority >= placed[j].annotation.priority:
149
179
  moved = _move_annotation(placed[j], placed[i], collision_threshold) or moved
150
180
  else:
151
181
  moved = _move_annotation(placed[i], placed[j], collision_threshold) or moved
152
182
 
153
- # Converged if nothing moved
154
183
  if not moved:
155
184
  break
156
185
 
157
- # Determine which annotations need leader lines
158
- leader_threshold = 30.0 # pixels
159
186
 
187
+ def _add_leader_lines(placed: list[PlacedAnnotation], leader_threshold: float) -> None:
188
+ """Add leader lines for displaced annotations."""
160
189
  for p in placed:
161
190
  dx = abs(p.display_x - p.annotation.x)
162
191
  dy = abs(p.display_y - p.annotation.y)
@@ -164,13 +193,11 @@ def place_annotations(
164
193
 
165
194
  if displacement > leader_threshold:
166
195
  p.needs_leader = True
167
- p.leader_points = _generate_leader_line(
196
+ p.leader_points = generate_leader_line(
168
197
  (p.annotation.x, p.annotation.y),
169
198
  (p.display_x, p.display_y),
170
199
  )
171
200
 
172
- return placed
173
-
174
201
 
175
202
  def _check_collision(
176
203
  p1: PlacedAnnotation,
@@ -246,36 +273,6 @@ def _move_annotation(
246
273
  return False
247
274
 
248
275
 
249
- def _generate_leader_line(
250
- anchor: tuple[float, float],
251
- label: tuple[float, float],
252
- ) -> list[tuple[float, float]]:
253
- """Generate orthogonal leader line from anchor to label.
254
-
255
- Args:
256
- anchor: Anchor point (x, y)
257
- label: Label position (x, y)
258
-
259
- Returns:
260
- List of points for leader line
261
- """
262
- ax, ay = anchor
263
- lx, ly = label
264
-
265
- # L-shaped leader line
266
- dx = abs(lx - ax)
267
- dy = abs(ly - ay)
268
-
269
- if dx > dy:
270
- # Horizontal-first
271
- mid = (lx, ay)
272
- else:
273
- # Vertical-first
274
- mid = (ax, ly)
275
-
276
- return [anchor, mid, label]
277
-
278
-
279
276
  def filter_by_zoom_level(
280
277
  placed: list[PlacedAnnotation],
281
278
  zoom_range: tuple[float, float],
@@ -256,7 +256,7 @@ def _adjust_for_contrast(
256
256
  b = int(color_val[4:6], 16)
257
257
 
258
258
  # Convert to HSL for easier lightness adjustment
259
- h, s, l = _rgb_to_hsl(r, g, b) # noqa: E741
259
+ h, s, l = _rgb_to_hsl(r, g, b)
260
260
 
261
261
  bg_lum = _relative_luminance(background)
262
262
 
@@ -280,18 +280,18 @@ def _adjust_for_contrast(
280
280
  if bg_lum > 0.5:
281
281
  # Dark background - make lighter
282
282
  l_min = l
283
- l = (l + l_max) / 2 # noqa: E741
283
+ l = (l + l_max) / 2
284
284
  else:
285
285
  # Light background - make darker
286
286
  l_max = l
287
- l = (l_min + l) / 2 # noqa: E741
287
+ l = (l_min + l) / 2
288
288
  # Too much contrast - move back
289
289
  elif bg_lum > 0.5:
290
290
  l_max = l
291
- l = (l_min + l) / 2 # noqa: E741
291
+ l = (l_min + l) / 2
292
292
  else:
293
293
  l_min = l
294
- l = (l + l_max) / 2 # noqa: E741
294
+ l = (l + l_max) / 2
295
295
 
296
296
  iterations += 1
297
297
 
@@ -319,7 +319,7 @@ def _rgb_to_hsl(r: int, g: int, b: int) -> tuple[float, float, float]:
319
319
  delta = max_c - min_c
320
320
 
321
321
  # Lightness
322
- l = (max_c + min_c) / 2.0 # noqa: E741
322
+ l = (max_c + min_c) / 2.0
323
323
 
324
324
  if delta == 0:
325
325
  # Achromatic
@@ -341,7 +341,7 @@ def _rgb_to_hsl(r: int, g: int, b: int) -> tuple[float, float, float]:
341
341
  return (h, s, l)
342
342
 
343
343
 
344
- def _hsl_to_rgb(h: float, s: float, l: float) -> tuple[int, int, int]: # noqa: E741
344
+ def _hsl_to_rgb(h: float, s: float, l: float) -> tuple[int, int, int]:
345
345
  """Convert HSL to RGB color space.
346
346
 
347
347
  Args:
@@ -38,6 +38,172 @@ if TYPE_CHECKING:
38
38
  from oscura.analyzers.protocols.base import Annotation
39
39
 
40
40
 
41
+ def _validate_timing_inputs(
42
+ traces: Sequence[WaveformTrace | DigitalTrace],
43
+ names: list[str] | None,
44
+ ) -> tuple[int, list[str]]:
45
+ """Validate plot_timing inputs and generate default names.
46
+
47
+ Args:
48
+ traces: List of traces to validate.
49
+ names: Channel names or None for defaults.
50
+
51
+ Returns:
52
+ Tuple of (n_channels, validated_names).
53
+
54
+ Raises:
55
+ ValueError: If traces empty or names length mismatch.
56
+ """
57
+ if len(traces) == 0:
58
+ raise ValueError("traces list cannot be empty")
59
+
60
+ n_channels = len(traces)
61
+
62
+ if names is None:
63
+ names = [f"CH{i + 1}" for i in range(n_channels)]
64
+
65
+ if len(names) != n_channels:
66
+ raise ValueError(f"names length ({len(names)}) must match traces ({n_channels})")
67
+
68
+ return n_channels, names
69
+
70
+
71
+ def _convert_to_digital_traces(
72
+ traces: Sequence[WaveformTrace | DigitalTrace],
73
+ threshold: float | str,
74
+ ) -> list[DigitalTrace]:
75
+ """Convert analog traces to digital using threshold.
76
+
77
+ Args:
78
+ traces: List of analog or digital traces.
79
+ threshold: Threshold for analog-to-digital conversion.
80
+
81
+ Returns:
82
+ List of digital traces.
83
+ """
84
+ from oscura.analyzers.digital.extraction import to_digital
85
+
86
+ digital_traces: list[DigitalTrace] = []
87
+ for trace in traces:
88
+ if isinstance(trace, WaveformTrace):
89
+ digital_traces.append(to_digital(trace, threshold=threshold)) # type: ignore[arg-type]
90
+ else:
91
+ digital_traces.append(trace)
92
+
93
+ return digital_traces
94
+
95
+
96
+ def _select_time_unit_and_multiplier(
97
+ digital_traces: list[DigitalTrace],
98
+ time_unit: str,
99
+ ) -> tuple[str, float]:
100
+ """Select appropriate time unit based on signal duration.
101
+
102
+ Args:
103
+ digital_traces: List of digital traces.
104
+ time_unit: Time unit ("auto" or specific unit).
105
+
106
+ Returns:
107
+ Tuple of (time_unit, multiplier).
108
+ """
109
+ if time_unit == "auto" and len(digital_traces) > 0:
110
+ ref_trace = digital_traces[0]
111
+ duration = len(ref_trace.data) * ref_trace.metadata.time_base
112
+ if duration < 1e-6:
113
+ time_unit = "ns"
114
+ elif duration < 1e-3:
115
+ time_unit = "us"
116
+ elif duration < 1:
117
+ time_unit = "ms"
118
+ else:
119
+ time_unit = "s"
120
+
121
+ time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
122
+ multiplier = time_multipliers.get(time_unit, 1.0)
123
+
124
+ return time_unit, multiplier
125
+
126
+
127
+ def _determine_plot_time_range(
128
+ digital_traces: list[DigitalTrace],
129
+ time_range: tuple[float, float] | None,
130
+ ) -> tuple[float, float]:
131
+ """Determine start and end times for plot.
132
+
133
+ Args:
134
+ digital_traces: List of digital traces.
135
+ time_range: User-specified time range or None for auto.
136
+
137
+ Returns:
138
+ Tuple of (start_time, end_time) in seconds.
139
+ """
140
+ if time_range is not None:
141
+ return time_range
142
+
143
+ start_time = 0.0
144
+ end_time = max(trace.duration for trace in digital_traces if len(trace.data) > 0)
145
+ return start_time, end_time
146
+
147
+
148
+ def _plot_timing_channel(
149
+ ax: Axes,
150
+ trace: DigitalTrace,
151
+ name: str,
152
+ channel_index: int,
153
+ multiplier: float,
154
+ time_range: tuple[float, float] | None,
155
+ show_grid: bool,
156
+ annotations: list[Annotation] | None,
157
+ time_unit: str,
158
+ ) -> None:
159
+ """Plot a single channel in the timing diagram.
160
+
161
+ Args:
162
+ ax: Matplotlib axes to plot on.
163
+ trace: Digital trace to plot.
164
+ name: Channel name for label.
165
+ channel_index: Index for color selection.
166
+ multiplier: Time unit multiplier.
167
+ time_range: Optional time range to display.
168
+ show_grid: Show vertical grid lines.
169
+ annotations: Optional protocol annotations.
170
+ time_unit: Time unit string.
171
+ """
172
+ time = trace.time_vector * multiplier
173
+
174
+ # Filter to time range
175
+ if time_range is not None:
176
+ start_time, end_time = time_range
177
+ start_idx = int(np.searchsorted(trace.time_vector, start_time))
178
+ end_idx = int(np.searchsorted(trace.time_vector, end_time))
179
+ time = time[start_idx:end_idx]
180
+ data_slice = trace.data[start_idx:end_idx]
181
+ else:
182
+ data_slice = trace.data
183
+
184
+ # Plot digital waveform as step function
185
+ ax.step(
186
+ time,
187
+ data_slice.astype(int),
188
+ where="post",
189
+ color=f"C{channel_index}",
190
+ linewidth=1.5,
191
+ )
192
+
193
+ # Set up digital signal display
194
+ ax.set_ylim(-0.2, 1.2)
195
+ ax.set_yticks([0, 1])
196
+ ax.set_yticklabels(["0", "1"])
197
+ ax.set_ylabel(name, rotation=0, ha="right", va="center", fontweight="bold")
198
+
199
+ if show_grid:
200
+ ax.grid(True, alpha=0.2, axis="x")
201
+
202
+ # Add protocol annotations if provided
203
+ if annotations:
204
+ _add_protocol_annotations(ax, annotations, multiplier, time_unit)
205
+
206
+
41
207
  def plot_timing(
42
208
  traces: Sequence[WaveformTrace | DigitalTrace],
43
209
  *,
@@ -87,108 +253,41 @@ def plot_timing(
87
253
  if not HAS_MATPLOTLIB:
88
254
  raise ImportError("matplotlib is required for visualization")
89
255
 
90
- if len(traces) == 0:
91
- raise ValueError("traces list cannot be empty")
92
-
93
- n_channels = len(traces)
94
-
95
- if names is None:
96
- names = [f"CH{i + 1}" for i in range(n_channels)]
256
+ # Data preparation/validation
257
+ n_channels, names = _validate_timing_inputs(traces, names)
258
+ digital_traces = _convert_to_digital_traces(traces, threshold)
97
259
 
98
- if len(names) != n_channels:
99
- raise ValueError(f"names length ({len(names)}) must match traces ({n_channels})")
260
+ # Unit/scale selection
261
+ time_unit, multiplier = _select_time_unit_and_multiplier(digital_traces, time_unit)
262
+ start_time, end_time = _determine_plot_time_range(digital_traces, time_range)
100
263
 
264
+ # Figure/axes creation
101
265
  if figsize is None:
102
266
  figsize = (12, 1.5 * n_channels)
103
267
 
104
- # Convert analog traces to digital
105
- from oscura.analyzers.digital.extraction import to_digital
106
-
107
- digital_traces: list[DigitalTrace] = []
108
- for trace in traces:
109
- if isinstance(trace, WaveformTrace):
110
- digital_traces.append(to_digital(trace, threshold=threshold)) # type: ignore[arg-type]
111
- else:
112
- digital_traces.append(trace)
113
-
114
- # Auto-select time unit from first trace
115
- if time_unit == "auto" and len(digital_traces) > 0:
116
- ref_trace = digital_traces[0]
117
- duration = len(ref_trace.data) * ref_trace.metadata.time_base
118
- if duration < 1e-6:
119
- time_unit = "ns"
120
- elif duration < 1e-3:
121
- time_unit = "us"
122
- elif duration < 1:
123
- time_unit = "ms"
124
- else:
125
- time_unit = "s"
126
-
127
- time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
128
- multiplier = time_multipliers.get(time_unit, 1.0)
129
-
130
- # Create figure
131
- fig, axes = plt.subplots(
132
- n_channels,
133
- 1,
134
- figsize=figsize,
135
- sharex=True,
136
- )
268
+ fig, axes = plt.subplots(n_channels, 1, figsize=figsize, sharex=True)
137
269
 
138
270
  if n_channels == 1:
139
271
  axes = [axes]
140
272
 
141
- # Determine time range
142
- if time_range is not None:
143
- start_time, end_time = time_range
144
- else:
145
- start_time = 0.0
146
- end_time = max(trace.duration for trace in digital_traces if len(trace.data) > 0)
147
-
273
+ # Plotting/rendering
148
274
  for i, (trace, name, ax) in enumerate(zip(digital_traces, names, axes, strict=False)):
149
- time = trace.time_vector * multiplier
150
-
151
- # Filter to time range
152
- if time_range is not None:
153
- start_idx = int(np.searchsorted(trace.time_vector, start_time))
154
- end_idx = int(np.searchsorted(trace.time_vector, end_time))
155
- time = time[start_idx:end_idx]
156
- data_slice = trace.data[start_idx:end_idx]
157
- else:
158
- data_slice = trace.data
159
-
160
- # Plot digital waveform as step function
161
- ax.step(
162
- time,
163
- data_slice.astype(int),
164
- where="post",
165
- color=f"C{i}",
166
- linewidth=1.5,
275
+ channel_annotations = annotations[i] if annotations and i < len(annotations) else None
276
+ _plot_timing_channel(
277
+ ax, trace, name, i, multiplier, time_range, show_grid, channel_annotations, time_unit
167
278
  )
168
279
 
169
- # Set up digital signal display
170
- ax.set_ylim(-0.2, 1.2)
171
- ax.set_yticks([0, 1])
172
- ax.set_yticklabels(["0", "1"])
173
- ax.set_ylabel(name, rotation=0, ha="right", va="center", fontweight="bold")
174
-
175
- if show_grid:
176
- ax.grid(True, alpha=0.2, axis="x")
177
-
178
- # Add protocol annotations if provided
179
- if annotations is not None and i < len(annotations) and annotations[i]:
180
- _add_protocol_annotations(ax, annotations[i], multiplier, time_unit)
181
-
182
280
  # Remove x-axis labels except for bottom plot
183
281
  if i < n_channels - 1:
184
282
  ax.set_xticklabels([])
185
283
 
186
- # Set x-label on bottom plot
284
+ # Annotation/labeling
187
285
  axes[-1].set_xlabel(f"Time ({time_unit})")
188
286
 
189
287
  if title:
190
288
  fig.suptitle(title, fontsize=14, fontweight="bold")
191
289
 
290
+ # Layout/formatting
192
291
  fig.tight_layout()
193
292
  return fig
194
293