oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@ mixed logic families and multi-voltage domains.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.exploratory.legacy import detect_logic_families_multi_channel
8
+ >>> from oscura.jupyter.exploratory.legacy import detect_logic_families_multi_channel
9
9
  >>> families = detect_logic_families_multi_channel(channels)
10
10
  >>> for ch, result in families.items():
11
11
  ... print(f"Channel {ch}: {result['family']} (confidence={result['confidence']:.2f})")
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
24
24
  from oscura.core.types import WaveformTrace
25
25
 
26
26
  # Logic family specifications per IEEE/JEDEC standards
27
- LOGIC_FAMILY_SPECS = {
27
+ LOGIC_FAMILY_SPECS: dict[str, dict[str, float | None]] = {
28
28
  "TTL": {
29
29
  "vil_max": 0.8,
30
30
  "vih_min": 2.0,
@@ -152,90 +152,138 @@ def detect_logic_families_multi_channel(
152
152
  confidence_thresholds = {"high": 0.9, "medium": 0.7}
153
153
 
154
154
  # Convert list to dict if needed
155
- if isinstance(channels, list):
156
- channels = dict(enumerate(channels))
155
+ channel_dict = dict(enumerate(channels)) if isinstance(channels, list) else channels
157
156
 
158
157
  results = {}
158
+ for ch_id, trace in channel_dict.items():
159
+ voltage_levels = _extract_voltage_levels(trace.data)
160
+ candidates = _score_all_logic_families(voltage_levels, voltage_tolerance)
161
+ result = _build_logic_family_result(
162
+ voltage_levels, candidates, min_edges_for_detection, warn_on_degradation
163
+ )
164
+ results[ch_id] = result
159
165
 
160
- for ch_id, trace in channels.items():
161
- data = trace.data
166
+ return results
162
167
 
163
- # Extract voltage percentiles
164
- p10 = np.percentile(data, 10)
165
- np.percentile(data, 50)
166
- p90 = np.percentile(data, 90)
167
168
 
168
- # Estimate low and high levels
169
- v_low = p10
170
- v_high = p90
171
- v_high - v_low
169
+ def _extract_voltage_levels(data: NDArray[np.float64]) -> dict[str, float]:
170
+ """Extract voltage levels from channel data.
172
171
 
173
- # Count edges for confidence
174
- threshold = (v_low + v_high) / 2
175
- edges = np.sum(np.abs(np.diff(data > threshold)))
172
+ Args:
173
+ data: Channel voltage data.
176
174
 
177
- # Score each logic family
178
- candidates = []
175
+ Returns:
176
+ Dict with v_low, v_high, threshold, edges.
177
+ """
178
+ v_low = float(np.percentile(data, 10))
179
+ v_high = float(np.percentile(data, 90))
180
+ threshold = (v_low + v_high) / 2
181
+ edges = int(np.sum(np.abs(np.diff(data > threshold))))
179
182
 
180
- for family_name, specs in LOGIC_FAMILY_SPECS.items():
181
- score = _score_logic_family(v_low, v_high, specs, voltage_tolerance) # type: ignore[arg-type]
182
- if score > 0:
183
- candidates.append((family_name, score))
183
+ return {"v_low": v_low, "v_high": v_high, "threshold": threshold, "edges": edges}
184
184
 
185
- # Sort by score descending
186
- candidates.sort(key=lambda x: x[1], reverse=True)
187
185
 
188
- if not candidates:
189
- # No match found
190
- result = LogicFamilyResult(
191
- family="UNKNOWN",
192
- confidence=0.0,
193
- v_low=v_low,
194
- v_high=v_high,
195
- alternatives=[],
196
- degradation_warning="No matching logic family found",
197
- )
198
- else:
199
- best_family, best_score = candidates[0]
200
- confidence = min(1.0, best_score)
201
-
202
- # Reduce confidence if insufficient edges
203
- if edges < min_edges_for_detection:
204
- confidence *= 0.5
205
-
206
- # Check for ambiguity (multiple families close in score)
207
- alternatives = [
208
- (name, score) for name, score in candidates[1:4] if best_score - score < 0.2
209
- ]
210
-
211
- # Check for degradation
212
- degradation_warning = None
213
- deviation_pct = 0.0
214
-
215
- if warn_on_degradation:
216
- specs = LOGIC_FAMILY_SPECS[best_family]
217
- if specs["voh_min"] is not None: # type: ignore[index]
218
- expected_voh = specs["voh_min"] # type: ignore[index]
219
- if v_high < expected_voh:
220
- deviation_pct = 100 * (expected_voh - v_high) / expected_voh
221
- if deviation_pct > 10:
222
- degradation_warning = (
223
- f"V_high below spec (expected >= {expected_voh:.3f}V)"
224
- )
225
-
226
- result = LogicFamilyResult(
227
- family=best_family,
228
- confidence=confidence,
229
- v_low=v_low,
230
- v_high=v_high,
231
- alternatives=alternatives,
232
- degradation_warning=degradation_warning,
233
- deviation_pct=deviation_pct,
234
- )
186
+ def _score_all_logic_families(
187
+ voltage_levels: dict[str, float], tolerance: float
188
+ ) -> list[tuple[str, float]]:
189
+ """Score all logic families against measured voltage levels.
235
190
 
236
- results[ch_id] = result
191
+ Args:
192
+ voltage_levels: Dict with v_low and v_high.
193
+ tolerance: Voltage tolerance for matching.
237
194
 
238
- return results
195
+ Returns:
196
+ List of (family_name, score) tuples sorted by score.
197
+ """
198
+ candidates = []
199
+ for family_name, specs in LOGIC_FAMILY_SPECS.items():
200
+ score = _score_logic_family(
201
+ voltage_levels["v_low"], voltage_levels["v_high"], specs, tolerance
202
+ )
203
+ if score > 0:
204
+ candidates.append((family_name, score))
205
+
206
+ candidates.sort(key=lambda x: x[1], reverse=True)
207
+ return candidates
208
+
209
+
210
+ def _build_logic_family_result(
211
+ voltage_levels: dict[str, float],
212
+ candidates: list[tuple[str, float]],
213
+ min_edges: int,
214
+ warn_on_degradation: bool,
215
+ ) -> LogicFamilyResult:
216
+ """Build LogicFamilyResult from voltage data and candidates.
217
+
218
+ Args:
219
+ voltage_levels: Dict with v_low, v_high, edges.
220
+ candidates: Sorted list of (family, score) tuples.
221
+ min_edges: Minimum edges for reliable detection.
222
+ warn_on_degradation: Check for signal degradation.
223
+
224
+ Returns:
225
+ LogicFamilyResult with classification.
226
+ """
227
+ v_low, v_high, edges = (
228
+ voltage_levels["v_low"],
229
+ voltage_levels["v_high"],
230
+ voltage_levels["edges"],
231
+ )
232
+
233
+ if not candidates:
234
+ return LogicFamilyResult(
235
+ family="UNKNOWN",
236
+ confidence=0.0,
237
+ v_low=v_low,
238
+ v_high=v_high,
239
+ alternatives=[],
240
+ degradation_warning="No matching logic family found",
241
+ )
242
+
243
+ best_family, best_score = candidates[0]
244
+ confidence = min(1.0, best_score)
245
+ if edges < min_edges:
246
+ confidence *= 0.5
247
+
248
+ alternatives = [(n, s) for n, s in candidates[1:4] if best_score - s < 0.2]
249
+ degradation_warning, deviation_pct = _check_degradation(
250
+ best_family, v_high, warn_on_degradation
251
+ )
252
+
253
+ return LogicFamilyResult(
254
+ family=best_family,
255
+ confidence=confidence,
256
+ v_low=v_low,
257
+ v_high=v_high,
258
+ alternatives=alternatives,
259
+ degradation_warning=degradation_warning,
260
+ deviation_pct=deviation_pct,
261
+ )
262
+
263
+
264
+ def _check_degradation(family: str, v_high: float, check: bool) -> tuple[str | None, float]:
265
+ """Check for signal degradation against spec.
266
+
267
+ Args:
268
+ family: Logic family name.
269
+ v_high: Measured high voltage.
270
+ check: Whether to perform check.
271
+
272
+ Returns:
273
+ Tuple of (warning_message, deviation_percent).
274
+ """
275
+ if not check:
276
+ return None, 0.0
277
+
278
+ specs = LOGIC_FAMILY_SPECS[family]
279
+ expected_voh = specs["voh_min"]
280
+
281
+ if expected_voh is not None and v_high < expected_voh:
282
+ deviation_pct = 100 * (expected_voh - v_high) / expected_voh
283
+ if deviation_pct > 10:
284
+ return f"V_high below spec (expected >= {expected_voh:.3f}V)", deviation_pct
285
+
286
+ return None, 0.0
239
287
 
240
288
 
241
289
  def _score_logic_family(
@@ -309,6 +357,104 @@ class CrossCorrelationResult:
309
357
  normalized_signal2: NDArray[np.float64] | None = None
310
358
 
311
359
 
360
+ def _prepare_signals_for_correlation(
361
+ data1: NDArray[np.float64], data2: NDArray[np.float64]
362
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], int]:
363
+ """Prepare signals for correlation by normalizing and aligning.
364
+
365
+ Args:
366
+ data1: First signal data.
367
+ data2: Second signal data.
368
+
369
+ Returns:
370
+ Tuple of (norm1, norm2_corrected, min_len).
371
+ """
372
+ norm1 = _normalize_to_logic_levels(data1)
373
+ norm2 = _normalize_to_logic_levels(data2)
374
+ dc_offset = np.mean(norm1) - np.mean(norm2)
375
+ norm2_corrected = norm2 + dc_offset
376
+ min_len = min(len(norm1), len(norm2_corrected))
377
+ return norm1[:min_len], norm2_corrected[:min_len], min_len
378
+
379
+
380
+ def _compute_correlation_and_lag(
381
+ norm1: NDArray[np.float64], norm2: NDArray[np.float64]
382
+ ) -> tuple[float, int]:
383
+ """Compute correlation coefficient and lag.
384
+
385
+ Args:
386
+ norm1: Normalized first signal.
387
+ norm2: Normalized second signal.
388
+
389
+ Returns:
390
+ Tuple of (correlation, lag_samples).
391
+ """
392
+ correlation = np.corrcoef(norm1, norm2)[0, 1]
393
+ xcorr = np.correlate(norm1 - np.mean(norm1), norm2 - np.mean(norm2), mode="full")
394
+ lag_samples = xcorr.argmax() - (len(norm1) - 1)
395
+ return float(correlation), int(lag_samples)
396
+
397
+
398
+ def _compute_reference_offset(
399
+ data1: NDArray[np.float64], data2: NDArray[np.float64], correlation: float
400
+ ) -> tuple[float, float]:
401
+ """Compute reference voltage offset and confidence.
402
+
403
+ Args:
404
+ data1: First signal data.
405
+ data2: Second signal data.
406
+ correlation: Correlation coefficient.
407
+
408
+ Returns:
409
+ Tuple of (ref_offset_mv, confidence).
410
+ """
411
+ v1_min: float = float(np.min(data1))
412
+ v2_min: float = float(np.min(data2))
413
+ ref_offset_mv: float = (v2_min - v1_min) * 1000
414
+ confidence: float = abs(correlation) * (1 - min(abs(ref_offset_mv) / 1000, 1.0))
415
+ return float(ref_offset_mv), float(confidence)
416
+
417
+
418
+ def _detect_reference_drift(
419
+ data1: NDArray[np.float64],
420
+ data2: NDArray[np.float64],
421
+ sample_rate: float,
422
+ drift_window_ms: float,
423
+ min_len: int,
424
+ ) -> tuple[bool, float | None]:
425
+ """Detect time-varying reference drift.
426
+
427
+ Args:
428
+ data1: First signal data.
429
+ data2: Second signal data.
430
+ sample_rate: Sample rate in Hz.
431
+ drift_window_ms: Window size in ms.
432
+ min_len: Minimum length of signals.
433
+
434
+ Returns:
435
+ Tuple of (drift_detected, drift_rate).
436
+ """
437
+ window_samples = int(drift_window_ms * 1e-3 * sample_rate)
438
+ n_windows = min_len // window_samples
439
+
440
+ if n_windows < 2:
441
+ return False, None
442
+
443
+ offsets = [
444
+ np.mean(data1[i * window_samples : (i + 1) * window_samples])
445
+ - np.mean(data2[i * window_samples : (i + 1) * window_samples])
446
+ for i in range(n_windows)
447
+ ]
448
+
449
+ offset_change = abs(offsets[-1] - offsets[0])
450
+ drift_rate_val = offset_change / (n_windows * drift_window_ms)
451
+
452
+ if drift_rate_val > 0.1:
453
+ return True, float(drift_rate_val)
454
+
455
+ return False, None
456
+
457
+
312
458
  def cross_correlate_multi_reference(
313
459
  signal1: WaveformTrace,
314
460
  signal2: WaveformTrace,
@@ -341,82 +487,33 @@ def cross_correlate_multi_reference(
341
487
  References:
342
488
  LEGACY-002: Multi-Reference Voltage Signal Correlation
343
489
  """
344
- data1 = signal1.data
345
- data2 = signal2.data
490
+ # Setup: normalize and align signals
491
+ norm1, norm2_corrected, min_len = _prepare_signals_for_correlation(signal1.data, signal2.data)
346
492
 
347
- # Normalize each signal to [0, 1]
348
- norm1 = _normalize_to_logic_levels(data1)
349
- norm2 = _normalize_to_logic_levels(data2)
350
-
351
- # Estimate DC offset between normalized signals
352
- dc_offset = np.mean(norm1) - np.mean(norm2)
493
+ # Processing: compute correlation
494
+ correlation, lag_samples = _compute_correlation_and_lag(norm1, norm2_corrected)
495
+ ref_offset_mv, confidence = _compute_reference_offset(signal1.data, signal2.data, correlation)
353
496
 
354
- # Apply offset correction
355
- norm2_corrected = norm2 + dc_offset
356
-
357
- # Compute cross-correlation
358
- min_len = min(len(norm1), len(norm2_corrected))
359
- norm1 = norm1[:min_len]
360
- norm2_corrected = norm2_corrected[:min_len]
361
-
362
- correlation = np.corrcoef(norm1, norm2_corrected)[0, 1]
363
-
364
- # Find lag using cross-correlation
365
- xcorr = np.correlate(
366
- norm1 - np.mean(norm1), norm2_corrected - np.mean(norm2_corrected), mode="full"
497
+ # Formatting: detect drift if requested
498
+ drift_result = (
499
+ _detect_reference_drift(
500
+ signal1.data, signal2.data, signal1.metadata.sample_rate, drift_window_ms, min_len
501
+ )
502
+ if detect_drift
503
+ else (False, None)
367
504
  )
368
- lag_samples = xcorr.argmax() - (len(norm1) - 1)
369
-
370
- # Convert lag to nanoseconds
371
- sample_rate = signal1.metadata.sample_rate
372
- lag_ns = lag_samples / sample_rate * 1e9
373
-
374
- # Estimate reference voltage offset
375
- # Reference offset is how much signal2's ground differs from signal1's ground
376
- v1_min = np.min(data1)
377
- v2_min = np.min(data2)
378
-
379
- # Reference offset is difference in ground levels (signal2 relative to signal1)
380
- ref_offset_mv = (v2_min - v1_min) * 1000
381
505
 
382
- # Confidence calculation
383
- offset_uncertainty_mv = abs(ref_offset_mv) * 0.1 # 10% uncertainty
384
- confidence = abs(correlation) * (1 - min(abs(ref_offset_mv) / 1000, 1.0))
385
-
386
- # Drift detection
387
- drift_detected = False
388
- drift_rate = None
389
-
390
- if detect_drift:
391
- # Calculate offset in windows
392
- window_samples = int(drift_window_ms * 1e-3 * sample_rate)
393
- n_windows = min_len // window_samples
394
-
395
- if n_windows >= 2:
396
- offsets = []
397
- for i in range(n_windows):
398
- start = i * window_samples
399
- end = start + window_samples
400
- win_offset = np.mean(data1[start:end]) - np.mean(data2[start:end])
401
- offsets.append(win_offset)
402
-
403
- # Check for drift
404
- offset_change = abs(offsets[-1] - offsets[0])
405
- drift_rate_val = offset_change / (n_windows * drift_window_ms)
406
-
407
- if drift_rate_val > 0.1: # V/ms threshold
408
- drift_detected = True
409
- drift_rate = drift_rate_val
506
+ lag_ns = lag_samples / signal1.metadata.sample_rate * 1e9
410
507
 
411
508
  return CrossCorrelationResult(
412
509
  correlation=float(correlation),
413
510
  confidence=float(confidence),
414
511
  ref_offset_mv=float(ref_offset_mv),
415
- offset_uncertainty_mv=float(offset_uncertainty_mv),
512
+ offset_uncertainty_mv=float(abs(ref_offset_mv) * 0.1),
416
513
  lag_samples=int(lag_samples),
417
514
  lag_ns=float(lag_ns),
418
- drift_detected=drift_detected,
419
- drift_rate=drift_rate,
515
+ drift_detected=drift_result[0],
516
+ drift_rate=drift_result[1],
420
517
  normalized_signal1=norm1,
421
518
  normalized_signal2=norm2_corrected,
422
519
  )
@@ -511,36 +608,95 @@ def assess_signal_quality(
511
608
  if logic_family not in LOGIC_FAMILY_SPECS:
512
609
  logic_family = "TTL" # Default fallback
513
610
 
514
- specs = LOGIC_FAMILY_SPECS[logic_family]
611
+ specs_dict: dict[str, float | None] = dict(LOGIC_FAMILY_SPECS[logic_family])
515
612
  data = signal.data
516
613
  sample_rate = signal.metadata.sample_rate
517
- n_samples = len(data)
518
-
519
- # Threshold for high/low classification
520
- threshold = (specs["vil_max"] + specs["vih_min"]) / 2 # type: ignore[index]
521
614
 
522
- # Classify samples
615
+ # Classify samples and check violations
616
+ vil_max = specs_dict["vil_max"]
617
+ vih_min = specs_dict["vih_min"]
618
+ assert vil_max is not None and vih_min is not None
619
+ threshold = (vil_max + vih_min) / 2
523
620
  is_high = data > threshold
524
- is_low = ~is_high
621
+ violations, voh_violations, vol_violations = _check_voltage_violations(
622
+ data, is_high, specs_dict, sample_rate
623
+ )
525
624
 
526
- # Count violations
625
+ # Calculate margins and status
527
626
  high_samples = data[is_high]
528
- low_samples = data[is_low]
627
+ low_samples = data[~is_high]
628
+ min_margin_mv = _calculate_voltage_margins(high_samples, low_samples, specs_dict)
629
+ status = _determine_quality_status(min_margin_mv)
630
+
631
+ # Calculate violation rates
632
+ n_high = len(high_samples)
633
+ n_low = len(low_samples)
634
+ voh_rate = voh_violations / n_high if n_high > 0 else 0.0
635
+ vol_rate = vol_violations / n_low if n_low > 0 else 0.0
636
+
637
+ # Aging analysis
638
+ aging_result = None
639
+ if check_aging and len(data) > 1000:
640
+ aging_result = _analyze_aging(
641
+ data,
642
+ high_samples,
643
+ specs_dict,
644
+ sample_rate,
645
+ time_window_s,
646
+ voh_violations,
647
+ vol_violations,
648
+ )
649
+
650
+ return SignalQualityResult(
651
+ status=status,
652
+ violation_count=voh_violations + vol_violations,
653
+ total_samples=len(data),
654
+ min_margin_mv=min_margin_mv,
655
+ violations=violations,
656
+ voh_violations=voh_violations,
657
+ vol_violations=vol_violations,
658
+ voh_rate=voh_rate,
659
+ vol_rate=vol_rate,
660
+ failure_diagnosis=aging_result.get("diagnosis") if aging_result else None,
661
+ time_to_failure_s=aging_result.get("time_to_failure") if aging_result else None,
662
+ drift_rate_mv_per_s=aging_result.get("drift_rate") if aging_result else None,
663
+ )
664
+
665
+
666
+ def _check_voltage_violations(
667
+ data: NDArray[np.float64],
668
+ is_high: NDArray[np.bool_],
669
+ specs: dict[str, float | None],
670
+ sample_rate: float,
671
+ ) -> tuple[list[dict[str, Any]], int, int]:
672
+ """Check for VOH and VOL violations.
529
673
 
530
- voh_min = specs["voh_min"] # type: ignore[index]
531
- vol_max = specs["vol_max"] # type: ignore[index]
674
+ Args:
675
+ data: Signal data.
676
+ is_high: Boolean mask for high samples.
677
+ specs: Logic family specifications.
678
+ sample_rate: Sample rate in Hz.
532
679
 
680
+ Returns:
681
+ Tuple of (violations list, voh_violation_count, vol_violation_count).
682
+ """
683
+ high_samples = data[is_high]
684
+ low_samples = data[~is_high]
685
+
686
+ voh_min = specs["voh_min"]
687
+ vol_max = specs["vol_max"]
688
+
689
+ violations = []
533
690
  voh_violations = 0
534
691
  vol_violations = 0
535
- violations = []
536
692
 
537
- # Check VOH violations (high samples below spec)
693
+ # Check VOH violations
538
694
  if voh_min is not None and len(high_samples) > 0:
539
695
  voh_mask = high_samples < voh_min
540
- voh_violations = np.sum(voh_mask)
696
+ voh_violations = int(np.sum(voh_mask))
541
697
  if voh_violations > 0:
542
698
  violation_indices = np.where(is_high)[0][voh_mask]
543
- for idx in violation_indices[:10]: # First 10 violations
699
+ for idx in violation_indices[:10]:
544
700
  violations.append(
545
701
  {
546
702
  "timestamp_us": idx / sample_rate * 1e6,
@@ -550,12 +706,12 @@ def assess_signal_quality(
550
706
  }
551
707
  )
552
708
 
553
- # Check VOL violations (low samples above spec)
709
+ # Check VOL violations
554
710
  if vol_max is not None and len(low_samples) > 0:
555
711
  vol_mask = low_samples > vol_max
556
- vol_violations = np.sum(vol_mask)
712
+ vol_violations = int(np.sum(vol_mask))
557
713
  if vol_violations > 0:
558
- violation_indices = np.where(is_low)[0][vol_mask]
714
+ violation_indices = np.where(~is_high)[0][vol_mask]
559
715
  for idx in violation_indices[:10]:
560
716
  violations.append(
561
717
  {
@@ -566,79 +722,113 @@ def assess_signal_quality(
566
722
  }
567
723
  )
568
724
 
569
- total_violations = voh_violations + vol_violations
725
+ return violations, voh_violations, vol_violations
726
+
727
+
728
+ def _calculate_voltage_margins(
729
+ high_samples: NDArray[np.float64],
730
+ low_samples: NDArray[np.float64],
731
+ specs: dict[str, float | None],
732
+ ) -> float:
733
+ """Calculate minimum voltage margin to spec.
734
+
735
+ Args:
736
+ high_samples: High-level samples.
737
+ low_samples: Low-level samples.
738
+ specs: Logic family specifications.
570
739
 
571
- # Calculate margins
572
- margins = []
740
+ Returns:
741
+ Minimum margin in mV.
742
+ """
743
+ voh_min = specs["voh_min"]
744
+ vol_max = specs["vol_max"]
745
+
746
+ margins: list[float] = []
573
747
  if len(high_samples) > 0 and voh_min is not None:
574
- margins.extend((high_samples - voh_min) * 1000) # Convert to mV
748
+ margins.extend((high_samples - voh_min) * 1000)
575
749
  if len(low_samples) > 0 and vol_max is not None:
576
750
  margins.extend((vol_max - low_samples) * 1000)
577
751
 
578
- min_margin_mv = min(margins) if margins else 0.0
752
+ return min(margins) if margins else 0.0
753
+
754
+
755
+ def _determine_quality_status(min_margin_mv: float) -> Literal["OK", "WARNING", "CRITICAL"]:
756
+ """Determine quality status from voltage margin.
579
757
 
580
- # Determine status
758
+ Args:
759
+ min_margin_mv: Minimum voltage margin in mV.
760
+
761
+ Returns:
762
+ Quality status.
763
+ """
581
764
  if min_margin_mv < 100:
582
- status: Literal["OK", "WARNING", "CRITICAL"] = "CRITICAL"
765
+ return "CRITICAL"
583
766
  elif min_margin_mv < 200:
584
- status = "WARNING"
767
+ return "WARNING"
585
768
  else:
586
- status = "OK"
769
+ return "OK"
587
770
 
588
- # Calculate rates
589
- n_high = len(high_samples)
590
- n_low = len(low_samples)
591
- voh_rate = voh_violations / n_high if n_high > 0 else 0.0
592
- vol_rate = vol_violations / n_low if n_low > 0 else 0.0
593
771
 
594
- # Aging analysis
595
- failure_diagnosis = None
596
- time_to_failure_s = None
597
- drift_rate_mv_per_s = None
598
-
599
- if check_aging and n_samples > 1000:
600
- # Calculate drift over time
601
- window_samples = int(time_window_s * sample_rate)
602
- n_windows = n_samples // window_samples
603
-
604
- if n_windows >= 2:
605
- window_means = [
606
- np.mean(data[i * window_samples : (i + 1) * window_samples])
607
- for i in range(n_windows)
608
- ]
609
-
610
- drift = window_means[-1] - window_means[0]
611
- drift_rate_mv_per_s = drift * 1000 / (n_windows * time_window_s)
612
-
613
- if abs(drift_rate_mv_per_s) > 0.1: # Significant drift
614
- # Estimate time to failure
615
- if voh_min is not None and drift_rate_mv_per_s < 0:
616
- current_margin = np.mean(high_samples) - voh_min
617
- if current_margin > 0:
618
- time_to_failure_s = current_margin * 1000 / abs(drift_rate_mv_per_s)
619
-
620
- # Diagnose failure mode
621
- if voh_violations > vol_violations:
622
- failure_diagnosis = "Degraded output driver (weak high)"
623
- elif vol_violations > voh_violations:
624
- failure_diagnosis = "Degraded output driver (weak low)"
625
- else:
626
- failure_diagnosis = "General signal degradation"
772
+ def _analyze_aging(
773
+ data: NDArray[np.float64],
774
+ high_samples: NDArray[np.float64],
775
+ specs: dict[str, float | None],
776
+ sample_rate: float,
777
+ time_window_s: float,
778
+ voh_violations: int,
779
+ vol_violations: int,
780
+ ) -> dict[str, Any] | None:
781
+ """Analyze signal for aging and degradation.
627
782
 
628
- return SignalQualityResult(
629
- status=status,
630
- violation_count=total_violations,
631
- total_samples=n_samples,
632
- min_margin_mv=min_margin_mv,
633
- violations=violations,
634
- voh_violations=voh_violations,
635
- vol_violations=vol_violations,
636
- voh_rate=voh_rate,
637
- vol_rate=vol_rate,
638
- failure_diagnosis=failure_diagnosis,
639
- time_to_failure_s=time_to_failure_s,
640
- drift_rate_mv_per_s=drift_rate_mv_per_s,
641
- )
783
+ Args:
784
+ data: Full signal data.
785
+ high_samples: High-level samples.
786
+ specs: Logic family specifications.
787
+ sample_rate: Sample rate in Hz.
788
+ time_window_s: Window size for drift analysis.
789
+ voh_violations: Count of VOH violations.
790
+ vol_violations: Count of VOL violations.
791
+
792
+ Returns:
793
+ Dict with diagnosis, time_to_failure, and drift_rate, or None.
794
+ """
795
+ window_samples = int(time_window_s * sample_rate)
796
+ n_windows = len(data) // window_samples
797
+
798
+ if n_windows < 2:
799
+ return None
800
+
801
+ window_means = [
802
+ np.mean(data[i * window_samples : (i + 1) * window_samples]) for i in range(n_windows)
803
+ ]
804
+
805
+ drift = window_means[-1] - window_means[0]
806
+ drift_rate_mv_per_s = drift * 1000 / (n_windows * time_window_s)
807
+
808
+ if abs(drift_rate_mv_per_s) <= 0.1:
809
+ return None
810
+
811
+ # Estimate time to failure
812
+ time_to_failure = None
813
+ voh_min = specs["voh_min"]
814
+ if voh_min is not None and drift_rate_mv_per_s < 0:
815
+ current_margin = np.mean(high_samples) - voh_min
816
+ if current_margin > 0:
817
+ time_to_failure = current_margin * 1000 / abs(drift_rate_mv_per_s)
818
+
819
+ # Diagnose failure mode
820
+ if voh_violations > vol_violations:
821
+ diagnosis = "Degraded output driver (weak high)"
822
+ elif vol_violations > voh_violations:
823
+ diagnosis = "Degraded output driver (weak low)"
824
+ else:
825
+ diagnosis = "General signal degradation"
826
+
827
+ return {
828
+ "diagnosis": diagnosis,
829
+ "time_to_failure": time_to_failure,
830
+ "drift_rate": drift_rate_mv_per_s,
831
+ }
642
832
 
643
833
 
644
834
  @dataclass
@@ -773,49 +963,81 @@ def _is_bimodal(data: NDArray[np.float64], bins: int = 50) -> bool:
773
963
  hist, bin_edges = np.histogram(data, bins=bins)
774
964
  centers = (bin_edges[:-1] + bin_edges[1:]) / 2
775
965
 
776
- # Find peaks (including edge bins for perfect bimodal signals)
966
+ # Find peaks in histogram
967
+ peaks = _find_histogram_peaks(hist, centers)
968
+
969
+ # Too many peaks suggests analog signal
970
+ if len(peaks) >= 4:
971
+ return False
972
+
973
+ # Check if distribution is bimodal
974
+ if len(peaks) == 2 or len(peaks) == 3:
975
+ return _is_bimodal_distribution(data, peaks)
976
+
977
+ return False
978
+
979
+
980
+ def _find_histogram_peaks(
981
+ hist: NDArray[np.int64], centers: NDArray[np.float64]
982
+ ) -> list[tuple[int, int, float]]:
983
+ """Find peaks in histogram.
984
+
985
+ Args:
986
+ hist: Histogram counts.
987
+ centers: Bin centers.
988
+
989
+ Returns:
990
+ List of (index, count, center) tuples.
991
+ """
777
992
  threshold = 0.1 * np.max(hist)
778
993
  peaks = []
779
994
 
780
995
  # Check first bin (only needs to be > right neighbor)
781
996
  if len(hist) > 1 and hist[0] > hist[1] and hist[0] > threshold:
782
- peaks.append((0, hist[0], centers[0]))
997
+ peaks.append((0, int(hist[0]), float(centers[0])))
783
998
 
784
999
  # Check middle bins (need to be > both neighbors)
785
1000
  for i in range(1, len(hist) - 1):
786
1001
  if hist[i] > hist[i - 1] and hist[i] > hist[i + 1] and hist[i] > threshold:
787
- peaks.append((i, hist[i], centers[i]))
1002
+ peaks.append((i, int(hist[i]), float(centers[i])))
788
1003
 
789
1004
  # Check last bin (only needs to be > left neighbor)
790
1005
  if len(hist) > 1 and hist[-1] > hist[-2] and hist[-1] > threshold:
791
- peaks.append((len(hist) - 1, hist[-1], centers[-1]))
1006
+ peaks.append((len(hist) - 1, int(hist[-1]), float(centers[-1])))
792
1007
 
793
- # Too many peaks suggests analog signal (e.g., sine wave)
794
- if len(peaks) >= 4:
795
- return False
1008
+ return peaks
796
1009
 
797
- # Bimodal if exactly 2-3 significant peaks that are well-separated
798
- if len(peaks) == 2 or len(peaks) == 3:
799
- peaks.sort(key=lambda x: x[1], reverse=True)
800
1010
 
801
- # Check if peaks are well-separated (digital signals have peaks at extremes)
802
- v_min, v_max = np.min(data), np.max(data)
803
- v_range = v_max - v_min
804
- if v_range == 0:
805
- return False
1011
+ def _is_bimodal_distribution(
1012
+ data: NDArray[np.float64], peaks: list[tuple[int, int, float]]
1013
+ ) -> bool:
1014
+ """Check if peaks indicate bimodal distribution.
806
1015
 
807
- # Normalize peak positions
808
- peak_positions = [(p[2] - v_min) / v_range for p in peaks[:2]]
1016
+ Args:
1017
+ data: Signal data.
1018
+ peaks: List of (index, count, center) tuples.
809
1019
 
810
- # Digital signals have one peak < 0.4 and one peak > 0.6
811
- has_low_peak = any(p < 0.4 for p in peak_positions)
812
- has_high_peak = any(p > 0.6 for p in peak_positions)
1020
+ Returns:
1021
+ True if distribution is bimodal.
1022
+ """
1023
+ # Sort peaks by count (descending)
1024
+ peaks.sort(key=lambda x: x[1], reverse=True)
1025
+
1026
+ # Check if peaks are well-separated
1027
+ v_min, v_max = np.min(data), np.max(data)
1028
+ v_range = v_max - v_min
1029
+ if v_range == 0:
1030
+ return False
813
1031
 
814
- # Second peak should be significant
815
- if has_low_peak and has_high_peak and peaks[1][1] > 0.3 * peaks[0][1]:
816
- return True
1032
+ # Normalize peak positions
1033
+ peak_positions = [(p[2] - v_min) / v_range for p in peaks[:2]]
817
1034
 
818
- return False
1035
+ # Digital signals have one peak < 0.4 and one peak > 0.6
1036
+ has_low_peak = any(p < 0.4 for p in peak_positions)
1037
+ has_high_peak = any(p > 0.6 for p in peak_positions)
1038
+
1039
+ # Second peak should be significant
1040
+ return has_low_peak and has_high_peak and peaks[1][1] > 0.3 * peaks[0][1]
819
1041
 
820
1042
 
821
1043
  __all__ = [