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
@@ -44,6 +44,138 @@ __all__ = [
44
44
  ]
45
45
 
46
46
 
47
+ def _determine_tie_time_unit(
48
+ tie_data: NDArray[np.floating[Any]], time_unit: str
49
+ ) -> tuple[str, float]:
50
+ """Determine time unit and multiplier for TIE display.
51
+
52
+ Args:
53
+ tie_data: TIE values in seconds.
54
+ time_unit: Requested time unit or "auto".
55
+
56
+ Returns:
57
+ Tuple of (time_unit, time_multiplier).
58
+ """
59
+ if time_unit == "auto":
60
+ max_tie = np.max(np.abs(tie_data))
61
+ if max_tie < 1e-12:
62
+ return "fs", 1e15
63
+ elif max_tie < 1e-9:
64
+ return "ps", 1e12
65
+ elif max_tie < 1e-6:
66
+ return "ns", 1e9
67
+ else:
68
+ return "us", 1e6
69
+ else:
70
+ time_mult_map = {
71
+ "s": 1,
72
+ "ms": 1e3,
73
+ "us": 1e6,
74
+ "ns": 1e9,
75
+ "ps": 1e12,
76
+ "fs": 1e15,
77
+ }
78
+ if time_unit in time_mult_map:
79
+ return time_unit, time_mult_map[time_unit]
80
+ else:
81
+ # Fallback to ps for invalid unit
82
+ return "ps", 1e12
83
+
84
+
85
+ def _calculate_tie_statistics(
86
+ tie_scaled: NDArray[np.floating[Any]],
87
+ ) -> tuple[float, float, float, float]:
88
+ """Calculate TIE statistical metrics.
89
+
90
+ Args:
91
+ tie_scaled: Scaled TIE values.
92
+
93
+ Returns:
94
+ Tuple of (mean, std, peak-to-peak, rms).
95
+ """
96
+ mean_val = float(np.mean(tie_scaled))
97
+ std_val = float(np.std(tie_scaled))
98
+ pp_val = float(np.ptp(tie_scaled))
99
+ rms_val = float(np.sqrt(np.mean(tie_scaled**2)))
100
+ return mean_val, std_val, pp_val, rms_val
101
+
102
+
103
+ def _add_gaussian_fit(
104
+ ax: Axes, bin_edges: NDArray[np.floating[Any]], mean_val: float, std_val: float, time_unit: str
105
+ ) -> None:
106
+ """Add Gaussian fit overlay to histogram.
107
+
108
+ Args:
109
+ ax: Matplotlib axes to plot on.
110
+ bin_edges: Histogram bin edges.
111
+ mean_val: Mean value.
112
+ std_val: Standard deviation.
113
+ time_unit: Time unit string for label.
114
+ """
115
+ if not HAS_SCIPY:
116
+ return
117
+
118
+ x_fit = np.linspace(bin_edges[0], bin_edges[-1], 200)
119
+ y_fit = norm.pdf(x_fit, mean_val, std_val)
120
+ ax.plot(
121
+ x_fit, y_fit, "r-", linewidth=2, label=f"Gaussian Fit (sigma={std_val:.2f} {time_unit})"
122
+ )
123
+
124
+
125
+ def _add_rj_dj_indicators(ax: Axes, mean_val: float, std_val: float) -> None:
126
+ """Add RJ/DJ separation indicators to plot.
127
+
128
+ Args:
129
+ ax: Matplotlib axes to plot on.
130
+ mean_val: Mean value.
131
+ std_val: Standard deviation.
132
+ """
133
+ # Mark ±3sigma region (RJ contribution)
134
+ ax.axvline(mean_val - 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7)
135
+ ax.axvline(mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7)
136
+
137
+ # Shade RJ region
138
+ ax.axvspan(
139
+ mean_val - 3 * std_val,
140
+ mean_val + 3 * std_val,
141
+ alpha=0.1,
142
+ color="#E74C3C",
143
+ label="±3sigma (99.7% RJ)",
144
+ )
145
+
146
+
147
+ def _add_statistics_box(
148
+ ax: Axes, mean_val: float, rms_val: float, std_val: float, pp_val: float, time_unit: str
149
+ ) -> None:
150
+ """Add statistics text box to plot.
151
+
152
+ Args:
153
+ ax: Matplotlib axes to plot on.
154
+ mean_val: Mean value.
155
+ rms_val: RMS value.
156
+ std_val: Standard deviation.
157
+ pp_val: Peak-to-peak value.
158
+ time_unit: Time unit string.
159
+ """
160
+ stats_text = (
161
+ f"Mean: {mean_val:.2f} {time_unit}\n"
162
+ f"RMS: {rms_val:.2f} {time_unit}\n"
163
+ f"Std Dev: {std_val:.2f} {time_unit}\n"
164
+ f"Peak-Peak: {pp_val:.2f} {time_unit}"
165
+ )
166
+ ax.text(
167
+ 0.98,
168
+ 0.98,
169
+ stats_text,
170
+ transform=ax.transAxes,
171
+ fontsize=9,
172
+ verticalalignment="top",
173
+ horizontalalignment="right",
174
+ bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
175
+ fontfamily="monospace",
176
+ )
177
+
178
+
47
179
  def plot_tie_histogram(
48
180
  tie_data: NDArray[np.floating[Any]],
49
181
  *,
@@ -68,7 +200,7 @@ def plot_tie_histogram(
68
200
  ax: Matplotlib axes. If None, creates new figure.
69
201
  figsize: Figure size in inches.
70
202
  title: Plot title.
71
- time_unit: Time unit ("s", "ms", "us", "ns", "ps", "auto").
203
+ time_unit: Time unit ("s", "ms", "us", "ns", "ps", "fs", "auto").
72
204
  bins: Number of bins or "auto" for automatic selection.
73
205
  show_gaussian_fit: Overlay Gaussian fit for RJ estimation.
74
206
  show_statistics: Show statistics box.
@@ -86,7 +218,34 @@ def plot_tie_histogram(
86
218
  if not HAS_MATPLOTLIB:
87
219
  raise ImportError("matplotlib is required for visualization")
88
220
 
89
- # Create figure if needed
221
+ fig, ax = _setup_tie_figure(ax, figsize)
222
+ time_unit, time_mult = _determine_tie_time_unit(tie_data, time_unit)
223
+ tie_scaled = tie_data * time_mult
224
+ mean_val, std_val, pp_val, rms_val = _calculate_tie_statistics(tie_scaled)
225
+
226
+ counts, bin_edges, patches = _plot_tie_histogram_data(ax, tie_scaled, bins)
227
+ _add_tie_overlays(ax, show_gaussian_fit, show_rj_dj, bin_edges, mean_val, std_val, time_unit)
228
+ _format_tie_plot(ax, show_statistics, mean_val, rms_val, std_val, pp_val, time_unit, title)
229
+
230
+ fig.tight_layout()
231
+ _save_and_show_tie_plot(fig, save_path, show)
232
+
233
+ return fig
234
+
235
+
236
+ def _setup_tie_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
237
+ """Setup figure and axes for TIE plot.
238
+
239
+ Args:
240
+ ax: Existing axes or None.
241
+ figsize: Figure size.
242
+
243
+ Returns:
244
+ Tuple of (figure, axes).
245
+
246
+ Raises:
247
+ ValueError: If axes has no figure.
248
+ """
90
249
  if ax is None:
91
250
  fig, ax = plt.subplots(figsize=figsize)
92
251
  else:
@@ -94,42 +253,23 @@ def plot_tie_histogram(
94
253
  if fig_temp is None:
95
254
  raise ValueError("Axes must have an associated figure")
96
255
  fig = cast("Figure", fig_temp)
256
+ return fig, ax
97
257
 
98
- # Select time unit
99
- if time_unit == "auto":
100
- max_tie = np.max(np.abs(tie_data))
101
- if max_tie < 1e-12:
102
- time_unit = "fs"
103
- time_mult = 1e15
104
- elif max_tie < 1e-9:
105
- time_unit = "ps"
106
- time_mult = 1e12
107
- elif max_tie < 1e-6:
108
- time_unit = "ns"
109
- time_mult = 1e9
110
- else:
111
- time_unit = "us"
112
- time_mult = 1e6
113
- else:
114
- time_mult = {
115
- "s": 1,
116
- "ms": 1e3,
117
- "us": 1e6,
118
- "ns": 1e9,
119
- "ps": 1e12,
120
- "fs": 1e15,
121
- }.get(time_unit, 1e12)
122
258
 
123
- tie_scaled = tie_data * time_mult
259
+ def _plot_tie_histogram_data(
260
+ ax: Axes, tie_scaled: NDArray[np.floating[Any]], bins: int | str
261
+ ) -> tuple[Any, NDArray[Any], Any]:
262
+ """Plot histogram data.
124
263
 
125
- # Calculate statistics
126
- mean_val = np.mean(tie_scaled)
127
- std_val = np.std(tie_scaled)
128
- pp_val = np.ptp(tie_scaled)
129
- rms_val = np.sqrt(np.mean(tie_scaled**2))
264
+ Args:
265
+ ax: Matplotlib axes.
266
+ tie_scaled: Scaled TIE data.
267
+ bins: Bin specification.
130
268
 
131
- # Plot histogram
132
- counts, bin_edges, patches = ax.hist(
269
+ Returns:
270
+ Tuple of (counts, bin_edges, patches) from matplotlib hist.
271
+ """
272
+ result: tuple[Any, NDArray[Any], Any] = ax.hist(
133
273
  tie_scaled,
134
274
  bins=bins,
135
275
  density=True,
@@ -138,75 +278,82 @@ def plot_tie_histogram(
138
278
  edgecolor="black",
139
279
  linewidth=0.5,
140
280
  )
281
+ return result
141
282
 
142
- # Gaussian fit overlay
143
- if show_gaussian_fit and HAS_SCIPY:
144
- x_fit = np.linspace(bin_edges[0], bin_edges[-1], 200)
145
- y_fit = norm.pdf(x_fit, mean_val, std_val)
146
- ax.plot(
147
- x_fit, y_fit, "r-", linewidth=2, label=f"Gaussian Fit (sigma={std_val:.2f} {time_unit})"
148
- )
149
283
 
150
- # RJ/DJ indicators
284
+ def _add_tie_overlays(
285
+ ax: Axes,
286
+ show_gaussian_fit: bool,
287
+ show_rj_dj: bool,
288
+ bin_edges: NDArray[Any],
289
+ mean_val: float,
290
+ std_val: float,
291
+ time_unit: str,
292
+ ) -> None:
293
+ """Add Gaussian fit and RJ/DJ overlays to TIE plot.
294
+
295
+ Args:
296
+ ax: Matplotlib axes.
297
+ show_gaussian_fit: Whether to show Gaussian fit.
298
+ show_rj_dj: Whether to show RJ/DJ indicators.
299
+ bin_edges: Histogram bin edges.
300
+ mean_val: Mean TIE value.
301
+ std_val: Standard deviation.
302
+ time_unit: Time unit string.
303
+ """
304
+ if show_gaussian_fit:
305
+ _add_gaussian_fit(ax, bin_edges, mean_val, std_val, time_unit)
151
306
  if show_rj_dj:
152
- # Mark ±3sigma region (RJ contribution)
153
- ax.axvline(
154
- mean_val - 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7
155
- )
156
- ax.axvline(
157
- mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7
158
- )
307
+ _add_rj_dj_indicators(ax, mean_val, std_val)
159
308
 
160
- # Shade RJ region
161
- ax.axvspan(
162
- mean_val - 3 * std_val,
163
- mean_val + 3 * std_val,
164
- alpha=0.1,
165
- color="#E74C3C",
166
- label="±3sigma (99.7% RJ)",
167
- )
168
309
 
169
- # Statistics box
310
+ def _format_tie_plot(
311
+ ax: Axes,
312
+ show_statistics: bool,
313
+ mean_val: float,
314
+ rms_val: float,
315
+ std_val: float,
316
+ pp_val: float,
317
+ time_unit: str,
318
+ title: str | None,
319
+ ) -> None:
320
+ """Format TIE plot axes and labels.
321
+
322
+ Args:
323
+ ax: Matplotlib axes.
324
+ show_statistics: Whether to show statistics box.
325
+ mean_val: Mean value.
326
+ rms_val: RMS value.
327
+ std_val: Standard deviation.
328
+ pp_val: Peak-to-peak value.
329
+ time_unit: Time unit.
330
+ title: Plot title.
331
+ """
170
332
  if show_statistics:
171
- stats_text = (
172
- f"Mean: {mean_val:.2f} {time_unit}\n"
173
- f"RMS: {rms_val:.2f} {time_unit}\n"
174
- f"Std Dev: {std_val:.2f} {time_unit}\n"
175
- f"Peak-Peak: {pp_val:.2f} {time_unit}"
176
- )
177
- ax.text(
178
- 0.98,
179
- 0.98,
180
- stats_text,
181
- transform=ax.transAxes,
182
- fontsize=9,
183
- verticalalignment="top",
184
- horizontalalignment="right",
185
- bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
186
- fontfamily="monospace",
187
- )
333
+ _add_statistics_box(ax, mean_val, rms_val, std_val, pp_val, time_unit)
188
334
 
189
- # Labels
190
335
  ax.set_xlabel(f"TIE ({time_unit})", fontsize=11)
191
336
  ax.set_ylabel("Probability Density", fontsize=11)
192
337
  ax.grid(True, alpha=0.3)
193
338
  ax.legend(loc="upper left")
194
339
 
195
- if title:
196
- ax.set_title(title, fontsize=12, fontweight="bold")
197
- else:
198
- ax.set_title("Time Interval Error Distribution", fontsize=12, fontweight="bold")
340
+ final_title = title if title else "Time Interval Error Distribution"
341
+ ax.set_title(final_title, fontsize=12, fontweight="bold")
199
342
 
200
- fig.tight_layout()
201
343
 
344
+ def _save_and_show_tie_plot(fig: Figure, save_path: str | Path | None, show: bool) -> None:
345
+ """Save and/or show TIE plot.
346
+
347
+ Args:
348
+ fig: Matplotlib figure.
349
+ save_path: Path to save file.
350
+ show: Whether to display interactively.
351
+ """
202
352
  if save_path is not None:
203
353
  fig.savefig(save_path, dpi=300, bbox_inches="tight")
204
-
205
354
  if show:
206
355
  plt.show()
207
356
 
208
- return fig
209
-
210
357
 
211
358
  def plot_bathtub_full(
212
359
  positions: NDArray[np.floating[Any]],
@@ -257,6 +404,35 @@ def plot_bathtub_full(
257
404
  if not HAS_MATPLOTLIB:
258
405
  raise ImportError("matplotlib is required for visualization")
259
406
 
407
+ fig, ax = _get_or_create_figure(ax, figsize)
408
+ ber_total = ber_total if ber_total is not None else ber_left + ber_right
409
+
410
+ # Plot BER curves
411
+ ber_total_plot = _plot_bathtub_ber_curves(ax, positions, ber_left, ber_right, ber_total)
412
+
413
+ # Optional annotations
414
+ if show_target:
415
+ _add_target_ber_line(ax, target_ber)
416
+
417
+ if show_eye_opening:
418
+ _add_eye_opening_annotation(ax, positions, ber_total_plot, target_ber, eye_opening)
419
+
420
+ # Styling
421
+ _style_bathtub_plot(ax, positions, ber_total_plot, title)
422
+
423
+ fig.tight_layout()
424
+
425
+ if save_path is not None:
426
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
427
+
428
+ if show:
429
+ plt.show()
430
+
431
+ return fig
432
+
433
+
434
+ def _get_or_create_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
435
+ """Get existing figure or create new one."""
260
436
  if ax is None:
261
437
  fig, ax = plt.subplots(figsize=figsize)
262
438
  else:
@@ -264,92 +440,104 @@ def plot_bathtub_full(
264
440
  if fig_temp is None:
265
441
  raise ValueError("Axes must have an associated figure")
266
442
  fig = cast("Figure", fig_temp)
443
+ return fig, ax
267
444
 
268
- # Compute total BER if not provided
269
- if ber_total is None:
270
- ber_total = ber_left + ber_right
271
445
 
272
- # Clip very small values for log plot
446
+ def _plot_bathtub_ber_curves(
447
+ ax: Axes,
448
+ positions: NDArray[np.floating[Any]],
449
+ ber_left: NDArray[np.floating[Any]],
450
+ ber_right: NDArray[np.floating[Any]],
451
+ ber_total: NDArray[np.floating[Any]],
452
+ ) -> NDArray[np.floating[Any]]:
453
+ """Plot BER curves and return clipped total BER."""
273
454
  ber_left_plot = np.clip(ber_left, 1e-18, 1)
274
455
  ber_right_plot = np.clip(ber_right, 1e-18, 1)
275
456
  ber_total_plot = np.clip(ber_total, 1e-18, 1)
276
457
 
277
- # Plot BER curves
278
458
  ax.semilogy(positions, ber_left_plot, "b-", linewidth=2, label="BER Left", alpha=0.8)
279
459
  ax.semilogy(positions, ber_right_plot, "r-", linewidth=2, label="BER Right", alpha=0.8)
280
460
  ax.semilogy(positions, ber_total_plot, "k-", linewidth=2.5, label="BER Total")
281
461
 
282
- # Target BER line
283
- if show_target:
284
- ax.axhline(
285
- target_ber,
286
- color="#27AE60",
287
- linestyle="--",
288
- linewidth=2,
289
- label=f"Target BER = {target_ber:.0e}",
290
- )
462
+ return ber_total_plot
291
463
 
292
- # Eye opening annotation
293
- if show_eye_opening:
294
- # Find eye opening at target BER
295
- if eye_opening is None:
296
- # Find crossover points
297
- left_cross = np.where(ber_total_plot < target_ber)[0]
298
- if len(left_cross) > 0:
299
- left_edge = positions[left_cross[0]]
300
- right_edge = positions[left_cross[-1]]
301
- eye_opening = right_edge - left_edge
302
- else:
303
- eye_opening = 0
304
-
305
- if eye_opening > 0:
306
- # Draw eye opening bracket
307
- center = 0.5
308
- left_edge = center - eye_opening / 2
309
- right_edge = center + eye_opening / 2
310
-
311
- ax.annotate(
312
- "",
313
- xy=(right_edge, target_ber),
314
- xytext=(left_edge, target_ber),
315
- arrowprops={"arrowstyle": "<->", "color": "#27AE60", "lw": 2},
316
- )
317
- ax.text(
318
- center,
319
- target_ber * 0.1,
320
- f"Eye Opening: {eye_opening:.3f} UI",
321
- ha="center",
322
- va="top",
323
- fontsize=10,
324
- fontweight="bold",
325
- color="#27AE60",
326
- )
327
-
328
- # Shading for bathtub
329
- ax.fill_between(positions, 1e-18, ber_total_plot, alpha=0.1, color="gray")
330
464
 
331
- # Labels
465
+ def _add_target_ber_line(ax: Axes, target_ber: float) -> None:
466
+ """Add target BER horizontal line."""
467
+ ax.axhline(
468
+ target_ber,
469
+ color="#27AE60",
470
+ linestyle="--",
471
+ linewidth=2,
472
+ label=f"Target BER = {target_ber:.0e}",
473
+ )
474
+
475
+
476
+ def _add_eye_opening_annotation(
477
+ ax: Axes,
478
+ positions: NDArray[np.floating[Any]],
479
+ ber_total_plot: NDArray[np.floating[Any]],
480
+ target_ber: float,
481
+ eye_opening: float | None,
482
+ ) -> None:
483
+ """Add eye opening annotation if applicable."""
484
+ if eye_opening is None:
485
+ eye_opening = _calculate_eye_opening(positions, ber_total_plot, target_ber)
486
+
487
+ if eye_opening <= 0:
488
+ return
489
+
490
+ center = 0.5
491
+ left_edge = center - eye_opening / 2
492
+ right_edge = center + eye_opening / 2
493
+
494
+ ax.annotate(
495
+ "",
496
+ xy=(right_edge, target_ber),
497
+ xytext=(left_edge, target_ber),
498
+ arrowprops={"arrowstyle": "<->", "color": "#27AE60", "lw": 2},
499
+ )
500
+ ax.text(
501
+ center,
502
+ target_ber * 0.1,
503
+ f"Eye Opening: {eye_opening:.3f} UI",
504
+ ha="center",
505
+ va="top",
506
+ fontsize=10,
507
+ fontweight="bold",
508
+ color="#27AE60",
509
+ )
510
+
511
+
512
+ def _calculate_eye_opening(
513
+ positions: NDArray[np.floating[Any]],
514
+ ber_total: NDArray[np.floating[Any]],
515
+ target_ber: float,
516
+ ) -> float:
517
+ """Calculate eye opening at target BER."""
518
+ left_cross = np.where(ber_total < target_ber)[0]
519
+ if len(left_cross) > 0:
520
+ left_edge = positions[left_cross[0]]
521
+ right_edge = positions[left_cross[-1]]
522
+ return float(right_edge - left_edge)
523
+ return 0.0
524
+
525
+
526
+ def _style_bathtub_plot(
527
+ ax: Axes,
528
+ positions: NDArray[np.floating[Any]],
529
+ ber_total_plot: NDArray[np.floating[Any]],
530
+ title: str | None,
531
+ ) -> None:
532
+ """Apply styling to bathtub plot."""
533
+ ax.fill_between(positions, 1e-18, ber_total_plot, alpha=0.1, color="gray")
332
534
  ax.set_xlabel("Sample Position (UI)", fontsize=11)
333
535
  ax.set_ylabel("Bit Error Rate", fontsize=11)
334
536
  ax.set_xlim(0, 1)
335
537
  ax.set_ylim(1e-15, 1)
336
538
  ax.grid(True, which="both", alpha=0.3)
337
539
  ax.legend(loc="upper right")
338
-
339
- if title:
340
- ax.set_title(title, fontsize=12, fontweight="bold")
341
- else:
342
- ax.set_title("Bathtub Curve", fontsize=12, fontweight="bold")
343
-
344
- fig.tight_layout()
345
-
346
- if save_path is not None:
347
- fig.savefig(save_path, dpi=300, bbox_inches="tight")
348
-
349
- if show:
350
- plt.show()
351
-
352
- return fig
540
+ ax.set_title(title or "Bathtub Curve", fontsize=12, fontweight="bold")
353
541
 
354
542
 
355
543
  def plot_ddj(
@@ -389,6 +577,13 @@ def plot_ddj(
389
577
  if not HAS_MATPLOTLIB:
390
578
  raise ImportError("matplotlib is required for visualization")
391
579
 
580
+ # Validate input lengths match
581
+ if len(patterns) != len(jitter_values):
582
+ raise ValueError(
583
+ f"Mismatched lengths: patterns has {len(patterns)} elements "
584
+ f"but jitter_values has {len(jitter_values)} elements"
585
+ )
586
+
392
587
  if ax is None:
393
588
  fig, ax = plt.subplots(figsize=figsize)
394
589
  else:
@@ -443,76 +638,71 @@ def plot_ddj(
443
638
  return fig
444
639
 
445
640
 
446
- def plot_dcd(
447
- high_times: NDArray[np.floating[Any]],
448
- low_times: NDArray[np.floating[Any]],
449
- *,
450
- ax: Axes | None = None,
451
- figsize: tuple[float, float] = (10, 6),
452
- title: str | None = None,
453
- time_unit: str = "auto",
454
- show: bool = True,
455
- save_path: str | Path | None = None,
456
- ) -> Figure:
457
- """Plot Duty Cycle Distortion (DCD) analysis.
458
-
459
- Creates overlaid histograms of high and low pulse times to visualize
460
- duty cycle distortion.
641
+ def _determine_dcd_time_unit(
642
+ high_times: NDArray[np.floating[Any]], low_times: NDArray[np.floating[Any]], time_unit: str
643
+ ) -> tuple[str, float]:
644
+ """Determine time unit and scaling for DCD plot.
461
645
 
462
646
  Args:
463
- high_times: Array of high-state durations.
464
- low_times: Array of low-state durations.
465
- ax: Matplotlib axes.
466
- figsize: Figure size.
467
- title: Plot title.
468
- time_unit: Time unit.
469
- show: Display plot.
470
- save_path: Save path.
647
+ high_times: High-state durations.
648
+ low_times: Low-state durations.
649
+ time_unit: Requested time unit or "auto".
471
650
 
472
651
  Returns:
473
- Matplotlib Figure object.
652
+ Tuple of (time_unit, time_multiplier).
474
653
  """
475
- if not HAS_MATPLOTLIB:
476
- raise ImportError("matplotlib is required for visualization")
477
-
478
- if ax is None:
479
- fig, ax = plt.subplots(figsize=figsize)
480
- else:
481
- fig_temp = ax.get_figure()
482
- if fig_temp is None:
483
- raise ValueError("Axes must have an associated figure")
484
- fig = cast("Figure", fig_temp)
485
-
486
- # Select time unit
487
654
  if time_unit == "auto":
488
655
  max_time = max(np.max(high_times), np.max(low_times))
489
656
  if max_time < 1e-9:
490
- time_unit = "ps"
491
- time_mult = 1e12
657
+ return "ps", 1e12
492
658
  elif max_time < 1e-6:
493
- time_unit = "ns"
494
- time_mult = 1e9
659
+ return "ns", 1e9
495
660
  else:
496
- time_unit = "us"
497
- time_mult = 1e6
661
+ return "us", 1e6
498
662
  else:
499
663
  time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
664
+ return time_unit, time_mult
500
665
 
501
- high_scaled = high_times * time_mult
502
- low_scaled = low_times * time_mult
503
666
 
504
- # Calculate statistics
505
- mean_high = np.mean(high_scaled)
506
- mean_low = np.mean(low_scaled)
667
+ def _compute_dcd_statistics(
668
+ high_scaled: NDArray[np.floating[Any]], low_scaled: NDArray[np.floating[Any]]
669
+ ) -> tuple[float, float, float, float]:
670
+ """Compute DCD statistics.
671
+
672
+ Args:
673
+ high_scaled: Scaled high-state durations.
674
+ low_scaled: Scaled low-state durations.
675
+
676
+ Returns:
677
+ Tuple of (mean_high, mean_low, duty_cycle, dcd).
678
+ """
679
+ mean_high = float(np.mean(high_scaled))
680
+ mean_low = float(np.mean(low_scaled))
507
681
  period = mean_high + mean_low
508
682
  duty_cycle = mean_high / period * 100
509
683
  dcd = (mean_high - mean_low) / 2
684
+ return mean_high, mean_low, duty_cycle, dcd
685
+
686
+
687
+ def _plot_dcd_histograms(
688
+ ax: Axes,
689
+ high_scaled: NDArray[np.floating[Any]],
690
+ low_scaled: NDArray[np.floating[Any]],
691
+ mean_high: float,
692
+ mean_low: float,
693
+ ) -> None:
694
+ """Plot DCD histograms with mean lines.
510
695
 
511
- # Determine common bins
696
+ Args:
697
+ ax: Matplotlib axes.
698
+ high_scaled: Scaled high-state durations.
699
+ low_scaled: Scaled low-state durations.
700
+ mean_high: Mean high value.
701
+ mean_low: Mean low value.
702
+ """
512
703
  all_times = np.concatenate([high_scaled, low_scaled])
513
704
  bins = np.linspace(np.min(all_times) * 0.95, np.max(all_times) * 1.05, 50)
514
705
 
515
- # Plot histograms
516
706
  ax.hist(
517
707
  high_scaled,
518
708
  bins=bins,
@@ -532,10 +722,55 @@ def plot_dcd(
532
722
  linewidth=0.5,
533
723
  )
534
724
 
535
- # Mean lines
536
725
  ax.axvline(mean_high, color="#E74C3C", linestyle="--", linewidth=2, alpha=0.8)
537
726
  ax.axvline(mean_low, color="#3498DB", linestyle="--", linewidth=2, alpha=0.8)
538
727
 
728
+
729
+ def plot_dcd(
730
+ high_times: NDArray[np.floating[Any]],
731
+ low_times: NDArray[np.floating[Any]],
732
+ *,
733
+ ax: Axes | None = None,
734
+ figsize: tuple[float, float] = (10, 6),
735
+ title: str | None = None,
736
+ time_unit: str = "auto",
737
+ show: bool = True,
738
+ save_path: str | Path | None = None,
739
+ ) -> Figure:
740
+ """Plot Duty Cycle Distortion (DCD) analysis.
741
+
742
+ Creates overlaid histograms of high and low pulse times to visualize
743
+ duty cycle distortion.
744
+
745
+ Args:
746
+ high_times: Array of high-state durations.
747
+ low_times: Array of low-state durations.
748
+ ax: Matplotlib axes.
749
+ figsize: Figure size.
750
+ title: Plot title.
751
+ time_unit: Time unit.
752
+ show: Display plot.
753
+ save_path: Save path.
754
+
755
+ Returns:
756
+ Matplotlib Figure object.
757
+ """
758
+ if not HAS_MATPLOTLIB:
759
+ raise ImportError("matplotlib is required for visualization")
760
+
761
+ fig, ax = _get_or_create_figure(ax, figsize)
762
+
763
+ # Scale times
764
+ time_unit, time_mult = _determine_dcd_time_unit(high_times, low_times, time_unit)
765
+ high_scaled = high_times * time_mult
766
+ low_scaled = low_times * time_mult
767
+
768
+ # Calculate statistics
769
+ mean_high, mean_low, duty_cycle, dcd = _compute_dcd_statistics(high_scaled, low_scaled)
770
+
771
+ # Plot histograms
772
+ _plot_dcd_histograms(ax, high_scaled, low_scaled, mean_high, mean_low)
773
+
539
774
  # Statistics box
540
775
  stats_text = (
541
776
  f"Mean High: {mean_high:.2f} {time_unit}\n"
@@ -614,6 +849,36 @@ def plot_jitter_trend(
614
849
  if not HAS_MATPLOTLIB:
615
850
  raise ImportError("matplotlib is required for visualization")
616
851
 
852
+ fig, ax = _setup_jitter_trend_figure(ax, figsize)
853
+ jitter_unit, jitter_mult = _determine_jitter_unit(jitter_values, jitter_unit)
854
+ jitter_scaled = jitter_values * jitter_mult
855
+
856
+ mean_val, std_val = _plot_jitter_data(ax, time_axis, jitter_scaled, jitter_unit)
857
+ _add_jitter_bounds(ax, time_axis, mean_val, std_val, jitter_unit, show_bounds)
858
+ _add_jitter_trend(ax, time_axis, jitter_scaled, jitter_unit, show_trend)
859
+ _format_jitter_trend_plot(ax, time_unit, jitter_unit, title)
860
+
861
+ fig.tight_layout()
862
+ _save_and_show_jitter_trend(fig, save_path, show)
863
+
864
+ return fig
865
+
866
+
867
+ def _setup_jitter_trend_figure(
868
+ ax: Axes | None, figsize: tuple[float, float]
869
+ ) -> tuple[Figure, Axes]:
870
+ """Setup figure and axes for jitter trend plot.
871
+
872
+ Args:
873
+ ax: Existing axes or None.
874
+ figsize: Figure size.
875
+
876
+ Returns:
877
+ Tuple of (figure, axes).
878
+
879
+ Raises:
880
+ ValueError: If axes has no figure.
881
+ """
617
882
  if ax is None:
618
883
  fig, ax = plt.subplots(figsize=figsize)
619
884
  else:
@@ -621,31 +886,56 @@ def plot_jitter_trend(
621
886
  if fig_temp is None:
622
887
  raise ValueError("Axes must have an associated figure")
623
888
  fig = cast("Figure", fig_temp)
889
+ return fig, ax
890
+
624
891
 
625
- # Auto-select jitter unit
892
+ def _determine_jitter_unit(
893
+ jitter_values: NDArray[np.floating[Any]], jitter_unit: str
894
+ ) -> tuple[str, float]:
895
+ """Determine jitter unit and multiplier.
896
+
897
+ Args:
898
+ jitter_values: Jitter value array.
899
+ jitter_unit: Requested unit or "auto".
900
+
901
+ Returns:
902
+ Tuple of (unit_str, multiplier).
903
+ """
626
904
  if jitter_unit == "auto":
627
905
  max_jitter = np.max(np.abs(jitter_values))
628
906
  if max_jitter < 1e-9:
629
- jitter_unit = "ps"
630
- jitter_mult = 1e12
907
+ return "ps", 1e12
631
908
  elif max_jitter < 1e-6:
632
- jitter_unit = "ns"
633
- jitter_mult = 1e9
909
+ return "ns", 1e9
634
910
  else:
635
- jitter_unit = "us"
636
- jitter_mult = 1e6
911
+ return "us", 1e6
637
912
  else:
638
913
  jitter_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(jitter_unit, 1e12)
914
+ return jitter_unit, jitter_mult
639
915
 
640
- jitter_scaled = jitter_values * jitter_mult
641
916
 
642
- # Plot jitter values
917
+ def _plot_jitter_data(
918
+ ax: Axes,
919
+ time_axis: NDArray[np.floating[Any]],
920
+ jitter_scaled: NDArray[np.floating[Any]],
921
+ jitter_unit: str,
922
+ ) -> tuple[float, float]:
923
+ """Plot jitter data and mean line.
924
+
925
+ Args:
926
+ ax: Matplotlib axes.
927
+ time_axis: Time array.
928
+ jitter_scaled: Scaled jitter values.
929
+ jitter_unit: Jitter unit string.
930
+
931
+ Returns:
932
+ Tuple of (mean_val, std_val).
933
+ """
643
934
  ax.plot(time_axis, jitter_scaled, "b-", linewidth=0.8, alpha=0.7, label="Jitter")
644
935
 
645
- mean_val = np.mean(jitter_scaled)
646
- std_val = np.std(jitter_scaled)
936
+ mean_val = float(np.mean(jitter_scaled))
937
+ std_val = float(np.std(jitter_scaled))
647
938
 
648
- # Mean line
649
939
  ax.axhline(
650
940
  mean_val,
651
941
  color="gray",
@@ -654,49 +944,99 @@ def plot_jitter_trend(
654
944
  label=f"Mean: {mean_val:.2f} {jitter_unit}",
655
945
  )
656
946
 
657
- # Statistical bounds
658
- if show_bounds:
659
- ax.axhline(mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1, alpha=0.7)
660
- ax.axhline(
661
- mean_val - 3 * std_val,
662
- color="#E74C3C",
663
- linestyle="--",
664
- linewidth=1,
665
- alpha=0.7,
666
- label=f"±3sigma: {3 * std_val:.2f} {jitter_unit}",
667
- )
668
- ax.fill_between(
669
- time_axis, mean_val - 3 * std_val, mean_val + 3 * std_val, alpha=0.1, color="#E74C3C"
670
- )
947
+ return mean_val, std_val
948
+
949
+
950
+ def _add_jitter_bounds(
951
+ ax: Axes,
952
+ time_axis: NDArray[np.floating[Any]],
953
+ mean_val: float,
954
+ std_val: float,
955
+ jitter_unit: str,
956
+ show_bounds: bool,
957
+ ) -> None:
958
+ """Add statistical bounds to plot.
959
+
960
+ Args:
961
+ ax: Matplotlib axes.
962
+ time_axis: Time array.
963
+ mean_val: Mean value.
964
+ std_val: Standard deviation.
965
+ jitter_unit: Unit string.
966
+ show_bounds: Whether to show bounds.
967
+ """
968
+ if not show_bounds:
969
+ return
970
+
971
+ ax.axhline(mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1, alpha=0.7)
972
+ ax.axhline(
973
+ mean_val - 3 * std_val,
974
+ color="#E74C3C",
975
+ linestyle="--",
976
+ linewidth=1,
977
+ alpha=0.7,
978
+ label=f"±3sigma: {3 * std_val:.2f} {jitter_unit}",
979
+ )
980
+ ax.fill_between(
981
+ time_axis, mean_val - 3 * std_val, mean_val + 3 * std_val, alpha=0.1, color="#E74C3C"
982
+ )
983
+
984
+
985
+ def _add_jitter_trend(
986
+ ax: Axes,
987
+ time_axis: NDArray[np.floating[Any]],
988
+ jitter_scaled: NDArray[np.floating[Any]],
989
+ jitter_unit: str,
990
+ show_trend: bool,
991
+ ) -> None:
992
+ """Add trend line to plot.
993
+
994
+ Args:
995
+ ax: Matplotlib axes.
996
+ time_axis: Time array.
997
+ jitter_scaled: Scaled jitter values.
998
+ jitter_unit: Unit string.
999
+ show_trend: Whether to show trend.
1000
+ """
1001
+ if not show_trend:
1002
+ return
1003
+
1004
+ z = np.polyfit(time_axis, jitter_scaled, 1)
1005
+ p = np.poly1d(z)
1006
+ ax.plot(
1007
+ time_axis, p(time_axis), "g-", linewidth=2, label=f"Trend: {z[0]:.2e} {jitter_unit}/unit"
1008
+ )
671
1009
 
672
- # Trend line
673
- if show_trend:
674
- z = np.polyfit(time_axis, jitter_scaled, 1)
675
- p = np.poly1d(z)
676
- ax.plot(
677
- time_axis,
678
- p(time_axis),
679
- "g-",
680
- linewidth=2,
681
- label=f"Trend: {z[0]:.2e} {jitter_unit}/unit",
682
- )
683
1010
 
1011
+ def _format_jitter_trend_plot(
1012
+ ax: Axes, time_unit: str, jitter_unit: str, title: str | None
1013
+ ) -> None:
1014
+ """Format jitter trend plot axes and labels.
1015
+
1016
+ Args:
1017
+ ax: Matplotlib axes.
1018
+ time_unit: Time unit string.
1019
+ jitter_unit: Jitter unit string.
1020
+ title: Plot title.
1021
+ """
684
1022
  ax.set_xlabel(f"Time ({time_unit})" if time_unit != "auto" else "Sample Index", fontsize=11)
685
1023
  ax.set_ylabel(f"Jitter ({jitter_unit})", fontsize=11)
686
1024
  ax.grid(True, alpha=0.3)
687
1025
  ax.legend(loc="upper right")
688
1026
 
689
- if title:
690
- ax.set_title(title, fontsize=12, fontweight="bold")
691
- else:
692
- ax.set_title("Jitter Trend Analysis", fontsize=12, fontweight="bold")
1027
+ final_title = title if title else "Jitter Trend Analysis"
1028
+ ax.set_title(final_title, fontsize=12, fontweight="bold")
693
1029
 
694
- fig.tight_layout()
695
1030
 
1031
+ def _save_and_show_jitter_trend(fig: Figure, save_path: str | Path | None, show: bool) -> None:
1032
+ """Save and/or show jitter trend plot.
1033
+
1034
+ Args:
1035
+ fig: Matplotlib figure.
1036
+ save_path: Path to save file.
1037
+ show: Whether to display interactively.
1038
+ """
696
1039
  if save_path is not None:
697
1040
  fig.savefig(save_path, dpi=300, bbox_inches="tight")
698
-
699
1041
  if show:
700
1042
  plt.show()
701
-
702
- return fig