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
@@ -273,98 +273,122 @@ def detect_glitches(
273
273
  References:
274
274
  Application note AN-905: Understanding Glitch Detection
275
275
  """
276
- if isinstance(trace, DigitalTrace):
277
- # Already digital - use directly
278
- digital = trace.data
279
- sample_rate = trace.metadata.sample_rate
280
- threshold_used = 0.5 # Not used for amplitude calc on digital
281
- data = trace.data.astype(np.float64)
282
- else:
283
- # Analog trace - need to threshold
284
- data = trace.data
285
- sample_rate = trace.metadata.sample_rate
276
+ digital, data, sample_rate, threshold_used = _prepare_glitch_detection_data(trace, threshold)
277
+ if digital is None or len(digital) < 3:
278
+ return []
286
279
 
287
- if len(data) < 3:
288
- return []
280
+ sample_period = 1.0 / sample_rate
281
+ rising_edges, falling_edges = _find_pulse_edges(digital)
282
+ glitches = []
283
+ glitches.extend(
284
+ _detect_positive_glitches(
285
+ rising_edges, falling_edges, data, sample_period, min_width, threshold_used, trace
286
+ )
287
+ )
288
+ glitches.extend(
289
+ _detect_negative_glitches(
290
+ falling_edges, rising_edges, data, sample_period, min_width, threshold_used, trace
291
+ )
292
+ )
293
+ glitches.sort(key=lambda g: g.timestamp)
294
+ return glitches
289
295
 
290
- # Find threshold
291
- low, high = _find_logic_levels(data)
292
- threshold_used = (low + high) / 2 if threshold is None else threshold
293
296
 
294
- amplitude = high - low
295
- if amplitude <= 0:
296
- return []
297
+ def _prepare_glitch_detection_data(
298
+ trace: WaveformTrace | DigitalTrace, threshold: float | None
299
+ ) -> tuple[NDArray[np.bool_] | None, NDArray[np.float64], float, float]:
300
+ """Prepare data for glitch detection."""
301
+ if isinstance(trace, DigitalTrace):
302
+ return trace.data, trace.data.astype(np.float64), trace.metadata.sample_rate, 0.5
297
303
 
298
- # Convert to binary
299
- digital = data >= threshold_used
304
+ data = trace.data
305
+ sample_rate = trace.metadata.sample_rate
306
+ if len(data) < 3:
307
+ return None, data, sample_rate, 0.0
300
308
 
301
- if len(digital) < 3:
302
- return []
309
+ low, high = _find_logic_levels(data)
310
+ threshold_used = (low + high) / 2 if threshold is None else threshold
311
+ if high - low <= 0:
312
+ return None, data, sample_rate, threshold_used
303
313
 
304
- sample_period = 1.0 / sample_rate
314
+ return data >= threshold_used, data, sample_rate, threshold_used
305
315
 
306
- glitches: list[Glitch] = []
307
316
 
308
- # Find all pulse edges
317
+ def _find_pulse_edges(digital: NDArray[np.bool_]) -> tuple[NDArray[np.intp], NDArray[np.intp]]:
318
+ """Find rising and falling edges in digital signal."""
309
319
  transitions = np.diff(digital.astype(np.int8))
310
320
  rising_edges = np.where(transitions == 1)[0]
311
321
  falling_edges = np.where(transitions == -1)[0]
322
+ return rising_edges, falling_edges
323
+
312
324
 
313
- # Check positive pulses (rising to falling)
325
+ def _detect_positive_glitches(
326
+ rising_edges: NDArray[np.intp],
327
+ falling_edges: NDArray[np.intp],
328
+ data: NDArray[np.float64],
329
+ sample_period: float,
330
+ min_width: float,
331
+ threshold_used: float,
332
+ trace: WaveformTrace | DigitalTrace,
333
+ ) -> list[Glitch]:
334
+ """Detect positive (high) glitches."""
335
+ glitches = []
314
336
  for rising_idx in rising_edges:
315
- # Find next falling edge
316
337
  subsequent_falling = falling_edges[falling_edges > rising_idx]
317
- if len(subsequent_falling) > 0:
318
- falling_idx = subsequent_falling[0]
319
- width = (falling_idx - rising_idx) * sample_period
320
-
321
- if width < min_width:
322
- # Calculate amplitude within pulse
323
- pulse_data = data[rising_idx : falling_idx + 1]
324
- if isinstance(trace, DigitalTrace):
325
- # For digital trace, amplitude is just 1.0 (logic high)
326
- pulse_amplitude = 1.0
327
- else:
328
- pulse_amplitude = float(np.max(pulse_data) - threshold_used)
329
-
330
- glitches.append(
331
- Glitch(
332
- timestamp=rising_idx * sample_period,
333
- width=width,
334
- polarity="positive",
335
- amplitude=pulse_amplitude,
336
- )
338
+ if len(subsequent_falling) == 0:
339
+ continue
340
+ falling_idx = subsequent_falling[0]
341
+ width = (falling_idx - rising_idx) * sample_period
342
+ if width < min_width:
343
+ pulse_data = data[rising_idx : falling_idx + 1]
344
+ amplitude = (
345
+ 1.0
346
+ if isinstance(trace, DigitalTrace)
347
+ else float(np.max(pulse_data) - threshold_used)
348
+ )
349
+ glitches.append(
350
+ Glitch(
351
+ timestamp=rising_idx * sample_period,
352
+ width=width,
353
+ polarity="positive",
354
+ amplitude=amplitude,
337
355
  )
356
+ )
357
+ return glitches
358
+
338
359
 
339
- # Check negative pulses (falling to rising)
360
+ def _detect_negative_glitches(
361
+ falling_edges: NDArray[np.intp],
362
+ rising_edges: NDArray[np.intp],
363
+ data: NDArray[np.float64],
364
+ sample_period: float,
365
+ min_width: float,
366
+ threshold_used: float,
367
+ trace: WaveformTrace | DigitalTrace,
368
+ ) -> list[Glitch]:
369
+ """Detect negative (low) glitches."""
370
+ glitches = []
340
371
  for falling_idx in falling_edges:
341
- # Find next rising edge
342
372
  subsequent_rising = rising_edges[rising_edges > falling_idx]
343
- if len(subsequent_rising) > 0:
344
- rising_idx = subsequent_rising[0]
345
- width = (rising_idx - falling_idx) * sample_period
346
-
347
- if width < min_width:
348
- # Calculate amplitude within pulse
349
- pulse_data = data[falling_idx : rising_idx + 1]
350
- if isinstance(trace, DigitalTrace):
351
- # For digital trace, amplitude is just 1.0 (logic low)
352
- pulse_amplitude = 1.0
353
- else:
354
- pulse_amplitude = float(threshold_used - np.min(pulse_data))
355
-
356
- glitches.append(
357
- Glitch(
358
- timestamp=falling_idx * sample_period,
359
- width=width,
360
- polarity="negative",
361
- amplitude=pulse_amplitude,
362
- )
373
+ if len(subsequent_rising) == 0:
374
+ continue
375
+ rising_idx = subsequent_rising[0]
376
+ width = (rising_idx - falling_idx) * sample_period
377
+ if width < min_width:
378
+ pulse_data = data[falling_idx : rising_idx + 1]
379
+ amplitude = (
380
+ 1.0
381
+ if isinstance(trace, DigitalTrace)
382
+ else float(threshold_used - np.min(pulse_data))
383
+ )
384
+ glitches.append(
385
+ Glitch(
386
+ timestamp=falling_idx * sample_period,
387
+ width=width,
388
+ polarity="negative",
389
+ amplitude=amplitude,
363
390
  )
364
-
365
- # Sort by timestamp
366
- glitches.sort(key=lambda g: g.timestamp)
367
-
391
+ )
368
392
  return glitches
369
393
 
370
394
 
@@ -604,22 +628,74 @@ def mask_test(
604
628
  # Convert UI to sample indices
605
629
  samples_per_ui = bit_period * sample_rate
606
630
  time_samples = time_ui * samples_per_ui
607
-
608
- # For simplicity, test over one or two bit periods
609
- # Align signal to start of bit period
610
631
  n_ui = int(np.max(time_ui)) # 1 or 2 UI mask
632
+ n_periods = n_samples // int(samples_per_ui * n_ui)
633
+
634
+ # Detect violations
635
+ violations, hit_count = _detect_mask_violations(
636
+ data,
637
+ n_samples,
638
+ sample_rate,
639
+ n_periods,
640
+ samples_per_ui,
641
+ n_ui,
642
+ time_ui,
643
+ time_samples,
644
+ v_top,
645
+ v_bottom,
646
+ )
647
+
648
+ # Calculate margins
649
+ margin_top, margin_bottom = _calculate_mask_margins(
650
+ data, n_samples, n_periods, samples_per_ui, n_ui, time_ui, time_samples, v_top, v_bottom
651
+ )
652
+
653
+ return MaskTestResult(
654
+ pass_fail=(hit_count == 0),
655
+ hit_count=hit_count,
656
+ total_samples=n_periods * len(time_ui),
657
+ margin_top=margin_top if margin_top != np.inf else 0.0,
658
+ margin_bottom=margin_bottom if margin_bottom != np.inf else 0.0,
659
+ violations=violations,
660
+ )
661
+
662
+
663
+ def _detect_mask_violations(
664
+ data: NDArray[np.float64],
665
+ n_samples: int,
666
+ sample_rate: float,
667
+ n_periods: int,
668
+ samples_per_ui: float,
669
+ n_ui: int,
670
+ time_ui: NDArray[np.float64],
671
+ time_samples: NDArray[np.float64],
672
+ v_top: NDArray[np.float64],
673
+ v_bottom: NDArray[np.float64],
674
+ ) -> tuple[list[tuple[float, float]], int]:
675
+ """Detect mask violations across all bit periods.
611
676
 
677
+ Args:
678
+ data: Signal voltage data.
679
+ n_samples: Total number of samples.
680
+ sample_rate: Sample rate in Hz.
681
+ n_periods: Number of complete bit periods to test.
682
+ samples_per_ui: Samples per unit interval.
683
+ n_ui: Number of UI in mask template.
684
+ time_ui: Mask time coordinates in UI.
685
+ time_samples: Mask time coordinates in samples.
686
+ v_top: Upper voltage boundary.
687
+ v_bottom: Lower voltage boundary.
688
+
689
+ Returns:
690
+ Tuple of (violations list, hit count).
691
+ """
612
692
  violations: list[tuple[float, float]] = []
613
693
  hit_count = 0
614
694
 
615
- # Test all complete bit periods in the signal
616
- n_periods = n_samples // int(samples_per_ui * n_ui)
617
-
618
695
  for period_idx in range(n_periods):
619
696
  period_start_sample = int(period_idx * samples_per_ui * n_ui)
620
697
 
621
- # Extract samples for this period
622
- for i, _t_ui in enumerate(time_ui):
698
+ for i in range(len(time_ui)):
623
699
  sample_idx = period_start_sample + int(time_samples[i])
624
700
 
625
701
  if sample_idx >= n_samples:
@@ -633,38 +709,53 @@ def mask_test(
633
709
  violations.append((timestamp, voltage))
634
710
  hit_count += 1
635
711
 
636
- # Calculate margins (minimum distance to mask boundaries)
712
+ return violations, hit_count
713
+
714
+
715
+ def _calculate_mask_margins(
716
+ data: NDArray[np.float64],
717
+ n_samples: int,
718
+ n_periods: int,
719
+ samples_per_ui: float,
720
+ n_ui: int,
721
+ time_ui: NDArray[np.float64],
722
+ time_samples: NDArray[np.float64],
723
+ v_top: NDArray[np.float64],
724
+ v_bottom: NDArray[np.float64],
725
+ ) -> tuple[float, float]:
726
+ """Calculate minimum margins to mask boundaries.
727
+
728
+ Args:
729
+ data: Signal voltage data.
730
+ n_samples: Total number of samples.
731
+ n_periods: Number of complete bit periods to test.
732
+ samples_per_ui: Samples per unit interval.
733
+ n_ui: Number of UI in mask template.
734
+ time_ui: Mask time coordinates in UI.
735
+ time_samples: Mask time coordinates in samples.
736
+ v_top: Upper voltage boundary.
737
+ v_bottom: Lower voltage boundary.
738
+
739
+ Returns:
740
+ Tuple of (margin_top, margin_bottom).
741
+ """
637
742
  margin_top = float(np.inf)
638
743
  margin_bottom = float(np.inf)
639
744
 
640
745
  for period_idx in range(n_periods):
641
746
  period_start_sample = int(period_idx * samples_per_ui * n_ui)
642
747
 
643
- for i, _t_ui in enumerate(time_ui):
748
+ for i in range(len(time_ui)):
644
749
  sample_idx = period_start_sample + int(time_samples[i])
645
750
 
646
751
  if sample_idx >= n_samples:
647
752
  break
648
753
 
649
754
  voltage = data[sample_idx]
650
-
651
- # Margin to top
652
755
  margin_top = min(margin_top, v_top[i] - voltage)
653
-
654
- # Margin to bottom
655
756
  margin_bottom = min(margin_bottom, voltage - v_bottom[i])
656
757
 
657
- # Pass if no hits
658
- pass_fail = hit_count == 0
659
-
660
- return MaskTestResult(
661
- pass_fail=pass_fail,
662
- hit_count=hit_count,
663
- total_samples=n_periods * len(time_ui),
664
- margin_top=margin_top if margin_top != np.inf else 0.0,
665
- margin_bottom=margin_bottom if margin_bottom != np.inf else 0.0,
666
- violations=violations,
667
- )
758
+ return margin_top, margin_bottom
668
759
 
669
760
 
670
761
  def _get_predefined_mask(mask_name: str) -> dict[str, NDArray[np.float64]]:
@@ -750,8 +841,37 @@ def pll_clock_recovery(
750
841
  References:
751
842
  Gardner, F. M. (2005). Phaselock Techniques, 3rd ed.
752
843
  """
753
- data = trace.data.astype(np.float64) if isinstance(trace, DigitalTrace) else trace.data
844
+ data, sample_rate, n_samples = _prepare_pll_data(trace)
845
+ dt = 1.0 / sample_rate
846
+
847
+ K1, K2, K_vco = _calculate_pll_coefficients(loop_bandwidth, damping, vco_gain)
848
+ edges = _find_edges_for_phase_detection(data)
849
+ nominal_phase_inc = 2 * np.pi * nominal_frequency * dt
850
+
851
+ phase, vco_control = _run_pll_loop(
852
+ n_samples, edges, nominal_phase_inc, K1, K2, K_vco, nominal_frequency, dt
853
+ )
854
+
855
+ lock_status, lock_time = _analyze_lock_status(vco_control, n_samples, dt)
856
+ recovered_frequency, frequency_error = _calculate_recovered_frequency(
857
+ vco_control, nominal_frequency, K_vco, n_samples
858
+ )
859
+
860
+ return PLLRecoveryResult(
861
+ recovered_frequency=float(recovered_frequency),
862
+ recovered_phase=phase,
863
+ vco_control=vco_control,
864
+ lock_status=lock_status,
865
+ lock_time=lock_time,
866
+ frequency_error=float(frequency_error),
867
+ )
754
868
 
869
+
870
+ def _prepare_pll_data(
871
+ trace: WaveformTrace | DigitalTrace,
872
+ ) -> tuple[NDArray[np.float64], float, int]:
873
+ """Prepare data for PLL processing."""
874
+ data = trace.data.astype(np.float64) if isinstance(trace, DigitalTrace) else trace.data
755
875
  sample_rate = trace.metadata.sample_rate
756
876
  n_samples = len(data)
757
877
 
@@ -763,104 +883,111 @@ def pll_clock_recovery(
763
883
  analysis_type="pll_clock_recovery",
764
884
  )
765
885
 
766
- dt = 1.0 / sample_rate
886
+ return data, sample_rate, n_samples
767
887
 
768
- # PLL parameters
769
- omega_n = 2 * np.pi * loop_bandwidth # Natural frequency
770
- K_vco = 2 * np.pi * vco_gain # VCO gain in rad/s/V
771
888
 
772
- # Loop filter coefficients (2nd order)
773
- # Transfer function: F(s) = K1 + K2/s
889
+ def _calculate_pll_coefficients(
890
+ loop_bandwidth: float, damping: float, vco_gain: float
891
+ ) -> tuple[float, float, float]:
892
+ """Calculate PLL loop filter coefficients."""
893
+ omega_n = 2 * np.pi * loop_bandwidth
894
+ K_vco = 2 * np.pi * vco_gain
774
895
  K1 = (2 * damping * omega_n) / K_vco
775
896
  K2 = (omega_n**2) / K_vco
897
+ return K1, K2, K_vco
898
+
899
+
900
+ def _find_edges_for_phase_detection(data: NDArray[np.float64]) -> NDArray[np.intp]:
901
+ """Find edges in data for phase detection."""
902
+ threshold = (np.max(data) + np.min(data)) / 2
903
+ return np.where(np.abs(np.diff(np.sign(data - threshold))) > 0)[0]
904
+
776
905
 
777
- # Initialize PLL state
906
+ def _run_pll_loop(
907
+ n_samples: int,
908
+ edges: NDArray[np.intp],
909
+ nominal_phase_inc: float,
910
+ K1: float,
911
+ K2: float,
912
+ K_vco: float,
913
+ nominal_frequency: float,
914
+ dt: float,
915
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
916
+ """Run PLL loop simulation."""
778
917
  phase = np.zeros(n_samples)
779
918
  vco_control = np.zeros(n_samples)
780
919
  integrator = 0.0
781
920
  theta = 0.0
782
-
783
- # Nominal phase increment per sample
784
- nominal_phase_inc = 2 * np.pi * nominal_frequency * dt
785
-
786
- # Find edges for phase detection (simplified)
787
- threshold = (np.max(data) + np.min(data)) / 2
788
- edges = np.where(np.abs(np.diff(np.sign(data - threshold))) > 0)[0]
789
-
790
921
  edge_idx = 0
791
922
 
792
- # Run PLL loop
793
923
  for i in range(n_samples):
794
- # Phase detector: compare VCO phase to input transitions
924
+ phase_error = _compute_phase_error(i, edge_idx, edges, nominal_phase_inc, theta)
795
925
  if edge_idx < len(edges) and i == edges[edge_idx]:
796
- # Edge detected - compute phase error
797
- # Phase error = expected phase - actual VCO phase
798
- expected_phase = (edges[edge_idx] * nominal_phase_inc) % (2 * np.pi)
799
- phase_error = expected_phase - (theta % (2 * np.pi))
800
-
801
- # Wrap to [-pi, pi]
802
- phase_error = (phase_error + np.pi) % (2 * np.pi) - np.pi
803
-
804
926
  edge_idx += 1
805
- else:
806
- phase_error = 0.0
807
927
 
808
- # Loop filter (proportional + integral)
809
928
  integrator += K2 * phase_error * dt
810
929
  vco_input = K1 * phase_error + integrator
811
930
 
812
- # VCO: frequency = nominal + K_vco * control voltage
813
931
  vco_freq = nominal_frequency + K_vco * vco_input / (2 * np.pi)
814
932
  phase_increment = 2 * np.pi * vco_freq * dt
815
-
816
- # Update phase
817
933
  theta += phase_increment
818
934
 
819
- # Store results
820
935
  phase[i] = theta
821
936
  vco_control[i] = vco_input
822
937
 
823
- # Analyze lock status
824
- # Consider locked if VCO control voltage is stable in last 20%
825
- lock_threshold = 0.1 # 10% variation
938
+ return phase, vco_control
939
+
940
+
941
+ def _compute_phase_error(
942
+ i: int, edge_idx: int, edges: NDArray[np.intp], nominal_phase_inc: float, theta: float
943
+ ) -> float:
944
+ """Compute phase error at sample i."""
945
+ if edge_idx < len(edges) and i == edges[edge_idx]:
946
+ expected_phase = (edges[edge_idx] * nominal_phase_inc) % (2 * np.pi)
947
+ phase_error = expected_phase - (theta % (2 * np.pi))
948
+ wrapped_error: float = float((phase_error + np.pi) % (2 * np.pi) - np.pi)
949
+ return wrapped_error
950
+ return 0.0
951
+
952
+
953
+ def _analyze_lock_status(
954
+ vco_control: NDArray[np.float64], n_samples: int, dt: float
955
+ ) -> tuple[bool, float | None]:
956
+ """Analyze PLL lock status and find lock time."""
957
+ lock_threshold = 0.1
826
958
  last_20_percent = vco_control[int(0.8 * n_samples) :]
827
959
 
828
- if len(last_20_percent) > 0:
829
- vco_std = np.std(last_20_percent)
830
- vco_mean = np.abs(np.mean(last_20_percent))
831
- lock_status = vco_std < lock_threshold * max(vco_mean, 1.0)
832
-
833
- # Find lock time (when variation drops below threshold)
834
- if lock_status:
835
- # Search for first point where subsequent variance is low
836
- window = int(0.1 * n_samples) # 10% window
837
- for i in range(window, n_samples - window):
838
- window_std = np.std(vco_control[i : i + window])
839
- if window_std < lock_threshold:
840
- lock_time = i * dt
841
- break
842
- else:
843
- lock_time = None
844
- else:
845
- lock_time = None
846
- else:
847
- lock_status = False
848
- lock_time = None
960
+ if len(last_20_percent) == 0:
961
+ return False, None
962
+
963
+ vco_std = np.std(last_20_percent)
964
+ vco_mean = np.abs(np.mean(last_20_percent))
965
+ lock_status = vco_std < lock_threshold * max(vco_mean, 1.0)
966
+
967
+ lock_time = _find_lock_time(vco_control, n_samples, dt, lock_threshold) if lock_status else None
968
+ return lock_status, lock_time
969
+
849
970
 
850
- # Recovered frequency from final VCO state
971
+ def _find_lock_time(
972
+ vco_control: NDArray[np.float64], n_samples: int, dt: float, lock_threshold: float
973
+ ) -> float | None:
974
+ """Find time when PLL achieved lock."""
975
+ window = int(0.1 * n_samples)
976
+ for i in range(window, n_samples - window):
977
+ window_std = np.std(vco_control[i : i + window])
978
+ if window_std < lock_threshold:
979
+ return i * dt
980
+ return None
981
+
982
+
983
+ def _calculate_recovered_frequency(
984
+ vco_control: NDArray[np.float64], nominal_frequency: float, K_vco: float, n_samples: int
985
+ ) -> tuple[float, float]:
986
+ """Calculate recovered frequency and frequency error."""
851
987
  final_vco = np.mean(vco_control[-int(0.1 * n_samples) :])
852
988
  recovered_frequency = nominal_frequency + K_vco * final_vco / (2 * np.pi)
853
-
854
989
  frequency_error = recovered_frequency - nominal_frequency
855
-
856
- return PLLRecoveryResult(
857
- recovered_frequency=float(recovered_frequency),
858
- recovered_phase=phase,
859
- vco_control=vco_control,
860
- lock_status=lock_status,
861
- lock_time=lock_time,
862
- frequency_error=float(frequency_error),
863
- )
990
+ return recovered_frequency, frequency_error
864
991
 
865
992
 
866
993
  __all__ = [