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
@@ -96,19 +96,47 @@ def generate_eye(
96
96
  """
97
97
  data = trace.data
98
98
  sample_rate = trace.metadata.sample_rate
99
- 1.0 / sample_rate
100
99
 
101
- # Calculate samples per UI
102
- samples_per_ui = round(unit_interval * sample_rate)
100
+ samples_per_ui = _validate_unit_interval(unit_interval, sample_rate)
101
+ total_ui_samples = samples_per_ui * n_ui
102
+ _validate_data_length(len(data), total_ui_samples)
103
+
104
+ trigger_indices = _find_trigger_points(data, trigger_level, trigger_edge)
105
+ eye_traces = _extract_eye_traces(
106
+ data, trigger_indices, samples_per_ui, total_ui_samples, max_traces
107
+ )
108
+ eye_data = np.array(eye_traces, dtype=np.float64)
109
+ time_axis = np.linspace(0, n_ui, total_ui_samples, endpoint=False)
110
+
111
+ histogram, voltage_bins, time_bins = _generate_histogram_if_requested(
112
+ eye_data, time_axis, n_ui, generate_histogram, histogram_bins
113
+ )
114
+
115
+ return EyeDiagram(
116
+ data=eye_data,
117
+ time_axis=time_axis,
118
+ unit_interval=unit_interval,
119
+ samples_per_ui=samples_per_ui,
120
+ n_traces=len(eye_traces),
121
+ sample_rate=sample_rate,
122
+ histogram=histogram,
123
+ voltage_bins=voltage_bins,
124
+ time_bins=time_bins,
125
+ )
103
126
 
127
+
128
+ def _validate_unit_interval(unit_interval: float, sample_rate: float) -> int:
129
+ """Validate unit interval and calculate samples per UI."""
130
+ samples_per_ui = round(unit_interval * sample_rate)
104
131
  if samples_per_ui < 4:
105
132
  raise AnalysisError(
106
133
  f"Unit interval too short: {samples_per_ui} samples/UI. Need at least 4 samples per UI."
107
134
  )
135
+ return samples_per_ui
108
136
 
109
- n_samples = len(data)
110
- total_ui_samples = samples_per_ui * n_ui
111
137
 
138
+ def _validate_data_length(n_samples: int, total_ui_samples: int) -> None:
139
+ """Validate that we have enough data for eye generation."""
112
140
  if n_samples < total_ui_samples * 2:
113
141
  raise InsufficientDataError(
114
142
  f"Need at least {total_ui_samples * 2} samples for eye diagram",
@@ -117,7 +145,13 @@ def generate_eye(
117
145
  analysis_type="eye_diagram_generation",
118
146
  )
119
147
 
120
- # Find trigger points
148
+
149
+ def _find_trigger_points(
150
+ data: NDArray[np.float64],
151
+ trigger_level: float,
152
+ trigger_edge: str,
153
+ ) -> NDArray[np.intp]:
154
+ """Find trigger points in the data."""
121
155
  low = np.percentile(data, 10)
122
156
  high = np.percentile(data, 90)
123
157
  threshold = low + trigger_level * (high - low)
@@ -137,9 +171,20 @@ def generate_eye(
137
171
  analysis_type="eye_diagram_generation",
138
172
  )
139
173
 
140
- # Extract eye traces
174
+ return trigger_indices
175
+
176
+
177
+ def _extract_eye_traces(
178
+ data: NDArray[np.float64],
179
+ trigger_indices: NDArray[np.intp],
180
+ samples_per_ui: int,
181
+ total_ui_samples: int,
182
+ max_traces: int | None,
183
+ ) -> list[NDArray[np.float64]]:
184
+ """Extract eye traces from data using trigger points."""
141
185
  eye_traces = []
142
- half_ui = samples_per_ui // 2 # Start half UI before trigger
186
+ half_ui = samples_per_ui // 2
187
+ n_samples = len(data)
143
188
 
144
189
  for trig_idx in trigger_indices:
145
190
  start_idx = trig_idx - half_ui
@@ -159,48 +204,35 @@ def generate_eye(
159
204
  analysis_type="eye_diagram_generation",
160
205
  )
161
206
 
162
- # Stack into 2D array
163
- eye_data = np.array(eye_traces, dtype=np.float64)
207
+ return eye_traces
164
208
 
165
- # Generate time axis in UI
166
- time_axis = np.linspace(0, n_ui, total_ui_samples, endpoint=False)
167
209
 
168
- # Optional: Generate 2D histogram
169
- histogram = None
170
- voltage_bins = None
171
- time_bins = None
172
-
173
- if generate_histogram:
174
- # Flatten for histogram
175
- all_voltages = eye_data.flatten()
176
- all_times = np.tile(time_axis, len(eye_traces))
177
-
178
- # Create histogram
179
- voltage_range = (np.min(all_voltages), np.max(all_voltages))
180
- time_range = (0, n_ui)
181
-
182
- histogram, voltage_edges, time_edges = np.histogram2d(
183
- all_voltages,
184
- all_times,
185
- bins=histogram_bins,
186
- range=[voltage_range, time_range],
187
- )
210
+ def _generate_histogram_if_requested(
211
+ eye_data: NDArray[np.float64],
212
+ time_axis: NDArray[np.float64],
213
+ n_ui: int,
214
+ generate_histogram: bool,
215
+ histogram_bins: tuple[int, int],
216
+ ) -> tuple[NDArray[np.float64] | None, NDArray[np.float64] | None, NDArray[np.float64] | None]:
217
+ """Generate 2D histogram if requested."""
218
+ if not generate_histogram:
219
+ return None, None, None
188
220
 
189
- voltage_bins = voltage_edges
190
- time_bins = time_edges
221
+ all_voltages = eye_data.flatten()
222
+ all_times = np.tile(time_axis, len(eye_data))
191
223
 
192
- return EyeDiagram(
193
- data=eye_data,
194
- time_axis=time_axis,
195
- unit_interval=unit_interval,
196
- samples_per_ui=samples_per_ui,
197
- n_traces=len(eye_traces),
198
- sample_rate=sample_rate,
199
- histogram=histogram,
200
- voltage_bins=voltage_bins,
201
- time_bins=time_bins,
224
+ voltage_range = (np.min(all_voltages), np.max(all_voltages))
225
+ time_range = (0, n_ui)
226
+
227
+ histogram, voltage_edges, time_edges = np.histogram2d(
228
+ all_voltages,
229
+ all_times,
230
+ bins=histogram_bins,
231
+ range=[voltage_range, time_range],
202
232
  )
203
233
 
234
+ return histogram, voltage_edges, time_edges
235
+
204
236
 
205
237
  def generate_eye_from_edges(
206
238
  trace: WaveformTrace,
@@ -301,6 +333,95 @@ def generate_eye_from_edges(
301
333
  )
302
334
 
303
335
 
336
+ def _calculate_trigger_threshold(data: NDArray[np.float64], trigger_fraction: float) -> float:
337
+ """Calculate trigger threshold from data amplitude.
338
+
339
+ Args:
340
+ data: Eye diagram data.
341
+ trigger_fraction: Trigger level as fraction of amplitude.
342
+
343
+ Returns:
344
+ Threshold value.
345
+ """
346
+ low = np.percentile(data, 10)
347
+ high = np.percentile(data, 90)
348
+ amplitude_range = high - low
349
+ threshold: float = float(low + trigger_fraction * amplitude_range)
350
+ return threshold
351
+
352
+
353
+ def _find_trace_crossings(data: NDArray[np.float64], threshold: float) -> list[int]:
354
+ """Find crossing indices for all traces.
355
+
356
+ Args:
357
+ data: Eye diagram trace data (n_traces x samples_per_trace).
358
+ threshold: Crossing threshold.
359
+
360
+ Returns:
361
+ List of crossing indices for traces with crossings.
362
+ """
363
+ n_traces, _samples_per_trace = data.shape
364
+ crossing_indices = []
365
+
366
+ for trace_idx in range(n_traces):
367
+ trace = data[trace_idx, :]
368
+ crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
369
+
370
+ if len(crossings) > 0:
371
+ crossing_indices.append(crossings[0])
372
+
373
+ return crossing_indices
374
+
375
+
376
+ def _align_traces_to_target(
377
+ data: NDArray[np.float64], threshold: float, target_crossing: int
378
+ ) -> NDArray[np.float64]:
379
+ """Align all traces to target crossing position.
380
+
381
+ Args:
382
+ data: Eye diagram trace data.
383
+ threshold: Crossing threshold.
384
+ target_crossing: Target crossing position.
385
+
386
+ Returns:
387
+ Aligned trace data.
388
+ """
389
+ n_traces, _samples_per_trace = data.shape
390
+ aligned_data = np.zeros_like(data)
391
+
392
+ for trace_idx in range(n_traces):
393
+ trace = data[trace_idx, :]
394
+ crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
395
+
396
+ if len(crossings) > 0:
397
+ crossing = crossings[0]
398
+ shift = target_crossing - crossing
399
+
400
+ if shift != 0:
401
+ aligned_data[trace_idx, :] = np.roll(trace, shift)
402
+ else:
403
+ aligned_data[trace_idx, :] = trace
404
+ else:
405
+ aligned_data[trace_idx, :] = trace
406
+
407
+ return aligned_data
408
+
409
+
410
+ def _apply_symmetric_centering(data: NDArray[np.float64]) -> NDArray[np.float64]:
411
+ """Apply symmetric amplitude centering if enabled.
412
+
413
+ Args:
414
+ data: Aligned trace data.
415
+
416
+ Returns:
417
+ Symmetrically centered data.
418
+ """
419
+ max_abs = np.max(np.abs(data))
420
+ if max_abs > 0:
421
+ data = data - np.mean(data)
422
+ return data
423
+
424
+
304
425
  def auto_center_eye_diagram(
305
426
  eye: EyeDiagram,
306
427
  *,
@@ -335,37 +456,12 @@ def auto_center_eye_diagram(
335
456
  if not 0 <= trigger_fraction <= 1:
336
457
  raise ValueError(f"trigger_fraction must be in [0, 1], got {trigger_fraction}")
337
458
 
459
+ # Setup: calculate threshold and find crossings
338
460
  data = eye.data
339
-
340
- # Calculate optimal trigger point using histogram-based threshold
341
- # Find median value (represents middle level)
342
- np.median(data)
343
-
344
- # Calculate amplitude range
345
- low = np.percentile(data, 10)
346
- high = np.percentile(data, 90)
347
- amplitude_range = high - low
348
-
349
- # Trigger threshold at specified fraction
350
- threshold = low + trigger_fraction * amplitude_range
351
-
352
- # Find crossing points for each trace
353
- # A crossing is where signal crosses threshold
354
- n_traces, samples_per_trace = data.shape
355
- crossing_indices = []
356
-
357
- for trace_idx in range(n_traces):
358
- trace = data[trace_idx, :]
359
-
360
- # Find zero-crossings relative to threshold
361
- crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
362
-
363
- if len(crossings) > 0:
364
- # Use first crossing in this trace
365
- crossing_indices.append(crossings[0])
461
+ threshold = _calculate_trigger_threshold(data, trigger_fraction)
462
+ crossing_indices = _find_trace_crossings(data, threshold)
366
463
 
367
464
  if len(crossing_indices) == 0:
368
- # No crossings found, return original
369
465
  import warnings
370
466
 
371
467
  warnings.warn(
@@ -375,44 +471,15 @@ def auto_center_eye_diagram(
375
471
  )
376
472
  return eye
377
473
 
378
- # Calculate median crossing position
379
- int(np.median(crossing_indices))
380
-
381
- # Align all traces to common crossing point
382
- # This requires resampling/shifting each trace
383
- aligned_data = np.zeros_like(data)
384
- target_crossing = samples_per_trace // 2 # Center of trace
474
+ # Processing: align traces to target crossing point
475
+ _n_traces, samples_per_trace = data.shape
476
+ target_crossing = samples_per_trace // 2
477
+ aligned_data = _align_traces_to_target(data, threshold, target_crossing)
385
478
 
386
- for trace_idx in range(n_traces):
387
- trace = data[trace_idx, :]
388
-
389
- # Find crossing for this trace
390
- crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
391
-
392
- if len(crossings) > 0:
393
- crossing = crossings[0]
394
- shift = target_crossing - crossing
395
-
396
- # Shift trace by interpolation
397
- if shift != 0:
398
- # Simple roll (circular shift)
399
- aligned_data[trace_idx, :] = np.roll(trace, shift)
400
- else:
401
- aligned_data[trace_idx, :] = trace
402
- else:
403
- # No crossing, keep original
404
- aligned_data[trace_idx, :] = trace
405
-
406
- # Scale amplitude to symmetric range if requested
479
+ # Result building: apply symmetric centering and create result
407
480
  if symmetric_range:
408
- max_abs = np.max(np.abs(aligned_data))
409
- if max_abs > 0:
410
- # Center on zero
411
- aligned_data = aligned_data - np.mean(aligned_data)
412
- # Scale to ±max for symmetric range
413
- # No additional scaling needed, data already centered
414
-
415
- # Create centered eye diagram
481
+ aligned_data = _apply_symmetric_centering(aligned_data)
482
+
416
483
  return EyeDiagram(
417
484
  data=aligned_data,
418
485
  time_axis=eye.time_axis,
@@ -420,7 +487,7 @@ def auto_center_eye_diagram(
420
487
  samples_per_ui=eye.samples_per_ui,
421
488
  n_traces=eye.n_traces,
422
489
  sample_rate=eye.sample_rate,
423
- histogram=None, # Invalidate histogram after centering
490
+ histogram=None,
424
491
  voltage_bins=None,
425
492
  time_bins=None,
426
493
  )
@@ -117,7 +117,7 @@ def eye_height(
117
117
  break
118
118
  else:
119
119
  # No position found with eye opening
120
- return np.nan # type: ignore[no-any-return]
120
+ return np.nan
121
121
 
122
122
  if ber is None:
123
123
  # Simple min-max eye height
@@ -198,7 +198,7 @@ def eye_width(
198
198
  time_indices.append(i)
199
199
 
200
200
  if len(separations) == 0:
201
- return np.nan # type: ignore[no-any-return]
201
+ return np.nan
202
202
 
203
203
  # Find contiguous region with good separation
204
204
  if len(time_indices) < 2:
@@ -285,7 +285,7 @@ def q_factor(eye: EyeDiagram, *, position: float = 0.5) -> float:
285
285
  break
286
286
  else:
287
287
  # No position found with eye opening
288
- return np.nan # type: ignore[no-any-return]
288
+ return np.nan
289
289
 
290
290
  mu_high = np.mean(high_voltages)
291
291
  mu_low = np.mean(low_voltages)
@@ -295,7 +295,7 @@ def q_factor(eye: EyeDiagram, *, position: float = 0.5) -> float:
295
295
  denominator = sigma_high + sigma_low
296
296
 
297
297
  if denominator <= 0:
298
- return np.inf if mu_high > mu_low else np.nan # type: ignore[no-any-return]
298
+ return np.inf if mu_high > mu_low else np.nan
299
299
 
300
300
  q = (mu_high - mu_low) / denominator
301
301
 
@@ -331,7 +331,7 @@ def crossing_percentage(eye: EyeDiagram) -> float:
331
331
  amplitude = all_high - all_low
332
332
 
333
333
  if amplitude <= 0:
334
- return np.nan # type: ignore[no-any-return]
334
+ return np.nan
335
335
 
336
336
  # Find crossing points (where traces cross the center time)
337
337
  # Look at the rising and falling edges
@@ -24,6 +24,10 @@ from oscura.analyzers.jitter.ber import (
24
24
  q_factor_from_ber,
25
25
  tj_at_ber,
26
26
  )
27
+ from oscura.analyzers.jitter.classification import (
28
+ JitterClassificationResult,
29
+ JitterComponentEstimate,
30
+ )
27
31
  from oscura.analyzers.jitter.decomposition import (
28
32
  DataDependentJitterResult,
29
33
  DeterministicJitterResult,
@@ -51,16 +55,14 @@ from oscura.analyzers.jitter.spectrum import (
51
55
  )
52
56
 
53
57
  __all__ = [
54
- # BER
55
58
  "BathtubCurveResult",
56
- # Measurements
57
59
  "CycleJitterResult",
58
60
  "DataDependentJitterResult",
59
61
  "DeterministicJitterResult",
60
62
  "DutyCycleDistortionResult",
61
- # Decomposition
63
+ "JitterClassificationResult",
64
+ "JitterComponentEstimate",
62
65
  "JitterDecomposition",
63
- # Spectrum
64
66
  "JitterSpectrumResult",
65
67
  "PeriodicJitterResult",
66
68
  "RandomJitterResult",
@@ -68,7 +68,7 @@ def q_factor_from_ber(ber: float) -> float:
68
68
  IEEE 2414-2020: Q = sqrt(2) * erfc_inv(2 * BER)
69
69
  """
70
70
  if ber <= 0 or ber >= 0.5:
71
- return np.nan # type: ignore[no-any-return]
71
+ return np.nan
72
72
 
73
73
  # BER = 0.5 * erfc(Q / sqrt(2))
74
74
  # erfc(Q / sqrt(2)) = 2 * BER
@@ -141,7 +141,7 @@ def tj_at_ber(
141
141
  q = q_factor_from_ber(ber)
142
142
 
143
143
  if np.isnan(q):
144
- return np.nan # type: ignore[no-any-return]
144
+ return np.nan
145
145
 
146
146
  # TJ = 2 * Q * RJ_rms + DJ_pp
147
147
  tj = 2 * q * rj_rms + dj_pp
@@ -182,68 +182,25 @@ def bathtub_curve(
182
182
  References:
183
183
  IEEE 2414-2020 Section 6.7
184
184
  """
185
- from oscura.analyzers.jitter.decomposition import extract_dj, extract_rj
186
185
 
187
186
  valid_data = tie_data[~np.isnan(tie_data)]
188
187
 
189
- # Normalize TIE to UI
190
- valid_data / unit_interval
191
-
192
188
  # Extract jitter components if not provided
193
189
  if rj_rms is None or dj_delta is None:
194
- try:
195
- rj_result = extract_rj(valid_data, min_samples=100)
196
- rj_rms = rj_result.rj_rms
197
- except Exception:
198
- rj_rms = np.std(valid_data)
199
-
200
- try:
201
- dj_result = extract_dj(valid_data, min_samples=100)
202
- dj_delta = dj_result.dj_delta
203
- except Exception:
204
- dj_delta = 0.0
190
+ rj_rms, dj_delta = _extract_jitter_components(valid_data)
205
191
 
206
- # Convert to UI
207
- sigma_ui = rj_rms / unit_interval
208
- delta_ui = dj_delta / unit_interval
209
-
210
- # Generate sampling positions (0 to 1 UI)
192
+ # Convert to UI and generate positions
193
+ sigma_ui, delta_ui = rj_rms / unit_interval, dj_delta / unit_interval
211
194
  positions = np.linspace(0, 1, n_points)
212
195
 
213
- # Calculate BER at each position using dual-Dirac model
214
- # Left side: probability of sampling a '1' when '0' is sent
215
- # Right side: probability of sampling a '0' when '1' is sent
216
-
217
- # For a dual-Dirac distribution centered at 0.5 UI:
218
- # Left Dirac at 0.5 - delta, Right Dirac at 0.5 + delta
219
-
220
- ber_left = np.zeros(n_points)
221
- ber_right = np.zeros(n_points)
222
-
223
- for i, pos in enumerate(positions):
224
- # Left BER: Q-function from left edge
225
- if sigma_ui > 0:
226
- # Distance from left edge to sampling point
227
- q_left = (pos - delta_ui) / sigma_ui
228
- ber_left[i] = 0.5 * special.erfc(q_left / np.sqrt(2))
229
-
230
- # Distance from right edge to sampling point
231
- q_right = (1 - pos - delta_ui) / sigma_ui
232
- ber_right[i] = 0.5 * special.erfc(q_right / np.sqrt(2))
233
- else:
234
- # No random jitter - step function
235
- ber_left[i] = 0.5 if pos <= delta_ui else 0
236
- ber_right[i] = 0.5 if pos >= (1 - delta_ui) else 0
237
-
238
- # Total BER is sum of left and right
239
- ber_total = ber_left + ber_right
196
+ # Calculate BER arrays using dual-Dirac model
197
+ ber_left, ber_right = _compute_ber_arrays(positions, sigma_ui, delta_ui)
240
198
 
241
- # Clip to valid range
242
- ber_total = np.clip(ber_total, 1e-18, 0.5)
199
+ # Combine and clip BER values
200
+ ber_total = np.clip(ber_left + ber_right, 1e-18, 0.5)
243
201
  ber_left = np.clip(ber_left, 1e-18, 0.5)
244
202
  ber_right = np.clip(ber_right, 1e-18, 0.5)
245
203
 
246
- # Calculate eye opening at target BER
247
204
  eye_opening = _calculate_eye_opening(positions, ber_total, target_ber)
248
205
 
249
206
  return BathtubCurveResult(
@@ -257,6 +214,49 @@ def bathtub_curve(
257
214
  )
258
215
 
259
216
 
217
+ def _extract_jitter_components(valid_data: NDArray[np.float64]) -> tuple[float, float]:
218
+ """Extract RJ and DJ components from TIE data."""
219
+ from oscura.analyzers.jitter.decomposition import extract_dj, extract_rj
220
+
221
+ try:
222
+ rj_result = extract_rj(valid_data, min_samples=100)
223
+ rj_rms = rj_result.rj_rms
224
+ except Exception:
225
+ rj_rms_raw = np.std(valid_data)
226
+ rj_rms = float(rj_rms_raw)
227
+
228
+ try:
229
+ dj_result = extract_dj(valid_data, min_samples=100)
230
+ dj_delta = dj_result.dj_delta
231
+ except Exception:
232
+ dj_delta = 0.0
233
+
234
+ return rj_rms, dj_delta
235
+
236
+
237
+ def _compute_ber_arrays(
238
+ positions: NDArray[np.float64], sigma_ui: float, delta_ui: float
239
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
240
+ """Compute BER arrays for left and right edges using dual-Dirac model."""
241
+ n_points = len(positions)
242
+ ber_left, ber_right = np.zeros(n_points), np.zeros(n_points)
243
+
244
+ for i, pos in enumerate(positions):
245
+ if sigma_ui > 0:
246
+ # Q-function for Gaussian random jitter
247
+ q_left = (pos - delta_ui) / sigma_ui
248
+ ber_left[i] = 0.5 * special.erfc(q_left / np.sqrt(2))
249
+
250
+ q_right = (1 - pos - delta_ui) / sigma_ui
251
+ ber_right[i] = 0.5 * special.erfc(q_right / np.sqrt(2))
252
+ else:
253
+ # No random jitter - step function
254
+ ber_left[i] = 0.5 if pos <= delta_ui else 0
255
+ ber_right[i] = 0.5 if pos >= (1 - delta_ui) else 0
256
+
257
+ return ber_left, ber_right
258
+
259
+
260
260
  def _calculate_eye_opening(
261
261
  positions: NDArray[np.float64],
262
262
  ber: NDArray[np.float64],