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
@@ -0,0 +1,156 @@
1
+ """Jitter classification result types and utilities.
2
+
3
+ This module provides result types for jitter classification analysis,
4
+ allowing unified representation of jitter component estimates with
5
+ confidence metrics and classification methods.
6
+
7
+ The classification results support IEEE 2414-2020 compliant jitter
8
+ decomposition workflows by providing structured outputs for RJ, DJ,
9
+ PJ, and TJ estimates with associated confidence levels.
10
+
11
+ Example:
12
+ >>> from oscura.analyzers.jitter.classification import (
13
+ ... JitterComponentEstimate,
14
+ ... JitterClassificationResult,
15
+ ... )
16
+ >>> rj_est = JitterComponentEstimate(value=0.05, confidence=0.92, unit="UI")
17
+ >>> dj_est = JitterComponentEstimate(value=0.12, confidence=0.88, unit="UI")
18
+ >>> result = JitterClassificationResult(
19
+ ... rj_estimate=rj_est,
20
+ ... dj_estimate=dj_est,
21
+ ... tj_estimate=0.17,
22
+ ... classification_method="dual_dirac",
23
+ ... ber_target=1e-12,
24
+ ... )
25
+ >>> print(f"Total Jitter at BER={result.ber_target}: {result.tj_estimate} UI")
26
+
27
+ References:
28
+ IEEE 2414-2020: Standard for Jitter and Phase Noise
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from dataclasses import dataclass
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class JitterComponentEstimate:
38
+ """Estimate of a single jitter component with confidence metrics.
39
+
40
+ Represents the estimated magnitude of a jitter component (RJ, DJ, PJ, etc.)
41
+ along with a confidence score indicating the reliability of the estimate
42
+ and the unit of measurement.
43
+
44
+ Attributes:
45
+ value: Estimated magnitude of the jitter component
46
+ confidence: Confidence score for this estimate (0.0-1.0, where 1.0 is
47
+ highest confidence). Based on fit quality, sample size, and
48
+ statistical significance.
49
+ unit: Unit of measurement (e.g., "UI" for unit intervals, "s" for
50
+ seconds, "ps" for picoseconds)
51
+
52
+ Example:
53
+ >>> rj = JitterComponentEstimate(value=5.2e-12, confidence=0.94, unit="s")
54
+ >>> print(f"RJ: {rj.value*1e12:.2f} ps (confidence: {rj.confidence:.1%})")
55
+ RJ: 5.20 ps (confidence: 94.0%)
56
+ """
57
+
58
+ value: float
59
+ confidence: float
60
+ unit: str
61
+
62
+ def __post_init__(self) -> None:
63
+ """Validate component estimate fields."""
64
+ if self.confidence < 0 or self.confidence > 1:
65
+ raise ValueError(f"Confidence must be in [0,1], got {self.confidence}")
66
+ if not isinstance(self.unit, str) or not self.unit:
67
+ raise ValueError(f"Unit must be non-empty string, got {self.unit!r}")
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class JitterClassificationResult:
72
+ """Complete jitter classification result with all components.
73
+
74
+ Represents the outcome of a comprehensive jitter analysis, including
75
+ estimates for random jitter (RJ), deterministic jitter (DJ), and
76
+ total jitter (TJ) at a specified BER target. Includes metadata about
77
+ the classification method used.
78
+
79
+ This result type is useful for timing closure analysis, link budget
80
+ calculations, and system-level jitter characterization.
81
+
82
+ Attributes:
83
+ rj_estimate: Random jitter component estimate with confidence
84
+ dj_estimate: Deterministic jitter component estimate with confidence
85
+ tj_estimate: Total jitter at the specified BER (RJ + DJ convolution)
86
+ classification_method: Method used for classification (e.g.,
87
+ "dual_dirac", "spectral_fit", "tail_fit"). Indicates the
88
+ algorithm used for RJ/DJ separation.
89
+ ber_target: Target bit error rate for TJ calculation (e.g., 1e-12)
90
+ pj_estimate: Optional periodic jitter component estimate
91
+ ddj_estimate: Optional data-dependent jitter component estimate
92
+
93
+ Example:
94
+ >>> result = JitterClassificationResult(
95
+ ... rj_estimate=JitterComponentEstimate(0.05, 0.92, "UI"),
96
+ ... dj_estimate=JitterComponentEstimate(0.12, 0.88, "UI"),
97
+ ... tj_estimate=0.17,
98
+ ... classification_method="dual_dirac",
99
+ ... ber_target=1e-12,
100
+ ... )
101
+ >>> print(f"TJ@{result.ber_target}: {result.tj_estimate} {result.rj_estimate.unit}")
102
+ TJ@1e-12: 0.17 UI
103
+
104
+ Notes:
105
+ For IEEE 2414-2020 compliance, TJ should be calculated using:
106
+ TJ = DJ_pp + n*RJ_rms, where n = Q(BER/2) and Q is the inverse
107
+ Q-function (Gaussian tail probability).
108
+ """
109
+
110
+ rj_estimate: JitterComponentEstimate
111
+ dj_estimate: JitterComponentEstimate
112
+ tj_estimate: float
113
+ classification_method: str
114
+ ber_target: float
115
+ pj_estimate: JitterComponentEstimate | None = None
116
+ ddj_estimate: JitterComponentEstimate | None = None
117
+
118
+ def __post_init__(self) -> None:
119
+ """Validate classification result fields."""
120
+ if self.ber_target <= 0 or self.ber_target >= 1:
121
+ raise ValueError(f"BER target must be in (0,1), got {self.ber_target}")
122
+ if not isinstance(self.classification_method, str) or not self.classification_method:
123
+ raise ValueError(
124
+ f"Classification method must be non-empty string, got "
125
+ f"{self.classification_method!r}"
126
+ )
127
+ if self.tj_estimate < 0:
128
+ raise ValueError(f"TJ estimate cannot be negative, got {self.tj_estimate}")
129
+
130
+ @property
131
+ def rj_confidence(self) -> float:
132
+ """Convenience accessor for RJ confidence score."""
133
+ return self.rj_estimate.confidence
134
+
135
+ @property
136
+ def dj_confidence(self) -> float:
137
+ """Convenience accessor for DJ confidence score."""
138
+ return self.dj_estimate.confidence
139
+
140
+ @property
141
+ def overall_confidence(self) -> float:
142
+ """Overall confidence as minimum of RJ and DJ confidence scores.
143
+
144
+ Returns the more conservative (lower) of the two confidence values,
145
+ as the TJ estimate depends on both RJ and DJ being accurate.
146
+
147
+ Returns:
148
+ Minimum confidence score between RJ and DJ estimates
149
+ """
150
+ return min(self.rj_estimate.confidence, self.dj_estimate.confidence)
151
+
152
+
153
+ __all__ = [
154
+ "JitterClassificationResult",
155
+ "JitterComponentEstimate",
156
+ ]
@@ -220,7 +220,7 @@ def _extract_rj_tail_fit(tie_data: NDArray[np.float64]) -> RandomJitterResult:
220
220
  n = len(sorted_data)
221
221
 
222
222
  # Use percentiles to estimate Gaussian parameters
223
- # For a Gaussian: P16 = μ - σ, P50 = μ, P84 = μ + σ # noqa: RUF003
223
+ # For a Gaussian: P16 = μ - σ, P50 = μ, P84 = μ + σ
224
224
  p16 = np.percentile(sorted_data, 16)
225
225
  p50 = np.percentile(sorted_data, 50)
226
226
  p84 = np.percentile(sorted_data, 84)
@@ -339,6 +339,98 @@ def _extract_rj_q_scale(tie_data: NDArray[np.float64]) -> RandomJitterResult:
339
339
  )
340
340
 
341
341
 
342
+ def _prepare_dj_histogram(
343
+ valid_data: NDArray[np.float64],
344
+ ) -> tuple[NDArray[np.int_], NDArray[np.float64]]:
345
+ """Create histogram for DJ analysis.
346
+
347
+ Args:
348
+ valid_data: Valid TIE data array.
349
+
350
+ Returns:
351
+ Tuple of (histogram, bin_centers).
352
+ """
353
+ n_bins = min(100, len(valid_data) // 50)
354
+ hist, bin_edges = np.histogram(valid_data, bins=n_bins, density=False)
355
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
356
+ return hist, bin_centers
357
+
358
+
359
+ def _detect_bimodal_peaks(hist: NDArray[np.int_], bin_centers: NDArray[np.float64]) -> float | None:
360
+ """Detect bimodal peaks in histogram for DJ estimation.
361
+
362
+ Args:
363
+ hist: Histogram counts.
364
+ bin_centers: Bin center positions.
365
+
366
+ Returns:
367
+ Peak separation distance or None if not bimodal.
368
+ """
369
+ from scipy.ndimage import gaussian_filter1d
370
+ from scipy.signal import find_peaks
371
+
372
+ if len(hist) < 5:
373
+ return None
374
+
375
+ hist_smooth = gaussian_filter1d(hist.astype(float), sigma=2)
376
+ peaks, properties = find_peaks(hist_smooth, prominence=np.max(hist_smooth) * 0.1)
377
+
378
+ if len(peaks) >= 2:
379
+ prominences = properties.get("prominences", np.ones(len(peaks)))
380
+ sorted_peak_idx = np.argsort(prominences)[::-1][:2]
381
+ top_peaks = peaks[sorted_peak_idx]
382
+ top_peaks = np.sort(top_peaks)
383
+ peak_positions = bin_centers[top_peaks]
384
+ separation: float = float(abs(peak_positions[1] - peak_positions[0]))
385
+ return separation
386
+
387
+ return None
388
+
389
+
390
+ def _calculate_dj_from_quantiles(sorted_data: NDArray[np.float64], rj_rms: float) -> float:
391
+ """Calculate DJ using quantile-based method.
392
+
393
+ Args:
394
+ sorted_data: Sorted TIE data.
395
+ rj_rms: Random jitter RMS.
396
+
397
+ Returns:
398
+ DJ peak-to-peak value.
399
+ """
400
+ n = len(sorted_data)
401
+ lower_idx = max(0, int(n * 0.0001))
402
+ upper_idx = min(n - 1, int(n * 0.9999))
403
+ tj_at_ber = sorted_data[upper_idx] - sorted_data[lower_idx]
404
+
405
+ # For dual-Dirac + RJ: TJ = 2*Q*RJ + DJ at BER = 1e-4 (Q ≈ 3.72)
406
+ q_factor = 3.72
407
+ rj_contribution = 2 * q_factor * rj_rms
408
+ dj_pp: float = float(max(0.0, tj_at_ber - rj_contribution))
409
+ return dj_pp
410
+
411
+
412
+ def _determine_dj_confidence(
413
+ peak_separation_dj: float | None, dj_pp: float, rj_rms: float, n_peaks: int
414
+ ) -> float:
415
+ """Determine confidence score for DJ extraction.
416
+
417
+ Args:
418
+ peak_separation_dj: Peak separation from histogram.
419
+ dj_pp: Calculated DJ peak-to-peak.
420
+ rj_rms: Random jitter RMS.
421
+ n_peaks: Number of peaks found.
422
+
423
+ Returns:
424
+ Confidence score (0.0 to 1.0).
425
+ """
426
+ if peak_separation_dj is not None:
427
+ return 0.9 if n_peaks == 2 else 0.7
428
+ elif dj_pp > 2 * rj_rms:
429
+ return 0.5
430
+ else:
431
+ return 0.2
432
+
433
+
342
434
  def extract_dj(
343
435
  tie_data: NDArray[np.float64],
344
436
  rj_result: RandomJitterResult | None = None,
@@ -376,83 +468,27 @@ def extract_dj(
376
468
  analysis_type="deterministic_jitter_extraction",
377
469
  )
378
470
 
471
+ # Setup: prepare data and compute RJ if needed
379
472
  valid_data = tie_data[~np.isnan(tie_data)]
380
-
381
- # Get RJ if not provided - use tail fitting for better RJ isolation
382
473
  if rj_result is None:
383
474
  rj_result = extract_rj(valid_data, method="tail_fit", min_samples=min_samples)
384
-
385
475
  rj_rms = rj_result.rj_rms
386
476
 
387
- # Create histogram for analysis
388
- n_bins = min(100, len(valid_data) // 50)
389
- hist, bin_edges = np.histogram(valid_data, bins=n_bins, density=False)
390
- bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
391
-
392
- # DJ extraction using dual-Dirac model:
393
- # The dual-Dirac model assumes DJ creates two impulses at ±δ,
394
- # each convolved with Gaussian RJ.
395
- # Total distribution is: 0.5*N(μ-δ, σ) + 0.5*N(μ+δ, σ) # noqa: RUF003
396
-
477
+ # Processing: analyze histogram and detect peaks
478
+ hist, bin_centers = _prepare_dj_histogram(valid_data)
479
+ peak_separation_dj = _detect_bimodal_peaks(hist, bin_centers)
397
480
  sorted_data = np.sort(valid_data)
398
- n = len(sorted_data)
399
481
 
400
- # Try to find DJ by detecting bimodal peaks in histogram
401
- from scipy.ndimage import gaussian_filter1d
402
- from scipy.signal import find_peaks
403
-
404
- dj_pp = 0.0
405
- peak_separation_dj = None
406
-
407
- # Smooth histogram for peak detection
408
- if len(hist) >= 5:
409
- hist_smooth = gaussian_filter1d(hist.astype(float), sigma=2)
410
- peaks, properties = find_peaks(hist_smooth, prominence=np.max(hist_smooth) * 0.1)
411
-
412
- # If we find 2 clear peaks, DJ is their separation
413
- if len(peaks) >= 2:
414
- # Sort peaks by prominence and take top 2
415
- prominences = properties.get("prominences", np.ones(len(peaks)))
416
- sorted_peak_idx = np.argsort(prominences)[::-1][:2]
417
- top_peaks = peaks[sorted_peak_idx]
418
- top_peaks = np.sort(top_peaks)
419
-
420
- # Peak separation in the histogram
421
- peak_positions = bin_centers[top_peaks]
422
- peak_separation_dj = abs(peak_positions[1] - peak_positions[0])
423
-
424
- # If we found peaks, use that as DJ
482
+ # Result building: calculate DJ and confidence
425
483
  if peak_separation_dj is not None and peak_separation_dj > 2 * rj_rms:
426
484
  dj_pp = peak_separation_dj
485
+ n_peaks = 2
427
486
  else:
428
- # Fallback: use quantile-based method
429
- # At BER = 1e-4 (Q ≈ 3.72), we're well into the tails
430
- lower_idx = max(0, int(n * 0.0001))
431
- upper_idx = min(n - 1, int(n * 0.9999))
432
-
433
- tj_at_ber = sorted_data[upper_idx] - sorted_data[lower_idx]
434
-
435
- # For dual-Dirac + RJ: TJ = 2*Q*RJ + DJ
436
- # Using Q for BER = 1e-4
437
- q_factor = 3.72
438
- rj_contribution = 2 * q_factor * rj_rms
439
-
440
- # DJ is what remains after removing RJ contribution
441
- dj_pp = max(0.0, tj_at_ber - rj_contribution)
487
+ dj_pp = _calculate_dj_from_quantiles(sorted_data, rj_rms)
488
+ n_peaks = 0
442
489
 
443
- # Delta is half the DJ peak-to-peak (dual-Dirac separation)
444
490
  dj_delta = dj_pp / 2
445
-
446
- # Confidence based on whether we found clear DJ
447
- if peak_separation_dj is not None:
448
- # Found bimodal peaks
449
- confidence = 0.9 if len(peaks) == 2 else 0.7
450
- elif dj_pp > 2 * rj_rms:
451
- # Significant DJ from quantile method
452
- confidence = 0.5
453
- else:
454
- # Little or no DJ detected
455
- confidence = 0.2
491
+ confidence = _determine_dj_confidence(peak_separation_dj, dj_pp, rj_rms, n_peaks)
456
492
 
457
493
  return DeterministicJitterResult(
458
494
  dj_pp=dj_pp,
@@ -496,20 +532,40 @@ def extract_pj(
496
532
  IEEE 2414-2020 Section 6.4
497
533
  """
498
534
  valid_data = tie_data[~np.isnan(tie_data)]
499
- n = len(valid_data)
535
+ if len(valid_data) < 32:
536
+ return _empty_pj_result()
500
537
 
501
- if n < 32:
502
- return PeriodicJitterResult(
503
- components=[],
504
- pj_pp=0.0,
505
- dominant_frequency=None,
506
- dominant_amplitude=None,
507
- )
538
+ # Compute spectrum
539
+ frequencies, magnitudes = _compute_pj_spectrum(valid_data, sample_rate)
540
+
541
+ # Filter frequency range
542
+ max_frequency = max_frequency or sample_rate / 2
543
+ valid_freqs, valid_mags = _filter_frequency_range(
544
+ frequencies, magnitudes, min_frequency, max_frequency
545
+ )
546
+
547
+ if len(valid_mags) < 3:
548
+ return _empty_pj_result()
508
549
 
509
- # Remove DC offset (mean)
510
- data_centered = valid_data - np.mean(valid_data)
550
+ # Extract peaks and create result
551
+ components = _extract_pj_peaks(valid_freqs, valid_mags, n_components)
552
+ return _create_pj_result(components)
511
553
 
512
- # Apply window to reduce spectral leakage
554
+
555
+ def _empty_pj_result() -> PeriodicJitterResult:
556
+ """Create empty PJ result for insufficient data."""
557
+ return PeriodicJitterResult(
558
+ components=[], pj_pp=0.0, dominant_frequency=None, dominant_amplitude=None
559
+ )
560
+
561
+
562
+ def _compute_pj_spectrum(
563
+ data: NDArray[np.float64], sample_rate: float
564
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
565
+ """Compute FFT spectrum of TIE data."""
566
+ n = len(data)
567
+ # Remove DC and apply window
568
+ data_centered = data - np.mean(data)
513
569
  window = np.hanning(n)
514
570
  data_windowed = data_centered * window
515
571
 
@@ -518,53 +574,47 @@ def extract_pj(
518
574
  spectrum = np.fft.rfft(data_windowed, n=nfft)
519
575
  frequencies = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
520
576
  magnitudes = np.abs(spectrum) * 2 / n # Scale for amplitude
577
+ return frequencies, magnitudes
521
578
 
522
- # Set frequency range
523
- if max_frequency is None:
524
- max_frequency = sample_rate / 2
525
579
 
526
- # Find peaks in valid frequency range
527
- freq_mask = (frequencies >= min_frequency) & (frequencies <= max_frequency)
528
- valid_freqs = frequencies[freq_mask]
529
- valid_mags = magnitudes[freq_mask]
580
+ def _filter_frequency_range(
581
+ frequencies: NDArray[np.float64],
582
+ magnitudes: NDArray[np.float64],
583
+ min_freq: float,
584
+ max_freq: float,
585
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
586
+ """Filter spectrum to valid frequency range."""
587
+ freq_mask = (frequencies >= min_freq) & (frequencies <= max_freq)
588
+ return frequencies[freq_mask], magnitudes[freq_mask]
530
589
 
531
- if len(valid_mags) < 3:
532
- return PeriodicJitterResult(
533
- components=[],
534
- pj_pp=0.0,
535
- dominant_frequency=None,
536
- dominant_amplitude=None,
537
- )
538
590
 
539
- # Find peaks (local maxima)
591
+ def _extract_pj_peaks(
592
+ frequencies: NDArray[np.float64], magnitudes: NDArray[np.float64], n_components: int
593
+ ) -> list[tuple[float, float]]:
594
+ """Extract top N periodic jitter peaks from spectrum."""
540
595
  from scipy.signal import find_peaks
541
596
 
542
- # Threshold: peaks must be 3x the median magnitude
543
- threshold = 3 * np.median(valid_mags)
544
- peak_indices, _properties = find_peaks(
545
- valid_mags,
546
- height=threshold,
547
- distance=3, # Minimum separation between peaks
548
- )
597
+ threshold = 3 * np.median(magnitudes)
598
+ peak_indices, _ = find_peaks(magnitudes, height=threshold, distance=3)
599
+
600
+ if len(peak_indices) == 0:
601
+ return []
549
602
 
550
603
  # Sort by amplitude and take top n_components
551
- if len(peak_indices) > 0:
552
- peak_heights = valid_mags[peak_indices]
553
- sorted_indices = np.argsort(peak_heights)[::-1][:n_components]
554
- top_peaks = peak_indices[sorted_indices]
604
+ peak_heights = magnitudes[peak_indices]
605
+ sorted_indices = np.argsort(peak_heights)[::-1][:n_components]
606
+ top_peaks = peak_indices[sorted_indices]
607
+ return [(float(frequencies[idx]), float(magnitudes[idx])) for idx in top_peaks]
555
608
 
556
- components = [(float(valid_freqs[idx]), float(valid_mags[idx])) for idx in top_peaks]
557
609
 
558
- # Calculate total PJ as sum of component amplitudes (peak-to-peak)
559
- pj_pp = 2 * sum(amp for _, amp in components)
610
+ def _create_pj_result(components: list[tuple[float, float]]) -> PeriodicJitterResult:
611
+ """Create PJ result from extracted components."""
612
+ if not components:
613
+ return _empty_pj_result()
560
614
 
561
- dominant_frequency = components[0][0] if components else None
562
- dominant_amplitude = components[0][1] if components else None
563
- else:
564
- components = []
565
- pj_pp = 0.0
566
- dominant_frequency = None
567
- dominant_amplitude = None
615
+ pj_pp = 2 * sum(amp for _, amp in components)
616
+ dominant_frequency = components[0][0]
617
+ dominant_amplitude = components[0][1]
568
618
 
569
619
  return PeriodicJitterResult(
570
620
  components=components,
@@ -48,6 +48,75 @@ class JitterSpectrumResult:
48
48
  peaks: list[tuple[float, float]]
49
49
 
50
50
 
51
+ def _create_empty_jitter_spectrum() -> JitterSpectrumResult:
52
+ """Create empty jitter spectrum result for insufficient data.
53
+
54
+ Returns:
55
+ Empty JitterSpectrumResult.
56
+ """
57
+ return JitterSpectrumResult(
58
+ frequencies=np.array([]),
59
+ magnitude=np.array([]),
60
+ magnitude_db=np.array([]),
61
+ dominant_frequency=None,
62
+ dominant_magnitude=None,
63
+ noise_floor=0.0,
64
+ peaks=[],
65
+ )
66
+
67
+
68
+ def _preprocess_tie_data(
69
+ valid_data: NDArray[np.float64], window: str, detrend: bool
70
+ ) -> tuple[NDArray[np.float64], float, int]:
71
+ """Preprocess TIE data with detrending and windowing.
72
+
73
+ Args:
74
+ valid_data: Valid TIE data (NaNs removed).
75
+ window: Window function name.
76
+ detrend: Whether to detrend data.
77
+
78
+ Returns:
79
+ Tuple of (windowed_data, window_factor, n_samples).
80
+ """
81
+ n = len(valid_data)
82
+
83
+ if detrend:
84
+ x = np.arange(n)
85
+ slope, intercept = np.polyfit(x, valid_data, 1)
86
+ data_detrended = valid_data - (slope * x + intercept)
87
+ else:
88
+ data_detrended = valid_data - np.mean(valid_data)
89
+
90
+ win = {"hann": np.hanning(n), "hamming": np.hamming(n), "blackman": np.blackman(n)}.get(
91
+ window, np.ones(n)
92
+ )
93
+
94
+ window_factor = np.sqrt(np.mean(win**2))
95
+ return data_detrended * win, float(window_factor), n
96
+
97
+
98
+ def _compute_jitter_fft(
99
+ data_windowed: NDArray[np.float64], n: int, window_factor: float, sample_rate: float
100
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
101
+ """Compute FFT of windowed jitter data.
102
+
103
+ Args:
104
+ data_windowed: Windowed TIE data.
105
+ n: Original sample count.
106
+ window_factor: Window power factor.
107
+ sample_rate: Sample rate in Hz.
108
+
109
+ Returns:
110
+ Tuple of (frequencies, magnitude, magnitude_db).
111
+ """
112
+ nfft = int(2 ** np.ceil(np.log2(n)))
113
+ spectrum = np.fft.rfft(data_windowed, n=nfft)
114
+ frequencies = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
115
+ magnitude = np.abs(spectrum) * 2 / n / window_factor
116
+ magnitude_db = 20 * np.log10(magnitude / 1e-12 + 1e-20)
117
+ return frequencies, magnitude, magnitude_db
118
+
119
+
51
120
  def jitter_spectrum(
52
121
  tie_data: NDArray[np.float64],
53
122
  sample_rate: float,
@@ -79,76 +148,23 @@ def jitter_spectrum(
79
148
  References:
80
149
  IEEE 2414-2020 Section 6.8
81
150
  """
151
+ # Setup: validate and prepare data
82
152
  valid_data = tie_data[~np.isnan(tie_data)]
83
- n = len(valid_data)
84
-
85
- if n < 16:
86
- return JitterSpectrumResult(
87
- frequencies=np.array([]),
88
- magnitude=np.array([]),
89
- magnitude_db=np.array([]),
90
- dominant_frequency=None,
91
- dominant_magnitude=None,
92
- noise_floor=0.0,
93
- peaks=[],
94
- )
95
-
96
- # Detrend if requested
97
- if detrend:
98
- # Remove linear trend
99
- x = np.arange(n)
100
- slope, intercept = np.polyfit(x, valid_data, 1)
101
- data_detrended = valid_data - (slope * x + intercept)
102
- else:
103
- data_detrended = valid_data - np.mean(valid_data)
104
-
105
- # Apply window
106
- if window == "hann":
107
- win = np.hanning(n)
108
- elif window == "hamming":
109
- win = np.hamming(n)
110
- elif window == "blackman":
111
- win = np.blackman(n)
112
- else:
113
- win = np.ones(n)
153
+ if len(valid_data) < 16:
154
+ return _create_empty_jitter_spectrum()
114
155
 
115
- # Compensate for window power loss
116
- window_factor = np.sqrt(np.mean(win**2))
117
- data_windowed = data_detrended * win
118
-
119
- # Zero-pad to next power of 2
120
- nfft = int(2 ** np.ceil(np.log2(n)))
121
-
122
- # Compute FFT
123
- spectrum = np.fft.rfft(data_windowed, n=nfft)
124
- frequencies = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
125
-
126
- # Calculate magnitude spectrum
127
- # Scale for proper amplitude: 2/N for single-sided, compensate for window
128
- magnitude = np.abs(spectrum) * 2 / n / window_factor
129
-
130
- # Convert to dB (relative to 1 ps = 1e-12 s)
131
- reference = 1e-12 # 1 ps
132
- magnitude_db = 20 * np.log10(magnitude / reference + 1e-20)
156
+ # Processing: apply preprocessing and FFT
157
+ data_windowed, window_factor, n = _preprocess_tie_data(valid_data, window, detrend)
158
+ frequencies, magnitude, magnitude_db = _compute_jitter_fft(
159
+ data_windowed, n, window_factor, sample_rate
160
+ )
133
161
 
134
- # Estimate noise floor (median of spectrum)
162
+ # Formatting: identify peaks and dominant frequency
135
163
  noise_floor = float(np.median(magnitude))
136
-
137
- # Find peaks
138
164
  peaks = identify_periodic_components(
139
- frequencies,
140
- magnitude,
141
- n_peaks=n_peaks,
142
- min_height=noise_floor * 3,
165
+ frequencies, magnitude, n_peaks=n_peaks, min_height=noise_floor * 3
143
166
  )
144
-
145
- # Get dominant component
146
- if len(peaks) > 0:
147
- dominant_frequency = peaks[0][0]
148
- dominant_magnitude = peaks[0][1]
149
- else:
150
- dominant_frequency = None
151
- dominant_magnitude = None
167
+ dominant_frequency, dominant_magnitude = (peaks[0][0], peaks[0][1]) if peaks else (None, None)
152
168
 
153
169
  return JitterSpectrumResult(
154
170
  frequencies=frequencies,