oscura 0.5.1__py3-none-any.whl → 0.7.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 (497) 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/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.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