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
@@ -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,