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
@@ -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__ = [