oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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