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 @@ including eye diagram masks and custom polygon masks.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.comparison import mask_test, eye_mask
8
+ >>> from oscura.utils.comparison import mask_test, eye_mask
9
9
  >>> mask = eye_mask(0.5, 0.4, 0.3)
10
10
  >>> result = mask_test(trace, mask)
11
11
 
@@ -274,20 +274,77 @@ def mask_test(
274
274
  >>> result = mask_test(eye_trace, mask)
275
275
  >>> print(f"Violations: {result.num_violations}")
276
276
  """
277
- # Get Y data
277
+ # Prepare test data
278
+ x_arr, y_arr = _prepare_mask_test_data(trace, x_data, normalize)
279
+
280
+ # Find all violations
281
+ violations, violations_by_region = _find_mask_violations(x_arr, y_arr, mask)
282
+
283
+ # Calculate statistics
284
+ unique_violations = list(set(violations))
285
+ num_violations = len(unique_violations)
286
+ violation_rate = num_violations / len(y_arr) if len(y_arr) > 0 else 0.0
287
+
288
+ # Calculate margin to mask boundary
289
+ margin = _calculate_mask_margin(x_arr, y_arr, mask, num_violations)
290
+
291
+ return MaskTestResult(
292
+ passed=num_violations == 0,
293
+ num_violations=num_violations,
294
+ violation_rate=violation_rate,
295
+ violation_points=unique_violations,
296
+ violations_by_region=violations_by_region,
297
+ margin=margin,
298
+ )
299
+
300
+
301
+ def _prepare_mask_test_data(
302
+ trace: WaveformTrace,
303
+ x_data: NDArray[np.floating[Any]] | None,
304
+ normalize: bool,
305
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
306
+ """Prepare X and Y data arrays for mask testing.
307
+
308
+ Args:
309
+ trace: Waveform trace.
310
+ x_data: Optional X coordinates (defaults to sample indices).
311
+ normalize: Whether to normalize Y data to [-1, 1].
312
+
313
+ Returns:
314
+ Tuple of (x_array, y_array) as float64.
315
+ """
278
316
  y_data = trace.data.astype(np.float64)
279
317
 
280
- # Get or create X data
318
+ # Generate or use provided X data
281
319
  if x_data is None:
282
- x_data = np.arange(len(y_data), dtype=np.float64)
320
+ x_arr = np.arange(len(y_data), dtype=np.float64)
321
+ else:
322
+ x_arr = x_data
283
323
 
284
- # Normalize if requested
324
+ # Normalize Y data if requested
285
325
  if normalize:
286
326
  y_min, y_max = np.min(y_data), np.max(y_data)
287
327
  if y_max - y_min > 0:
288
328
  y_data = 2 * (y_data - y_min) / (y_max - y_min) - 1
289
329
 
290
- # Test each point against mask regions
330
+ return (x_arr, y_data)
331
+
332
+
333
+ def _find_mask_violations(
334
+ x_data: NDArray[np.float64],
335
+ y_data: NDArray[np.float64],
336
+ mask: Mask,
337
+ ) -> tuple[list[tuple[float, float]], dict[str, int]]:
338
+ """Find all mask violations in waveform data.
339
+
340
+ Args:
341
+ x_data: X coordinates.
342
+ y_data: Y coordinates.
343
+ mask: Mask with regions to test.
344
+
345
+ Returns:
346
+ Tuple of (violation_points, violations_by_region_name).
347
+ """
291
348
  violations: list[tuple[float, float]] = []
292
349
  violations_by_region: dict[str, int] = {}
293
350
 
@@ -296,48 +353,97 @@ def mask_test(
296
353
  violations_by_region[region_name] = 0
297
354
 
298
355
  if region.region_type == "violation":
299
- # Check if points are inside violation region
300
- for i, (x, y) in enumerate(zip(x_data, y_data, strict=False)): # noqa: B007
301
- if region.contains_point(float(x), float(y)):
302
- violations.append((float(x), float(y)))
303
- violations_by_region[region_name] += 1
304
-
356
+ # Points inside violation region are violations
357
+ region_violations = _check_violation_region(x_data, y_data, region)
358
+ violations.extend(region_violations)
359
+ violations_by_region[region_name] = len(region_violations)
305
360
  elif region.region_type == "boundary":
306
- # Check if points are outside boundary region
307
- for i, (x, y) in enumerate(zip(x_data, y_data, strict=False)): # noqa: B007
308
- if not region.contains_point(float(x), float(y)):
309
- violations.append((float(x), float(y)))
310
- violations_by_region[region_name] += 1
361
+ # Points outside boundary region are violations
362
+ region_violations = _check_boundary_region(x_data, y_data, region)
363
+ violations.extend(region_violations)
364
+ violations_by_region[region_name] = len(region_violations)
311
365
 
312
- # Remove duplicates
313
- unique_violations = list(set(violations))
314
- num_violations = len(unique_violations)
315
- violation_rate = num_violations / len(y_data) if len(y_data) > 0 else 0.0
316
-
317
- # Estimate margin (simplified - distance to nearest mask edge)
318
- margin = None
319
- if num_violations == 0 and mask.regions:
320
- # Find minimum distance to any violation region
321
- min_dist = float("inf")
322
- for region in mask.regions:
323
- if region.region_type == "violation":
324
- for x, y in zip(x_data, y_data, strict=False):
325
- for i in range(len(region.vertices)):
326
- x1, y1 = region.vertices[i]
327
- x2, y2 = region.vertices[(i + 1) % len(region.vertices)]
328
- # Distance to line segment
329
- dist = _point_to_segment_distance(x, y, x1, y1, x2, y2)
330
- min_dist = min(min_dist, dist)
331
- margin = min_dist if min_dist != float("inf") else None
366
+ return (violations, violations_by_region)
332
367
 
333
- return MaskTestResult(
334
- passed=num_violations == 0,
335
- num_violations=num_violations,
336
- violation_rate=violation_rate,
337
- violation_points=unique_violations,
338
- violations_by_region=violations_by_region,
339
- margin=margin,
340
- )
368
+
369
+ def _check_violation_region(
370
+ x_data: NDArray[np.float64],
371
+ y_data: NDArray[np.float64],
372
+ region: MaskRegion,
373
+ ) -> list[tuple[float, float]]:
374
+ """Check for points inside violation region.
375
+
376
+ Args:
377
+ x_data: X coordinates.
378
+ y_data: Y coordinates.
379
+ region: Violation region.
380
+
381
+ Returns:
382
+ List of (x, y) points violating this region.
383
+ """
384
+ violations = []
385
+ for x, y in zip(x_data, y_data, strict=False):
386
+ if region.contains_point(float(x), float(y)):
387
+ violations.append((float(x), float(y)))
388
+ return violations
389
+
390
+
391
+ def _check_boundary_region(
392
+ x_data: NDArray[np.float64],
393
+ y_data: NDArray[np.float64],
394
+ region: MaskRegion,
395
+ ) -> list[tuple[float, float]]:
396
+ """Check for points outside boundary region.
397
+
398
+ Args:
399
+ x_data: X coordinates.
400
+ y_data: Y coordinates.
401
+ region: Boundary region.
402
+
403
+ Returns:
404
+ List of (x, y) points violating this region.
405
+ """
406
+ violations = []
407
+ for x, y in zip(x_data, y_data, strict=False):
408
+ if not region.contains_point(float(x), float(y)):
409
+ violations.append((float(x), float(y)))
410
+ return violations
411
+
412
+
413
+ def _calculate_mask_margin(
414
+ x_data: NDArray[np.float64],
415
+ y_data: NDArray[np.float64],
416
+ mask: Mask,
417
+ num_violations: int,
418
+ ) -> float | None:
419
+ """Calculate margin to nearest mask edge.
420
+
421
+ Only calculated when there are no violations.
422
+
423
+ Args:
424
+ x_data: X coordinates.
425
+ y_data: Y coordinates.
426
+ mask: Mask with regions.
427
+ num_violations: Number of violations found.
428
+
429
+ Returns:
430
+ Minimum distance to mask boundary, or None if violations exist or no regions.
431
+ """
432
+ if num_violations > 0 or not mask.regions:
433
+ return None
434
+
435
+ # Find minimum distance to any violation region edge
436
+ min_dist = float("inf")
437
+ for region in mask.regions:
438
+ if region.region_type == "violation":
439
+ for x, y in zip(x_data, y_data, strict=False):
440
+ for i in range(len(region.vertices)):
441
+ x1, y1 = region.vertices[i]
442
+ x2, y2 = region.vertices[(i + 1) % len(region.vertices)]
443
+ dist = _point_to_segment_distance(x, y, x1, y1, x2, y2)
444
+ min_dist = min(min_dist, dist)
445
+
446
+ return min_dist if min_dist != float("inf") else None
341
447
 
342
448
 
343
449
  def _point_to_segment_distance(
@@ -6,7 +6,7 @@ discovery.comparison module to maintain API compatibility.
6
6
 
7
7
 
8
8
  Example:
9
- >>> from oscura.comparison import compare_traces
9
+ >>> from oscura.utils.comparison import compare_traces
10
10
  >>> diff = compare_traces(trace1, trace2)
11
11
  >>> for d in diff.differences:
12
12
  ... print(f"{d.category}: {d.description}")
@@ -5,7 +5,7 @@ overlay plots, difference plots, and comparison heat maps.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.comparison.visualization import (
8
+ >>> from oscura.utils.comparison.visualization import (
9
9
  ... plot_overlay,
10
10
  ... plot_difference,
11
11
  ... plot_comparison_heatmap
@@ -24,12 +24,14 @@ from typing import TYPE_CHECKING, Any
24
24
  import matplotlib.pyplot as plt
25
25
  import numpy as np
26
26
  from matplotlib.gridspec import GridSpec
27
+ from numpy.typing import NDArray
27
28
 
28
29
  if TYPE_CHECKING:
30
+ from matplotlib.axes import Axes
29
31
  from matplotlib.figure import Figure
30
32
 
31
- from oscura.comparison.compare import ComparisonResult
32
33
  from oscura.core.types import WaveformTrace
34
+ from oscura.utils.comparison.compare import ComparisonResult
33
35
 
34
36
 
35
37
  def plot_overlay(
@@ -61,7 +63,7 @@ def plot_overlay(
61
63
  Matplotlib Figure object
62
64
 
63
65
  Example:
64
- >>> from oscura.comparison.visualization import plot_overlay
66
+ >>> from oscura.utils.comparison.visualization import plot_overlay
65
67
  >>> fig = plot_overlay(measured, reference,
66
68
  ... labels=("Measured", "Reference"))
67
69
  >>> plt.show()
@@ -160,7 +162,7 @@ def plot_difference(
160
162
  Matplotlib Figure object
161
163
 
162
164
  Example:
163
- >>> from oscura.comparison.visualization import plot_difference
165
+ >>> from oscura.utils.comparison.visualization import plot_difference
164
166
  >>> fig = plot_difference(measured, reference, normalize=True)
165
167
  >>> plt.show()
166
168
 
@@ -259,89 +261,111 @@ def plot_comparison_heatmap(
259
261
  Matplotlib Figure object
260
262
 
261
263
  Example:
262
- >>> from oscura.comparison.visualization import plot_comparison_heatmap
264
+ >>> from oscura.utils.comparison.visualization import plot_comparison_heatmap
263
265
  >>> fig = plot_comparison_heatmap(trace1, trace2, window_size=50)
264
266
  >>> plt.show()
265
267
 
266
268
  References:
267
269
  CMP-003: Difference heat map showing where changes occur
268
270
  """
271
+ fig, (ax_heat, ax_trace) = _create_heatmap_axes(figsize)
272
+
273
+ # Align data and compute difference
274
+ data1, data2, diff, min_len = _prepare_diff_data(trace1, trace2)
275
+
276
+ # Create and plot heatmap
277
+ n_windows, window_size = _compute_window_params(min_len, window_size)
278
+ heatmap_data = _build_heatmap(data1, data2, diff, n_windows, window_size, min_len)
279
+ _plot_heatmap(ax_heat, heatmap_data, title, **kwargs)
280
+
281
+ # Plot difference trace
282
+ time, xlabel = _compute_time_axis(trace1, min_len)
283
+ _plot_diff_trace(ax_trace, time, diff, xlabel)
284
+
285
+ plt.tight_layout()
286
+ return fig
287
+
288
+
289
+ def _create_heatmap_axes(figsize: tuple[float, float]) -> tuple[Figure, tuple[Axes, Axes]]:
290
+ """Create figure with heatmap and trace axes."""
269
291
  fig = plt.figure(figsize=figsize)
270
292
  gs = GridSpec(2, 1, height_ratios=[3, 1], hspace=0.3)
271
293
  ax_heat = fig.add_subplot(gs[0])
272
294
  ax_trace = fig.add_subplot(gs[1], sharex=ax_heat)
295
+ return fig, (ax_heat, ax_trace)
273
296
 
274
- # Get data
297
+
298
+ def _prepare_diff_data(
299
+ trace1: WaveformTrace, trace2: WaveformTrace
300
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64], int]:
301
+ """Prepare aligned data and compute difference."""
275
302
  data1 = trace1.data.astype(np.float64)
276
303
  data2 = trace2.data.astype(np.float64)
277
-
278
- # Align lengths
279
304
  min_len = min(len(data1), len(data2))
280
- data1 = data1[:min_len]
281
- data2 = data2[:min_len]
282
-
283
- # Compute difference
305
+ data1, data2 = data1[:min_len], data2[:min_len]
284
306
  diff = np.abs(data1 - data2)
307
+ return data1, data2, diff, min_len
308
+
285
309
 
286
- # Create windowed heatmap
310
+ def _compute_window_params(min_len: int, window_size: int) -> tuple[int, int]:
311
+ """Compute window parameters for heatmap."""
287
312
  n_windows = min_len // window_size
288
313
  if n_windows == 0:
289
- n_windows = 1
290
- window_size = min_len
291
-
314
+ return 1, min_len
315
+ return n_windows, window_size
316
+
317
+
318
+ def _build_heatmap(
319
+ data1: NDArray[np.float64],
320
+ data2: NDArray[np.float64],
321
+ diff: NDArray[np.float64],
322
+ n_windows: int,
323
+ window_size: int,
324
+ min_len: int,
325
+ ) -> NDArray[np.float64]:
326
+ """Build heatmap data from windowed differences."""
292
327
  heatmap_data = np.zeros((10, n_windows))
293
328
  for i in range(n_windows):
294
329
  start = i * window_size
295
330
  end = min(start + window_size, min_len)
296
- window_diff = diff[start:end]
297
-
298
- # Bin into 10 levels based on amplitude
299
- window_data1 = data1[start:end]
300
- window_data2 = data2[start:end]
301
- y_min = min(np.min(window_data1), np.min(window_data2))
302
- y_max = max(np.max(window_data1), np.max(window_data2))
303
-
331
+ window_data1, window_diff = data1[start:end], diff[start:end]
332
+ y_min, y_max = (
333
+ min(np.min(data1[start:end]), np.min(data2[start:end])),
334
+ max(np.max(data1[start:end]), np.max(data2[start:end])),
335
+ )
304
336
  if y_max - y_min > 0:
305
337
  bins = np.linspace(y_min, y_max, 11)
306
- for sample_idx in range(len(window_diff)):
307
- y_val = window_data1[sample_idx] # window_data1 is already sliced
308
- bin_idx = np.digitize(y_val, bins) - 1
309
- bin_idx = max(0, min(9, bin_idx))
338
+ for sample_idx, y_val in enumerate(window_data1):
339
+ bin_idx = max(0, min(9, np.digitize(y_val, bins) - 1))
310
340
  heatmap_data[bin_idx, i] += window_diff[sample_idx]
341
+ return heatmap_data / window_size
342
+
311
343
 
312
- # Normalize heatmap
313
- heatmap_data = heatmap_data / window_size
314
-
315
- # Plot heatmap
316
- im = ax_heat.imshow(
317
- heatmap_data,
318
- aspect="auto",
319
- cmap="hot",
320
- origin="lower",
321
- interpolation="nearest",
322
- **kwargs,
344
+ def _plot_heatmap(ax: Axes, heatmap_data: NDArray[np.float64], title: str, **kwargs: Any) -> None:
345
+ """Plot heatmap on axes."""
346
+ im = ax.imshow(
347
+ heatmap_data, aspect="auto", cmap="hot", origin="lower", interpolation="nearest", **kwargs
323
348
  )
324
- plt.colorbar(im, ax=ax_heat, label="Average Difference")
349
+ plt.colorbar(im, ax=ax, label="Average Difference")
350
+ ax.set_ylabel("Amplitude Bin")
351
+ ax.set_title(title)
325
352
 
326
- ax_heat.set_ylabel("Amplitude Bin")
327
- ax_heat.set_title(title)
328
353
 
329
- # Plot difference trace below
330
- if hasattr(trace1, "metadata") and trace1.metadata.sample_rate is not None:
331
- sample_rate = trace1.metadata.sample_rate
332
- time = np.arange(min_len) / sample_rate
333
- xlabel = "Time (s)"
334
- else:
335
- time = np.arange(min_len, dtype=np.float64)
336
- xlabel = "Sample"
354
+ def _compute_time_axis(trace: WaveformTrace, min_len: int) -> tuple[NDArray[np.float64], str]:
355
+ """Compute time axis and label."""
356
+ if hasattr(trace, "metadata") and trace.metadata.sample_rate is not None:
357
+ return np.arange(min_len) / trace.metadata.sample_rate, "Time (s)"
358
+ return np.arange(min_len, dtype=np.float64), "Sample"
337
359
 
338
- ax_trace.plot(time, diff, linewidth=0.5)
339
- ax_trace.set_xlabel(xlabel)
340
- ax_trace.set_ylabel("Difference")
341
- ax_trace.grid(True, alpha=0.3)
342
360
 
343
- plt.tight_layout()
344
- return fig
361
+ def _plot_diff_trace(
362
+ ax: Axes, time: NDArray[np.float64], diff: NDArray[np.float64], xlabel: str
363
+ ) -> None:
364
+ """Plot difference trace on axes."""
365
+ ax.plot(time, diff, linewidth=0.5)
366
+ ax.set_xlabel(xlabel)
367
+ ax.set_ylabel("Difference")
368
+ ax.grid(True, alpha=0.3)
345
369
 
346
370
 
347
371
  def plot_comparison_summary(
@@ -363,8 +387,8 @@ def plot_comparison_summary(
363
387
  Matplotlib Figure object
364
388
 
365
389
  Example:
366
- >>> from oscura.comparison import compare_traces
367
- >>> from oscura.comparison.visualization import plot_comparison_summary
390
+ >>> from oscura.utils.comparison import compare_traces
391
+ >>> from oscura.utils.comparison.visualization import plot_comparison_summary
368
392
  >>> result = compare_traces(trace1, trace2)
369
393
  >>> fig = plot_comparison_summary(result)
370
394
  >>> plt.show()
@@ -375,7 +399,28 @@ def plot_comparison_summary(
375
399
  fig = plt.figure(figsize=figsize)
376
400
  gs = GridSpec(3, 2, hspace=0.4, wspace=0.3)
377
401
 
378
- # Statistics table
402
+ # Create statistics table
403
+ _plot_statistics_table(fig, gs, result, title)
404
+
405
+ # Plot difference trace
406
+ if result.difference_trace is not None:
407
+ _plot_difference_trace(fig, gs, result.difference_trace)
408
+ _plot_difference_histogram(fig, gs, result.difference_trace)
409
+
410
+ # Plot violation locations
411
+ _plot_violations(fig, gs, result)
412
+
413
+ plt.tight_layout()
414
+ return fig
415
+
416
+
417
+ def _plot_statistics_table(
418
+ fig: Figure,
419
+ gs: GridSpec,
420
+ result: ComparisonResult,
421
+ title: str,
422
+ ) -> None:
423
+ """Plot statistics table at top of summary."""
379
424
  ax_stats = fig.add_subplot(gs[0, :])
380
425
  ax_stats.axis("off")
381
426
 
@@ -415,34 +460,50 @@ def plot_comparison_summary(
415
460
 
416
461
  ax_stats.set_title(title, fontsize=14, fontweight="bold", pad=20)
417
462
 
418
- # Overlay plot
419
- if result.difference_trace is not None:
420
- # Plot difference trace
421
- ax_overlay = fig.add_subplot(gs[1, :])
422
- n_samples = len(result.difference_trace.data)
423
- time = np.arange(n_samples)
424
- ax_overlay.plot(time, result.difference_trace.data, label="Difference")
425
- ax_overlay.axhline(y=0, color="k", linestyle="--", alpha=0.5)
426
- ax_overlay.set_xlabel("Sample")
427
- ax_overlay.set_ylabel("Difference")
428
- ax_overlay.set_title("Difference Trace")
429
- ax_overlay.legend()
430
- ax_overlay.grid(True, alpha=0.3)
431
-
432
- # Histogram of differences
433
- if result.difference_trace is not None:
434
- ax_hist = fig.add_subplot(gs[2, 0])
435
- diff_data = result.difference_trace.data
436
- ax_hist.hist(diff_data, bins=50, edgecolor="black", alpha=0.7)
437
- ax_hist.axvline(x=0, color="r", linestyle="--", linewidth=2, label="Zero difference")
438
- ax_hist.set_xlabel("Difference")
439
- ax_hist.set_ylabel("Count")
440
- ax_hist.set_title("Difference Distribution")
441
- ax_hist.legend()
442
- ax_hist.grid(True, alpha=0.3)
443
-
444
- # Violation locations
463
+
464
+ def _plot_difference_trace(
465
+ fig: Figure,
466
+ gs: GridSpec,
467
+ difference_trace: Any,
468
+ ) -> None:
469
+ """Plot difference trace in middle row."""
470
+ ax_overlay = fig.add_subplot(gs[1, :])
471
+ n_samples = len(difference_trace.data)
472
+ time = np.arange(n_samples)
473
+ ax_overlay.plot(time, difference_trace.data, label="Difference")
474
+ ax_overlay.axhline(y=0, color="k", linestyle="--", alpha=0.5)
475
+ ax_overlay.set_xlabel("Sample")
476
+ ax_overlay.set_ylabel("Difference")
477
+ ax_overlay.set_title("Difference Trace")
478
+ ax_overlay.legend()
479
+ ax_overlay.grid(True, alpha=0.3)
480
+
481
+
482
+ def _plot_difference_histogram(
483
+ fig: Figure,
484
+ gs: GridSpec,
485
+ difference_trace: Any,
486
+ ) -> None:
487
+ """Plot histogram of differences."""
488
+ ax_hist = fig.add_subplot(gs[2, 0])
489
+ diff_data = difference_trace.data
490
+ ax_hist.hist(diff_data, bins=50, edgecolor="black", alpha=0.7)
491
+ ax_hist.axvline(x=0, color="r", linestyle="--", linewidth=2, label="Zero difference")
492
+ ax_hist.set_xlabel("Difference")
493
+ ax_hist.set_ylabel("Count")
494
+ ax_hist.set_title("Difference Distribution")
495
+ ax_hist.legend()
496
+ ax_hist.grid(True, alpha=0.3)
497
+
498
+
499
+ def _plot_violations(
500
+ fig: Figure,
501
+ gs: GridSpec,
502
+ result: ComparisonResult,
503
+ ) -> None:
504
+ """Plot violation locations."""
445
505
  ax_viol = fig.add_subplot(gs[2, 1])
506
+
446
507
  if result.violations is not None and len(result.violations) > 0:
447
508
  ax_viol.scatter(
448
509
  result.violations,
@@ -469,9 +530,6 @@ def plot_comparison_summary(
469
530
  )
470
531
  ax_viol.axis("off")
471
532
 
472
- plt.tight_layout()
473
- return fig
474
-
475
533
 
476
534
  __all__ = [
477
535
  "plot_comparison_heatmap",
@@ -4,17 +4,17 @@ This module provides TDR-based impedance extraction, capacitance/inductance
4
4
  measurement, parasitic extraction, and transmission line analysis.
5
5
  """
6
6
 
7
- from oscura.component.impedance import (
7
+ from oscura.utils.component.impedance import (
8
8
  discontinuity_analysis,
9
9
  extract_impedance,
10
10
  impedance_profile,
11
11
  )
12
- from oscura.component.reactive import (
12
+ from oscura.utils.component.reactive import (
13
13
  extract_parasitics,
14
14
  measure_capacitance,
15
15
  measure_inductance,
16
16
  )
17
- from oscura.component.transmission_line import (
17
+ from oscura.utils.component.transmission_line import (
18
18
  characteristic_impedance,
19
19
  propagation_delay,
20
20
  transmission_line_analysis,