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
@@ -90,6 +90,31 @@ def plot_tdr(
90
90
  >>> fig = plot_tdr(z_profile, dist, z0=50, show=False)
91
91
  >>> fig.savefig("tdr_impedance.png")
92
92
  """
93
+ _validate_tdr_inputs(impedance, distance)
94
+ fig, ax = _setup_tdr_figure(ax, figsize)
95
+
96
+ distance_unit_final, dist_scaled = _scale_tdr_distance(distance, distance_unit)
97
+ impedance_display = np.clip(impedance, 0, 500)
98
+
99
+ _plot_tdr_impedance_profile(ax, dist_scaled, impedance_display)
100
+ _fill_tdr_impedance_regions(ax, dist_scaled, impedance_display, z0, discontinuity_threshold)
101
+
102
+ if show_reference:
103
+ _add_tdr_reference_line(ax, z0)
104
+
105
+ if show_discontinuities:
106
+ _annotate_tdr_discontinuities(ax, dist_scaled, impedance_display, discontinuity_threshold)
107
+
108
+ _format_tdr_axes(ax, dist_scaled, impedance_display, distance_unit_final, title)
109
+ _finalize_tdr_plot(fig, save_path, show)
110
+
111
+ return fig
112
+
113
+
114
+ def _validate_tdr_inputs(
115
+ impedance: NDArray[np.floating[Any]], distance: NDArray[np.floating[Any]]
116
+ ) -> None:
117
+ """Validate TDR input arrays."""
93
118
  if not HAS_MATPLOTLIB:
94
119
  raise ImportError("matplotlib is required for visualization")
95
120
 
@@ -99,16 +124,23 @@ def plot_tdr(
99
124
  f"(got {len(impedance)} and {len(distance)})"
100
125
  )
101
126
 
102
- # Create figure if needed
127
+
128
+ def _setup_tdr_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
129
+ """Setup TDR figure and axes."""
103
130
  if ax is None:
104
- fig, ax = plt.subplots(figsize=figsize)
105
- else:
106
- fig_temp = ax.get_figure()
107
- if fig_temp is None:
108
- raise ValueError("Axes must have an associated figure")
109
- fig = cast("Figure", fig_temp)
131
+ fig, ax_new = plt.subplots(figsize=figsize)
132
+ return fig, ax_new
133
+
134
+ fig_temp = ax.get_figure()
135
+ if fig_temp is None:
136
+ raise ValueError("Axes must have an associated figure")
137
+ return cast("Figure", fig_temp), ax
110
138
 
111
- # Convert distance units
139
+
140
+ def _scale_tdr_distance(
141
+ distance: NDArray[np.floating[Any]], distance_unit: str
142
+ ) -> tuple[str, NDArray[np.floating[Any]]]:
143
+ """Scale distance to appropriate unit."""
112
144
  if distance_unit == "auto":
113
145
  max_dist = np.max(distance)
114
146
  if max_dist < 0.01:
@@ -124,25 +156,29 @@ def plot_tdr(
124
156
  distance_mult = {"m": 1.0, "cm": 100.0, "mm": 1000.0}.get(distance_unit, 1.0)
125
157
 
126
158
  dist_scaled = distance * distance_mult
159
+ return distance_unit, dist_scaled
127
160
 
128
- # Clip impedance for display (handle inf values)
129
- impedance_display = np.clip(impedance, 0, 500)
130
161
 
131
- # Plot impedance profile
162
+ def _plot_tdr_impedance_profile(
163
+ ax: Axes,
164
+ dist_scaled: NDArray[np.floating[Any]],
165
+ impedance_display: NDArray[np.floating[Any]],
166
+ ) -> None:
167
+ """Plot main impedance profile line."""
132
168
  ax.plot(dist_scaled, impedance_display, "b-", linewidth=2, label="Impedance")
133
169
 
134
- # Fill regions based on impedance deviation from z0
170
+
171
+ def _fill_tdr_impedance_regions(
172
+ ax: Axes,
173
+ dist_scaled: NDArray[np.floating[Any]],
174
+ impedance_display: NDArray[np.floating[Any]],
175
+ z0: float,
176
+ discontinuity_threshold: float,
177
+ ) -> None:
178
+ """Fill colored regions based on impedance deviation."""
135
179
  for i in range(len(dist_scaled) - 1):
136
180
  z = impedance_display[i]
137
- if z > z0 + discontinuity_threshold:
138
- color = "#FFA500" # Orange for high-Z
139
- alpha = 0.3
140
- elif z < z0 - discontinuity_threshold:
141
- color = "#1E90FF" # Blue for low-Z
142
- alpha = 0.3
143
- else:
144
- color = "#90EE90" # Light green for matched
145
- alpha = 0.2
181
+ color, alpha = _get_tdr_region_color(z, z0, discontinuity_threshold)
146
182
 
147
183
  ax.fill_between(
148
184
  [dist_scaled[i], dist_scaled[i + 1]],
@@ -152,52 +188,85 @@ def plot_tdr(
152
188
  alpha=alpha,
153
189
  )
154
190
 
155
- # Reference line
156
- if show_reference:
157
- ax.axhline(z0, color="gray", linestyle="--", linewidth=1.5, label=f"Z0 = {z0} Ω")
158
191
 
159
- # Find and annotate discontinuities
160
- if show_discontinuities:
161
- # Find significant changes
162
- z_diff = np.abs(np.diff(impedance_display))
163
- discontinuities = np.where(z_diff > discontinuity_threshold)[0]
164
-
165
- for idx in discontinuities:
166
- z_before = impedance_display[idx]
167
- z_after = impedance_display[idx + 1]
168
- d = dist_scaled[idx]
169
-
170
- # Determine discontinuity type
171
- if z_after > z_before + discontinuity_threshold:
172
- disc_type = "High-Z"
173
- color = "orange"
174
- elif z_after < z_before - discontinuity_threshold:
175
- disc_type = "Low-Z"
176
- color = "blue"
177
- else:
178
- continue
179
-
180
- # Add marker
181
- ax.plot(d, z_after, "o", color=color, markersize=8)
182
-
183
- # Add annotation
184
- z_str = f"{z_after:.0f}" if z_after < 500 else "Open"
185
- ax.annotate(
186
- f"{disc_type}\n{z_str} Ω",
187
- xy=(d, z_after),
188
- xytext=(10, 10),
189
- textcoords="offset points",
190
- fontsize=8,
191
- ha="left",
192
- bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.8},
193
- )
194
-
195
- # Labels and formatting
192
+ def _get_tdr_region_color(z: float, z0: float, threshold: float) -> tuple[str, float]:
193
+ """Get color and alpha for impedance region."""
194
+ if z > z0 + threshold:
195
+ return "#FFA500", 0.3 # Orange for high-Z
196
+ elif z < z0 - threshold:
197
+ return "#1E90FF", 0.3 # Blue for low-Z
198
+ else:
199
+ return "#90EE90", 0.2 # Light green for matched
200
+
201
+
202
+ def _add_tdr_reference_line(ax: Axes, z0: float) -> None:
203
+ """Add reference impedance line."""
204
+ ax.axhline(z0, color="gray", linestyle="--", linewidth=1.5, label=f"Z0 = {z0} Ω")
205
+
206
+
207
+ def _annotate_tdr_discontinuities(
208
+ ax: Axes,
209
+ dist_scaled: NDArray[np.floating[Any]],
210
+ impedance_display: NDArray[np.floating[Any]],
211
+ discontinuity_threshold: float,
212
+ ) -> None:
213
+ """Find and annotate impedance discontinuities."""
214
+ z_diff = np.abs(np.diff(impedance_display))
215
+ discontinuities = np.where(z_diff > discontinuity_threshold)[0]
216
+
217
+ for idx in discontinuities:
218
+ z_before = impedance_display[idx]
219
+ z_after = impedance_display[idx + 1]
220
+ d = dist_scaled[idx]
221
+
222
+ disc_type, color = _classify_tdr_discontinuity(z_before, z_after, discontinuity_threshold)
223
+ if disc_type is None:
224
+ continue
225
+
226
+ _add_tdr_discontinuity_marker(ax, d, z_after, disc_type, color)
227
+
228
+
229
+ def _classify_tdr_discontinuity(
230
+ z_before: float, z_after: float, threshold: float
231
+ ) -> tuple[str | None, str]:
232
+ """Classify discontinuity type."""
233
+ if z_after > z_before + threshold:
234
+ return "High-Z", "orange"
235
+ elif z_after < z_before - threshold:
236
+ return "Low-Z", "blue"
237
+ return None, ""
238
+
239
+
240
+ def _add_tdr_discontinuity_marker(
241
+ ax: Axes, d: float, z_after: float, disc_type: str, color: str
242
+ ) -> None:
243
+ """Add marker and annotation for discontinuity."""
244
+ ax.plot(d, z_after, "o", color=color, markersize=8)
245
+
246
+ z_str = f"{z_after:.0f}" if z_after < 500 else "Open"
247
+ ax.annotate(
248
+ f"{disc_type}\n{z_str} Ω",
249
+ xy=(d, z_after),
250
+ xytext=(10, 10),
251
+ textcoords="offset points",
252
+ fontsize=8,
253
+ ha="left",
254
+ bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.8},
255
+ )
256
+
257
+
258
+ def _format_tdr_axes(
259
+ ax: Axes,
260
+ dist_scaled: NDArray[np.floating[Any]],
261
+ impedance_display: NDArray[np.floating[Any]],
262
+ distance_unit: str,
263
+ title: str | None,
264
+ ) -> None:
265
+ """Format axes labels, limits, and title."""
196
266
  ax.set_xlabel(f"Distance ({distance_unit})", fontsize=11)
197
267
  ax.set_ylabel("Impedance (Ω)", fontsize=11)
198
268
  ax.set_xlim(0, dist_scaled[-1])
199
269
 
200
- # Set y-axis limits with padding
201
270
  y_min = max(0, np.min(impedance_display) - 10)
202
271
  y_max = min(200, np.max(impedance_display) + 10)
203
272
  ax.set_ylim(y_min, y_max)
@@ -205,22 +274,73 @@ def plot_tdr(
205
274
  ax.grid(True, alpha=0.3)
206
275
  ax.legend(loc="upper right")
207
276
 
208
- if title:
209
- ax.set_title(title, fontsize=12, fontweight="bold")
210
- else:
211
- ax.set_title("TDR Impedance Profile", fontsize=12, fontweight="bold")
277
+ title_text = title if title else "TDR Impedance Profile"
278
+ ax.set_title(title_text, fontsize=12, fontweight="bold")
279
+
212
280
 
281
+ def _finalize_tdr_plot(fig: Figure, save_path: str | Path | None, show: bool) -> None:
282
+ """Finalize plot layout, save, and show."""
213
283
  fig.tight_layout()
214
284
 
215
- # Save if requested
216
285
  if save_path is not None:
217
286
  fig.savefig(save_path, dpi=300, bbox_inches="tight")
218
287
 
219
- # Show if requested
220
288
  if show:
221
289
  plt.show()
222
290
 
223
- return fig
291
+
292
+ def _select_sparams_freq_unit(
293
+ frequencies: NDArray[np.floating[Any]], freq_unit: str
294
+ ) -> tuple[str, float]:
295
+ """Select frequency unit and divisor for S-parameter plots."""
296
+ if freq_unit == "auto":
297
+ max_freq = np.max(frequencies)
298
+ if max_freq >= 1e9:
299
+ return "GHz", 1e9
300
+ elif max_freq >= 1e6:
301
+ return "MHz", 1e6
302
+ elif max_freq >= 1e3:
303
+ return "kHz", 1e3
304
+ else:
305
+ return "Hz", 1.0
306
+ else:
307
+ freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
308
+ return freq_unit, freq_div
309
+
310
+
311
+ def _convert_sparam_to_db(s: NDArray[Any]) -> NDArray[np.floating[Any]]:
312
+ """Convert S-parameter to dB."""
313
+ if np.iscomplexobj(s):
314
+ result: NDArray[np.floating[Any]] = 20 * np.log10(np.abs(s) + 1e-12)
315
+ return result
316
+ return np.asarray(s, dtype=np.float64)
317
+
318
+
319
+ def _add_3db_marker(
320
+ ax: Axes,
321
+ freq_scaled: NDArray[np.floating[Any]],
322
+ s_db: NDArray[np.floating[Any]],
323
+ freq_unit: str,
324
+ ) -> None:
325
+ """Add -3dB bandwidth marker to S21 plot."""
326
+ max_db = np.max(s_db)
327
+ db_3_level = max_db - 3
328
+
329
+ crossings = np.where(np.diff(np.sign(s_db - db_3_level)))[0]
330
+ if len(crossings) > 0:
331
+ f_3db = float(freq_scaled[crossings[0]])
332
+ db_3_level_float = float(db_3_level)
333
+ ax.axhline(db_3_level_float, color="gray", linestyle=":", alpha=0.7, linewidth=1)
334
+ ax.axvline(f_3db, color="gray", linestyle=":", alpha=0.7, linewidth=1)
335
+ ax.plot(f_3db, db_3_level_float, "ko", markersize=6)
336
+ ax.annotate(
337
+ f"-3dB: {f_3db:.2f} {freq_unit}",
338
+ xy=(f_3db, db_3_level_float),
339
+ xytext=(10, -15),
340
+ textcoords="offset points",
341
+ fontsize=9,
342
+ ha="left",
343
+ )
224
344
 
225
345
 
226
346
  def plot_sparams_magnitude(
@@ -270,51 +390,17 @@ def plot_sparams_magnitude(
270
390
  if not HAS_MATPLOTLIB:
271
391
  raise ImportError("matplotlib is required for visualization")
272
392
 
273
- # Create figure if needed
274
- if ax is None:
275
- fig, ax = plt.subplots(figsize=figsize)
276
- else:
277
- fig_temp = ax.get_figure()
278
- if fig_temp is None:
279
- raise ValueError("Axes must have an associated figure")
280
- fig = cast("Figure", fig_temp)
281
-
282
- # Select frequency unit
283
- if freq_unit == "auto":
284
- max_freq = np.max(frequencies)
285
- if max_freq >= 1e9:
286
- freq_unit = "GHz"
287
- freq_div = 1e9
288
- elif max_freq >= 1e6:
289
- freq_unit = "MHz"
290
- freq_div = 1e6
291
- elif max_freq >= 1e3:
292
- freq_unit = "kHz"
293
- freq_div = 1e3
294
- else:
295
- freq_unit = "Hz"
296
- freq_div = 1.0
297
- else:
298
- freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
299
-
393
+ fig, ax = _setup_tdr_figure(ax, figsize)
394
+ freq_unit, freq_div = _select_sparams_freq_unit(frequencies, freq_unit)
300
395
  freq_scaled = frequencies / freq_div
301
396
 
302
- def to_db(s: NDArray[Any]) -> NDArray[np.floating[Any]]:
303
- """Convert S-parameter to dB."""
304
- if np.iscomplexobj(s):
305
- result: NDArray[np.floating[Any]] = 20 * np.log10(np.abs(s) + 1e-12)
306
- return result
307
- return np.asarray(s, dtype=np.float64)
308
-
309
- # Color scheme
310
397
  colors = {"S11": "#E74C3C", "S21": "#3498DB", "S12": "#2ECC71", "S22": "#9B59B6"}
311
398
  linestyles = {"S11": "-", "S21": "-", "S12": "--", "S22": "--"}
312
-
313
399
  params = [("S11", s11), ("S21", s21), ("S12", s12), ("S22", s22)]
314
400
 
315
401
  for name, s_param in params:
316
402
  if s_param is not None:
317
- s_db = to_db(s_param)
403
+ s_db = _convert_sparam_to_db(s_param)
318
404
  ax.semilogx(
319
405
  freq_scaled,
320
406
  s_db,
@@ -324,46 +410,21 @@ def plot_sparams_magnitude(
324
410
  label=name,
325
411
  )
326
412
 
327
- # -3dB marker for S21
328
413
  if name == "S21" and db_3_marker:
329
- max_db = np.max(s_db)
330
- db_3_level = max_db - 3
331
-
332
- # Find -3dB crossover
333
- crossings = np.where(np.diff(np.sign(s_db - db_3_level)))[0]
334
- if len(crossings) > 0:
335
- f_3db = float(freq_scaled[crossings[0]])
336
- db_3_level_float = float(db_3_level)
337
- ax.axhline(
338
- db_3_level_float, color="gray", linestyle=":", alpha=0.7, linewidth=1
339
- )
340
- ax.axvline(f_3db, color="gray", linestyle=":", alpha=0.7, linewidth=1)
341
- ax.plot(f_3db, db_3_level_float, "ko", markersize=6)
342
- ax.annotate(
343
- f"-3dB: {f_3db:.2f} {freq_unit}",
344
- xy=(f_3db, db_3_level_float),
345
- xytext=(10, -15),
346
- textcoords="offset points",
347
- fontsize=9,
348
- ha="left",
349
- )
350
-
351
- # Labels and formatting
414
+ _add_3db_marker(ax, freq_scaled, s_db, freq_unit)
415
+
352
416
  ax.set_xlabel(f"Frequency ({freq_unit})", fontsize=11)
353
417
  ax.set_ylabel("Magnitude (dB)", fontsize=11)
354
418
  ax.grid(True, which="both", alpha=0.3)
355
419
  ax.legend(loc="best")
356
-
357
- if title:
358
- ax.set_title(title, fontsize=12, fontweight="bold")
359
- else:
360
- ax.set_title("S-Parameter Magnitude Response", fontsize=12, fontweight="bold")
420
+ ax.set_title(
421
+ title if title else "S-Parameter Magnitude Response", fontsize=12, fontweight="bold"
422
+ )
361
423
 
362
424
  fig.tight_layout()
363
425
 
364
426
  if save_path is not None:
365
427
  fig.savefig(save_path, dpi=300, bbox_inches="tight")
366
-
367
428
  if show:
368
429
  plt.show()
369
430
 
@@ -517,44 +578,127 @@ def plot_setup_hold_timing(
517
578
  if not HAS_MATPLOTLIB:
518
579
  raise ImportError("matplotlib is required for visualization")
519
580
 
520
- # Create figure with multiple rows
581
+ # Create figure and axes
582
+ fig, axes = _create_timing_figure(ax, clock_data, figsize)
583
+
584
+ # Determine time scaling
585
+ time_unit_final, time_mult = _select_time_unit(time_unit, clock_edges, data_edges)
586
+ setup_scaled = setup_time * time_mult
587
+ hold_scaled = hold_time * time_mult
588
+
589
+ # Plot waveforms if provided
590
+ ax_timing = _plot_timing_waveforms(axes, clock_data, data_data, time_axis, time_mult)
591
+
592
+ # Setup timing annotation panel
593
+ _setup_timing_panel(ax_timing, clock_edges, data_edges, time_mult)
594
+
595
+ # Draw timing arrows
596
+ if show_margins:
597
+ _draw_timing_arrows(
598
+ ax_timing,
599
+ clock_edges,
600
+ data_edges,
601
+ setup_scaled,
602
+ hold_scaled,
603
+ time_mult,
604
+ time_unit_final,
605
+ )
606
+
607
+ # Add pass/fail status
608
+ _add_passfail_status(
609
+ ax_timing, setup_time, hold_time, setup_spec, hold_spec, time_mult, time_unit_final
610
+ )
611
+
612
+ # Finalize plot
613
+ axes[-1].set_xlabel(f"Time ({time_unit_final})", fontsize=11)
614
+ fig.suptitle(title if title else "Setup/Hold Timing Analysis", fontsize=14, fontweight="bold")
615
+ fig.tight_layout()
616
+
617
+ if save_path is not None:
618
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
619
+ if show:
620
+ plt.show()
621
+
622
+ return fig
623
+
624
+
625
+ def _create_timing_figure(
626
+ ax: Axes | None,
627
+ clock_data: NDArray[np.floating[Any]] | None,
628
+ figsize: tuple[float, float],
629
+ ) -> tuple[Figure, list[Any]]:
630
+ """Create figure and axes for timing diagram.
631
+
632
+ Args:
633
+ ax: Existing axes or None.
634
+ clock_data: Clock waveform data (determines row count).
635
+ figsize: Figure size.
636
+
637
+ Returns:
638
+ Tuple of (figure, axes_list).
639
+ """
521
640
  if ax is not None:
522
641
  fig_temp = ax.get_figure()
523
642
  if fig_temp is None:
524
643
  raise ValueError("Axes must have an associated figure")
525
- fig = cast("Figure", fig_temp)
526
- axes = [ax]
527
- n_rows = 1
528
- else:
529
- n_rows = 3 if clock_data is not None else 1
530
- fig, axes = plt.subplots(
531
- n_rows, 1, figsize=figsize, sharex=True, gridspec_kw={"height_ratios": [1] * n_rows}
532
- )
533
- if n_rows == 1:
534
- axes = [axes]
644
+ return cast("Figure", fig_temp), [ax]
645
+
646
+ n_rows = 3 if clock_data is not None else 1
647
+ fig, axes = plt.subplots(
648
+ n_rows, 1, figsize=figsize, sharex=True, gridspec_kw={"height_ratios": [1] * n_rows}
649
+ )
650
+ return fig, [axes] if n_rows == 1 else axes
651
+
652
+
653
+ def _select_time_unit(
654
+ time_unit: str,
655
+ clock_edges: NDArray[np.floating[Any]],
656
+ data_edges: NDArray[np.floating[Any]],
657
+ ) -> tuple[str, float]:
658
+ """Select appropriate time unit and multiplier.
535
659
 
536
- # Select time unit
660
+ Args:
661
+ time_unit: Requested unit or "auto".
662
+ clock_edges: Clock edge times.
663
+ data_edges: Data edge times.
664
+
665
+ Returns:
666
+ Tuple of (unit_string, multiplier).
667
+ """
537
668
  if time_unit == "auto":
538
669
  max_time = max(np.max(clock_edges), np.max(data_edges))
539
670
  if max_time < 1e-9:
540
- time_unit = "ps"
541
- time_mult = 1e12
671
+ return "ps", 1e12
542
672
  elif max_time < 1e-6:
543
- time_unit = "ns"
544
- time_mult = 1e9
673
+ return "ns", 1e9
545
674
  elif max_time < 1e-3:
546
- time_unit = "us"
547
- time_mult = 1e6
675
+ return "us", 1e6
548
676
  else:
549
- time_unit = "ms"
550
- time_mult = 1e3
677
+ return "ms", 1e3
551
678
  else:
552
- time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
679
+ mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
680
+ return time_unit, mult
553
681
 
554
- setup_scaled = setup_time * time_mult
555
- hold_scaled = hold_time * time_mult
556
682
 
557
- # If waveforms provided, plot them
683
+ def _plot_timing_waveforms(
684
+ axes: list[Any],
685
+ clock_data: NDArray[np.floating[Any]] | None,
686
+ data_data: NDArray[np.floating[Any]] | None,
687
+ time_axis: NDArray[np.floating[Any]] | None,
688
+ time_mult: float,
689
+ ) -> Any:
690
+ """Plot clock and data waveforms.
691
+
692
+ Args:
693
+ axes: List of axes.
694
+ clock_data: Clock waveform.
695
+ data_data: Data waveform.
696
+ time_axis: Time axis.
697
+ time_mult: Time multiplier.
698
+
699
+ Returns:
700
+ Axes for timing annotations.
701
+ """
558
702
  if clock_data is not None and data_data is not None and time_axis is not None:
559
703
  time_scaled = time_axis * time_mult
560
704
 
@@ -578,88 +722,131 @@ def plot_setup_hold_timing(
578
722
  ax_data.set_yticks([0, 1])
579
723
  ax_data.grid(True, axis="x", alpha=0.3)
580
724
 
581
- ax_timing = axes[2] if len(axes) > 2 else axes[-1]
725
+ return axes[2] if len(axes) > 2 else axes[-1]
582
726
  else:
583
- ax_timing = axes[0]
584
-
585
- # Timing annotation panel
586
- ax_timing.set_ylim(0, 1)
587
- ax_timing.set_xlim(0, max(clock_edges[-1], data_edges[-1]) * time_mult * 1.1)
588
- ax_timing.axis("off")
589
-
590
- # Draw timing arrows for first clock edge
591
- if len(clock_edges) > 0 and len(data_edges) > 0:
592
- clk_edge = clock_edges[0] * time_mult
593
-
594
- # Find nearest data edge before clock
595
- data_before = data_edges[data_edges < clock_edges[0]]
596
- if len(data_before) > 0:
597
- data_edge = data_before[-1] * time_mult
598
-
599
- # Setup time arrow (data_edge to clk_edge)
600
- if show_margins:
601
- y_setup = 0.7
602
- ax_timing.annotate(
603
- "",
604
- xy=(clk_edge, y_setup),
605
- xytext=(data_edge, y_setup),
606
- arrowprops={
607
- "arrowstyle": "<->",
608
- "color": "#27AE60",
609
- "lw": 2,
610
- },
611
- )
612
- ax_timing.text(
613
- (data_edge + clk_edge) / 2,
614
- y_setup + 0.1,
615
- f"Setup: {setup_scaled:.2f} {time_unit}",
616
- ha="center",
617
- va="bottom",
618
- fontsize=10,
619
- fontweight="bold",
620
- color="#27AE60",
621
- )
622
-
623
- # Find nearest data edge after clock
624
- data_after = data_edges[data_edges > clock_edges[0]]
625
- if len(data_after) > 0:
626
- data_edge_after = data_after[0] * time_mult
627
-
628
- # Hold time arrow (clk_edge to data_edge_after)
629
- if show_margins:
630
- y_hold = 0.3
631
- ax_timing.annotate(
632
- "",
633
- xy=(data_edge_after, y_hold),
634
- xytext=(clk_edge, y_hold),
635
- arrowprops={
636
- "arrowstyle": "<->",
637
- "color": "#E67E22",
638
- "lw": 2,
639
- },
640
- )
641
- ax_timing.text(
642
- (clk_edge + data_edge_after) / 2,
643
- y_hold + 0.1,
644
- f"Hold: {hold_scaled:.2f} {time_unit}",
645
- ha="center",
646
- va="bottom",
647
- fontsize=10,
648
- fontweight="bold",
649
- color="#E67E22",
650
- )
727
+ return axes[0]
651
728
 
652
- # Add pass/fail status
729
+
730
+ def _setup_timing_panel(
731
+ ax: Any,
732
+ clock_edges: NDArray[np.floating[Any]],
733
+ data_edges: NDArray[np.floating[Any]],
734
+ time_mult: float,
735
+ ) -> None:
736
+ """Setup timing annotation panel.
737
+
738
+ Args:
739
+ ax: Timing axes.
740
+ clock_edges: Clock edge times.
741
+ data_edges: Data edge times.
742
+ time_mult: Time multiplier.
743
+ """
744
+ ax.set_ylim(0, 1)
745
+ ax.set_xlim(0, max(clock_edges[-1], data_edges[-1]) * time_mult * 1.1)
746
+ ax.axis("off")
747
+
748
+
749
+ def _draw_timing_arrows(
750
+ ax: Any,
751
+ clock_edges: NDArray[np.floating[Any]],
752
+ data_edges: NDArray[np.floating[Any]],
753
+ setup_scaled: float,
754
+ hold_scaled: float,
755
+ time_mult: float,
756
+ time_unit: str,
757
+ ) -> None:
758
+ """Draw setup and hold timing arrows.
759
+
760
+ Args:
761
+ ax: Timing axes.
762
+ clock_edges: Clock edge times.
763
+ data_edges: Data edge times.
764
+ setup_scaled: Scaled setup time.
765
+ hold_scaled: Scaled hold time.
766
+ time_mult: Time multiplier.
767
+ time_unit: Time unit string.
768
+ """
769
+ if len(clock_edges) == 0 or len(data_edges) == 0:
770
+ return
771
+
772
+ clk_edge = clock_edges[0] * time_mult
773
+
774
+ # Setup time arrow
775
+ data_before = data_edges[data_edges < clock_edges[0]]
776
+ if len(data_before) > 0:
777
+ data_edge = data_before[-1] * time_mult
778
+ y_setup = 0.7
779
+ ax.annotate(
780
+ "",
781
+ xy=(clk_edge, y_setup),
782
+ xytext=(data_edge, y_setup),
783
+ arrowprops={"arrowstyle": "<->", "color": "#27AE60", "lw": 2},
784
+ )
785
+ ax.text(
786
+ (data_edge + clk_edge) / 2,
787
+ y_setup + 0.1,
788
+ f"Setup: {setup_scaled:.2f} {time_unit}",
789
+ ha="center",
790
+ va="bottom",
791
+ fontsize=10,
792
+ fontweight="bold",
793
+ color="#27AE60",
794
+ )
795
+
796
+ # Hold time arrow
797
+ data_after = data_edges[data_edges > clock_edges[0]]
798
+ if len(data_after) > 0:
799
+ data_edge_after = data_after[0] * time_mult
800
+ y_hold = 0.3
801
+ ax.annotate(
802
+ "",
803
+ xy=(data_edge_after, y_hold),
804
+ xytext=(clk_edge, y_hold),
805
+ arrowprops={"arrowstyle": "<->", "color": "#E67E22", "lw": 2},
806
+ )
807
+ ax.text(
808
+ (clk_edge + data_edge_after) / 2,
809
+ y_hold + 0.1,
810
+ f"Hold: {hold_scaled:.2f} {time_unit}",
811
+ ha="center",
812
+ va="bottom",
813
+ fontsize=10,
814
+ fontweight="bold",
815
+ color="#E67E22",
816
+ )
817
+
818
+
819
+ def _add_passfail_status(
820
+ ax: Any,
821
+ setup_time: float,
822
+ hold_time: float,
823
+ setup_spec: float | None,
824
+ hold_spec: float | None,
825
+ time_mult: float,
826
+ time_unit: str,
827
+ ) -> None:
828
+ """Add pass/fail status text.
829
+
830
+ Args:
831
+ ax: Timing axes.
832
+ setup_time: Measured setup time.
833
+ hold_time: Measured hold time.
834
+ setup_spec: Setup specification.
835
+ hold_spec: Hold specification.
836
+ time_mult: Time multiplier.
837
+ time_unit: Time unit string.
838
+ """
653
839
  status_y = 0.9
840
+
654
841
  if setup_spec is not None:
655
842
  setup_pass = setup_time >= setup_spec
656
843
  status = "PASS" if setup_pass else "FAIL"
657
844
  color = "#27AE60" if setup_pass else "#E74C3C"
658
- ax_timing.text(
845
+ ax.text(
659
846
  0.02,
660
847
  status_y,
661
848
  f"Setup: {status} (spec: {setup_spec * time_mult:.2f} {time_unit})",
662
- transform=ax_timing.transAxes,
849
+ transform=ax.transAxes,
663
850
  fontsize=10,
664
851
  color=color,
665
852
  fontweight="bold",
@@ -670,34 +857,16 @@ def plot_setup_hold_timing(
670
857
  hold_pass = hold_time >= hold_spec
671
858
  status = "PASS" if hold_pass else "FAIL"
672
859
  color = "#27AE60" if hold_pass else "#E74C3C"
673
- ax_timing.text(
860
+ ax.text(
674
861
  0.02,
675
862
  status_y,
676
863
  f"Hold: {status} (spec: {hold_spec * time_mult:.2f} {time_unit})",
677
- transform=ax_timing.transAxes,
864
+ transform=ax.transAxes,
678
865
  fontsize=10,
679
866
  color=color,
680
867
  fontweight="bold",
681
868
  )
682
869
 
683
- # Set x-label on bottom axes
684
- axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
685
-
686
- if title:
687
- fig.suptitle(title, fontsize=14, fontweight="bold")
688
- else:
689
- fig.suptitle("Setup/Hold Timing Analysis", fontsize=14, fontweight="bold")
690
-
691
- fig.tight_layout()
692
-
693
- if save_path is not None:
694
- fig.savefig(save_path, dpi=300, bbox_inches="tight")
695
-
696
- if show:
697
- plt.show()
698
-
699
- return fig
700
-
701
870
 
702
871
  def plot_timing_margin(
703
872
  setup_times: NDArray[np.floating[Any]],
@@ -735,6 +904,32 @@ def plot_timing_margin(
735
904
  if not HAS_MATPLOTLIB:
736
905
  raise ImportError("matplotlib is required for visualization")
737
906
 
907
+ fig, ax = _get_or_create_axes(ax, figsize)
908
+ time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
909
+
910
+ setup_scaled, hold_scaled = setup_times * time_mult, hold_times * time_mult
911
+
912
+ # Scatter plot
913
+ ax.scatter(setup_scaled, hold_scaled, c="#3498DB", alpha=0.6, s=50)
914
+
915
+ # Add specification lines and regions
916
+ _add_spec_lines(ax, setup_spec, hold_spec, time_mult, time_unit)
917
+ _add_pass_region(ax, setup_spec, hold_spec, time_mult)
918
+
919
+ # Configure axes
920
+ ax.set_xlabel(f"Setup Time ({time_unit})", fontsize=11)
921
+ ax.set_ylabel(f"Hold Time ({time_unit})", fontsize=11)
922
+ ax.grid(True, alpha=0.3)
923
+ ax.legend(loc="best")
924
+ ax.set_title(title or "Setup/Hold Timing Margin", fontsize=12, fontweight="bold")
925
+
926
+ fig.tight_layout()
927
+ _save_and_show_figure(fig, save_path, show)
928
+ return fig
929
+
930
+
931
+ def _get_or_create_axes(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
932
+ """Get existing axes or create new figure with axes."""
738
933
  if ax is None:
739
934
  fig, ax = plt.subplots(figsize=figsize)
740
935
  else:
@@ -742,16 +937,13 @@ def plot_timing_margin(
742
937
  if fig_temp is None:
743
938
  raise ValueError("Axes must have an associated figure")
744
939
  fig = cast("Figure", fig_temp)
940
+ return fig, ax
745
941
 
746
- time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
747
-
748
- setup_scaled = setup_times * time_mult
749
- hold_scaled = hold_times * time_mult
750
942
 
751
- # Scatter plot
752
- ax.scatter(setup_scaled, hold_scaled, c="#3498DB", alpha=0.6, s=50)
753
-
754
- # Add specification lines if provided
943
+ def _add_spec_lines(
944
+ ax: Axes, setup_spec: float | None, hold_spec: float | None, time_mult: float, time_unit: str
945
+ ) -> None:
946
+ """Add specification lines to timing margin plot."""
755
947
  if setup_spec is not None:
756
948
  spec_scaled = setup_spec * time_mult
757
949
  ax.axvline(
@@ -772,12 +964,13 @@ def plot_timing_margin(
772
964
  label=f"Hold Spec ({spec_scaled:.2f} {time_unit})",
773
965
  )
774
966
 
775
- # Mark pass/fail regions
776
- if setup_spec is not None and hold_spec is not None:
777
- x_lim = ax.get_xlim()
778
- y_lim = ax.get_ylim()
779
967
 
780
- # Pass region (upper right)
968
+ def _add_pass_region(
969
+ ax: Axes, setup_spec: float | None, hold_spec: float | None, time_mult: float
970
+ ) -> None:
971
+ """Add pass/fail region shading to timing margin plot."""
972
+ if setup_spec is not None and hold_spec is not None:
973
+ x_lim, y_lim = ax.get_xlim(), ax.get_ylim()
781
974
  ax.fill_between(
782
975
  [setup_spec * time_mult, x_lim[1]],
783
976
  [hold_spec * time_mult, hold_spec * time_mult],
@@ -787,22 +980,10 @@ def plot_timing_margin(
787
980
  label="Pass Region",
788
981
  )
789
982
 
790
- ax.set_xlabel(f"Setup Time ({time_unit})", fontsize=11)
791
- ax.set_ylabel(f"Hold Time ({time_unit})", fontsize=11)
792
- ax.grid(True, alpha=0.3)
793
- ax.legend(loc="best")
794
-
795
- if title:
796
- ax.set_title(title, fontsize=12, fontweight="bold")
797
- else:
798
- ax.set_title("Setup/Hold Timing Margin", fontsize=12, fontweight="bold")
799
-
800
- fig.tight_layout()
801
983
 
984
+ def _save_and_show_figure(fig: Figure, save_path: str | Path | None, show: bool) -> None:
985
+ """Save and optionally show figure."""
802
986
  if save_path is not None:
803
987
  fig.savefig(save_path, dpi=300, bbox_inches="tight")
804
-
805
988
  if show:
806
989
  plt.show()
807
-
808
- return fig