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
@@ -92,62 +92,19 @@ class ZoomState:
92
92
  home_ylim: tuple[float, float] | None = None
93
93
 
94
94
 
95
- def enable_zoom_pan(
96
- ax: Axes,
97
- *,
98
- enable_zoom: bool = True,
99
- enable_pan: bool = True,
100
- zoom_factor: float = 1.5,
101
- ) -> ZoomState:
102
- """Enable interactive zoom and pan on an axes.
103
-
104
- Adds scroll wheel zoom and click-drag pan functionality.
105
-
106
- Args:
107
- ax: Matplotlib axes to enable zoom/pan on.
108
- enable_zoom: Enable scroll wheel zoom.
109
- enable_pan: Enable click-drag pan.
110
- zoom_factor: Zoom factor per scroll step.
111
-
112
- Returns:
113
- ZoomState object tracking zoom history.
114
-
115
- Raises:
116
- ImportError: If matplotlib is not available.
117
-
118
- Example:
119
- >>> fig, ax = plt.subplots()
120
- >>> ax.plot(trace.time_vector, trace.data)
121
- >>> state = enable_zoom_pan(ax)
122
-
123
- References:
124
- VIS-007
125
- """
126
- if not MATPLOTLIB_AVAILABLE:
127
- raise ImportError("matplotlib is required for interactive visualization")
128
-
129
- # Store initial state
130
- xlim = ax.get_xlim()
131
- ylim = ax.get_ylim()
132
- state = ZoomState(
133
- xlim=xlim,
134
- ylim=ylim,
135
- home_xlim=xlim,
136
- home_ylim=ylim,
137
- )
95
+ def _create_scroll_handler(ax: Axes, state: ZoomState, zoom_factor: float) -> Any:
96
+ """Create scroll event handler for zooming."""
138
97
 
139
98
  def on_scroll(event): # type: ignore[no-untyped-def]
140
99
  if event.inaxes != ax:
141
100
  return
142
101
 
143
- # Get mouse position
144
102
  x_data = event.xdata
145
103
  y_data = event.ydata
146
104
 
147
105
  if x_data is None or y_data is None:
148
106
  return
149
107
 
150
- # Determine zoom direction
151
108
  if event.button == "up":
152
109
  factor = 1 / zoom_factor
153
110
  elif event.button == "down":
@@ -155,10 +112,8 @@ def enable_zoom_pan(
155
112
  else:
156
113
  return
157
114
 
158
- # Save current state
159
115
  state.history.append((state.xlim, state.ylim))
160
116
 
161
- # Calculate new limits centered on mouse position
162
117
  cur_xlim = ax.get_xlim()
163
118
  cur_ylim = ax.get_ylim()
164
119
 
@@ -184,17 +139,18 @@ def enable_zoom_pan(
184
139
 
185
140
  ax.figure.canvas.draw_idle()
186
141
 
187
- if enable_zoom:
188
- ax.figure.canvas.mpl_connect("scroll_event", on_scroll)
142
+ return on_scroll
143
+
189
144
 
190
- # Pan state
145
+ def _create_pan_handlers(ax: Axes, state: ZoomState) -> tuple[Any, Any, Any]:
146
+ """Create pan event handlers for click-drag panning."""
191
147
  pan_active = [False]
192
148
  pan_start: list[float | None] = [None, None]
193
149
 
194
150
  def on_press(event): # type: ignore[no-untyped-def]
195
151
  if event.inaxes != ax:
196
152
  return
197
- if event.button == 1: # Left click
153
+ if event.button == 1:
198
154
  pan_active[0] = True
199
155
  pan_start[0] = event.xdata
200
156
  pan_start[1] = event.ydata
@@ -228,10 +184,56 @@ def enable_zoom_pan(
228
184
 
229
185
  ax.figure.canvas.draw_idle()
230
186
 
187
+ return on_press, on_release, on_motion
188
+
189
+
190
+ def enable_zoom_pan(
191
+ ax: Axes,
192
+ *,
193
+ enable_zoom: bool = True,
194
+ enable_pan: bool = True,
195
+ zoom_factor: float = 1.5,
196
+ ) -> ZoomState:
197
+ """Enable interactive zoom and pan on an axes.
198
+
199
+ Adds scroll wheel zoom and click-drag pan functionality.
200
+
201
+ Args:
202
+ ax: Matplotlib axes to enable zoom/pan on.
203
+ enable_zoom: Enable scroll wheel zoom.
204
+ enable_pan: Enable click-drag pan.
205
+ zoom_factor: Zoom factor per scroll step.
206
+
207
+ Returns:
208
+ ZoomState object tracking zoom history.
209
+
210
+ Raises:
211
+ ImportError: If matplotlib is not available.
212
+
213
+ Example:
214
+ >>> fig, ax = plt.subplots()
215
+ >>> ax.plot(trace.time_vector, trace.data)
216
+ >>> state = enable_zoom_pan(ax)
217
+
218
+ References:
219
+ VIS-007
220
+ """
221
+ if not MATPLOTLIB_AVAILABLE:
222
+ raise ImportError("matplotlib is required for interactive visualization")
223
+
224
+ xlim = ax.get_xlim()
225
+ ylim = ax.get_ylim()
226
+ state = ZoomState(xlim=xlim, ylim=ylim, home_xlim=xlim, home_ylim=ylim)
227
+
228
+ if enable_zoom:
229
+ on_scroll = _create_scroll_handler(ax, state, zoom_factor)
230
+ ax.figure.canvas.mpl_connect("scroll_event", on_scroll)
231
+
231
232
  if enable_pan:
233
+ on_press, on_release, on_motion = _create_pan_handlers(ax, state)
232
234
  ax.figure.canvas.mpl_connect("button_press_event", on_press)
233
- ax.figure.canvas.mpl_connect("button_release_event", on_release) # type: ignore[arg-type]
234
- ax.figure.canvas.mpl_connect("motion_notify_event", on_motion) # type: ignore[arg-type]
235
+ ax.figure.canvas.mpl_connect("button_release_event", on_release)
236
+ ax.figure.canvas.mpl_connect("motion_notify_event", on_motion)
235
237
 
236
238
  return state
237
239
 
@@ -339,42 +341,70 @@ def add_measurement_cursors(
339
341
  if not MATPLOTLIB_AVAILABLE:
340
342
  raise ImportError("matplotlib is required for interactive visualization")
341
343
 
342
- state: dict[str, float | None | Any] = {
343
- "x1": None,
344
- "x2": None,
345
- "y1": None,
346
- "y2": None,
347
- "line1": None,
348
- "line2": None,
349
- }
344
+ # Setup: initialize state
345
+ state = _create_cursor_state()
346
+
347
+ # Processing: create selector with callback
348
+ onselect_callback = _create_cursor_select_handler(ax, state)
349
+ span = SpanSelector(
350
+ ax,
351
+ onselect_callback,
352
+ "horizontal",
353
+ useblit=True,
354
+ props={"alpha": 0.3, "facecolor": color},
355
+ interactive=True,
356
+ )
357
+
358
+ # Formatting: create measurement accessor
359
+ get_measurement = _create_measurement_getter(state)
360
+
361
+ return {"span": span, "state": state, "get_measurement": get_measurement}
362
+
363
+
364
+ def _create_cursor_state() -> dict[str, float | None | Any]:
365
+ """Create cursor state dictionary.
366
+
367
+ Returns:
368
+ State dictionary with x/y positions and line references.
369
+ """
370
+ return {"x1": None, "x2": None, "y1": None, "y2": None, "line1": None, "line2": None}
371
+
372
+
373
+ def _create_cursor_select_handler(ax: Axes, state: dict[str, float | None | Any]) -> Any:
374
+ """Create cursor selection callback.
375
+
376
+ Args:
377
+ ax: Axes to interpolate data from.
378
+ state: Cursor state dictionary.
379
+
380
+ Returns:
381
+ Selection callback function.
382
+ """
350
383
 
351
384
  def onselect(xmin: float, xmax: float) -> None:
352
385
  state["x1"] = xmin
353
386
  state["x2"] = xmax
354
387
 
355
- # Get Y values at cursor positions
356
388
  for line in ax.get_lines():
357
- xdata = line.get_xdata()
358
- ydata = line.get_ydata()
359
- # Type narrowing: these return ArrayLike from Line2D
360
- xdata_arr = np.asarray(xdata)
361
- ydata_arr = np.asarray(ydata)
389
+ xdata_arr = np.asarray(line.get_xdata())
390
+ ydata_arr = np.asarray(line.get_ydata())
362
391
  if len(xdata_arr) > 0:
363
- # Interpolate Y at cursor positions
364
- y1_interp: float = float(np.interp(xmin, xdata_arr, ydata_arr))
365
- y2_interp: float = float(np.interp(xmax, xdata_arr, ydata_arr))
366
- state["y1"] = y1_interp
367
- state["y2"] = y2_interp
392
+ state["y1"] = float(np.interp(xmin, xdata_arr, ydata_arr))
393
+ state["y2"] = float(np.interp(xmax, xdata_arr, ydata_arr))
368
394
  break
369
395
 
370
- span = SpanSelector(
371
- ax,
372
- onselect,
373
- "horizontal",
374
- useblit=True,
375
- props={"alpha": 0.3, "facecolor": color},
376
- interactive=True,
377
- )
396
+ return onselect
397
+
398
+
399
+ def _create_measurement_getter(state: dict[str, float | None | Any]) -> Any:
400
+ """Create measurement getter function.
401
+
402
+ Args:
403
+ state: Cursor state dictionary.
404
+
405
+ Returns:
406
+ Function that returns CursorMeasurement or None.
407
+ """
378
408
 
379
409
  def get_measurement() -> CursorMeasurement | None:
380
410
  x1 = state["x1"]
@@ -406,11 +436,7 @@ def add_measurement_cursors(
406
436
  slope=delta_y / delta_x if delta_x != 0 else None,
407
437
  )
408
438
 
409
- return {
410
- "span": span,
411
- "state": state,
412
- "get_measurement": get_measurement,
413
- }
439
+ return get_measurement
414
440
 
415
441
 
416
442
  def plot_phase(
@@ -505,6 +531,7 @@ def plot_bode(
505
531
  *,
506
532
  magnitude_db: bool = True,
507
533
  phase_degrees: bool = True,
534
+ show_margins: bool = False,
508
535
  fig: Figure | None = None,
509
536
  **plot_kwargs: Any,
510
537
  ) -> Figure:
@@ -520,6 +547,7 @@ def plot_bode(
520
547
  phase: Phase array in radians (optional). Ignored if magnitude is complex.
521
548
  magnitude_db: If True, magnitude is already in dB. Ignored if complex input.
522
549
  phase_degrees: If True, convert phase to degrees.
550
+ show_margins: If True, annotate stability margins (currently unused, reserved for future).
523
551
  fig: Existing figure to plot on.
524
552
  **plot_kwargs: Additional arguments to plot().
525
553
 
@@ -649,35 +677,81 @@ def plot_waterfall(
649
677
  raise ImportError("matplotlib is required for interactive visualization")
650
678
 
651
679
  data = np.asarray(data)
680
+ Sxx_db, frequencies, times = _prepare_waterfall_data(
681
+ data, time_axis, freq_axis, sample_rate, nperseg, noverlap
682
+ )
683
+ fig, ax = _create_waterfall_figure(ax)
684
+ T, F = np.meshgrid(times, frequencies)
685
+ Sxx_db = _align_waterfall_dimensions(Sxx_db, T)
686
+ surf = _plot_waterfall_surface(ax, T, F, Sxx_db, cmap)
687
+ _format_waterfall_axes(ax, fig, surf)
688
+
689
+ return fig, ax
690
+
652
691
 
653
- # Check if data is 2D (precomputed spectrogram)
692
+ def _prepare_waterfall_data(
693
+ data: NDArray[np.floating[Any]],
694
+ time_axis: NDArray[np.floating[Any]] | None,
695
+ freq_axis: NDArray[np.floating[Any]] | None,
696
+ sample_rate: float,
697
+ nperseg: int,
698
+ noverlap: int | None,
699
+ ) -> tuple[NDArray[np.floating[Any]], NDArray[np.floating[Any]], NDArray[np.floating[Any]]]:
700
+ """Prepare spectrogram data for waterfall plot.
701
+
702
+ Args:
703
+ data: Input data array.
704
+ time_axis: Time axis.
705
+ freq_axis: Frequency axis.
706
+ sample_rate: Sample rate.
707
+ nperseg: Segment length.
708
+ noverlap: Overlap length.
709
+
710
+ Returns:
711
+ Tuple of (Sxx_db, frequencies, times).
712
+ """
654
713
  if data.ndim == 2:
655
- # Treat as precomputed spectrogram (n_traces, n_points)
656
714
  Sxx_db = data
657
715
  n_traces, n_points = data.shape
658
- frequencies = freq_axis if freq_axis is not None else np.arange(n_points)
659
- times = time_axis if time_axis is not None else np.arange(n_traces)
716
+ frequencies: NDArray[np.floating[Any]] = (
717
+ freq_axis if freq_axis is not None else np.arange(n_points, dtype=np.float64)
718
+ )
719
+ times: NDArray[np.floating[Any]] = (
720
+ time_axis if time_axis is not None else np.arange(n_traces, dtype=np.float64)
721
+ )
660
722
  elif freq_axis is not None:
661
- # 1D data with explicit freq_axis means precomputed
662
723
  Sxx_db = data
663
724
  frequencies = freq_axis
664
725
  times = (
665
726
  time_axis
666
727
  if time_axis is not None
667
- else np.arange(Sxx_db.shape[1] if Sxx_db.ndim > 1 else 1)
728
+ else np.arange(Sxx_db.shape[1] if Sxx_db.ndim > 1 else 1, dtype=np.float64)
668
729
  )
669
730
  else:
670
- # Compute spectrogram from 1D signal
671
731
  if noverlap is None:
672
732
  noverlap = nperseg // 2
673
-
674
- frequencies, times, Sxx = scipy_signal.spectrogram(
733
+ frequencies_raw, times_raw, Sxx = scipy_signal.spectrogram(
675
734
  data, fs=sample_rate, nperseg=nperseg, noverlap=noverlap
676
735
  )
736
+ frequencies = np.asarray(frequencies_raw, dtype=np.float64)
677
737
  Sxx_db = 10 * np.log10(Sxx + 1e-10)
678
- times = time_axis if time_axis is not None else np.arange(Sxx_db.shape[1])
738
+ times = time_axis if time_axis is not None else np.arange(Sxx_db.shape[1], dtype=np.float64)
679
739
 
680
- # Create 3D figure
740
+ return Sxx_db, frequencies, times
741
+
742
+
743
+ def _create_waterfall_figure(ax: Axes | None) -> tuple[Figure, Axes]:
744
+ """Create 3D figure for waterfall plot.
745
+
746
+ Args:
747
+ ax: Existing axes or None.
748
+
749
+ Returns:
750
+ Tuple of (figure, axes).
751
+
752
+ Raises:
753
+ ValueError: If axes has no figure.
754
+ """
681
755
  if ax is None:
682
756
  fig = plt.figure(figsize=(12, 8))
683
757
  ax = fig.add_subplot(111, projection="3d")
@@ -686,41 +760,71 @@ def plot_waterfall(
686
760
  if fig_temp is None:
687
761
  raise ValueError("Axes must have an associated figure")
688
762
  fig = cast("Figure", fig_temp)
763
+ return fig, ax
689
764
 
690
- # Create meshgrid
691
- T, F = np.meshgrid(times, frequencies)
692
765
 
693
- # Ensure Sxx_db matches meshgrid shape (n_frequencies, n_times)
766
+ def _align_waterfall_dimensions(
767
+ Sxx_db: NDArray[np.floating[Any]], T: NDArray[np.floating[Any]]
768
+ ) -> NDArray[np.floating[Any]]:
769
+ """Align spectrogram dimensions to match meshgrid.
770
+
771
+ Args:
772
+ Sxx_db: Spectrogram data.
773
+ T: Meshgrid time array.
774
+
775
+ Returns:
776
+ Aligned spectrogram.
777
+ """
694
778
  if Sxx_db.shape != T.shape:
695
779
  if Sxx_db.T.shape == T.shape:
696
780
  Sxx_db = Sxx_db.T
697
- # If still mismatched, the data dimensions may be incompatible
698
- # but we'll let plot_surface raise a more informative error
781
+ return Sxx_db
782
+
699
783
 
700
- # Plot surface
701
- # Type checking: ax must be a 3D axes at this point
784
+ def _plot_waterfall_surface(
785
+ ax: Axes,
786
+ T: NDArray[np.floating[Any]],
787
+ F: NDArray[np.floating[Any]],
788
+ Sxx_db: NDArray[np.floating[Any]],
789
+ cmap: str,
790
+ ) -> Any:
791
+ """Plot waterfall surface.
792
+
793
+ Args:
794
+ ax: 3D axes.
795
+ T: Time meshgrid.
796
+ F: Frequency meshgrid.
797
+ Sxx_db: Spectrogram data.
798
+ cmap: Colormap.
799
+
800
+ Returns:
801
+ Surface object.
802
+
803
+ Raises:
804
+ TypeError: If axes is not 3D.
805
+ """
702
806
  if not hasattr(ax, "plot_surface"):
703
807
  raise TypeError("Axes must be a 3D axes for waterfall plot")
704
- surf = ax.plot_surface( # type: ignore[attr-defined,union-attr]
705
- T,
706
- F,
707
- Sxx_db,
708
- cmap=cmap,
709
- linewidth=0,
710
- antialiased=True,
711
- alpha=0.8,
808
+ return cast("Any", ax).plot_surface(
809
+ T, F, Sxx_db, cmap=cmap, linewidth=0, antialiased=True, alpha=0.8
712
810
  )
713
811
 
812
+
813
+ def _format_waterfall_axes(ax: Axes, fig: Figure, surf: Any) -> None:
814
+ """Format waterfall plot axes.
815
+
816
+ Args:
817
+ ax: 3D axes.
818
+ fig: Figure object.
819
+ surf: Surface object.
820
+ """
714
821
  ax.set_xlabel("Time (s)")
715
822
  ax.set_ylabel("Frequency (Hz)")
716
823
  if hasattr(ax, "set_zlabel"):
717
- ax.set_zlabel("Power (dB)") # type: ignore[attr-defined]
824
+ ax.set_zlabel("Power (dB)")
718
825
  ax.set_title("Waterfall Plot (Spectrogram)")
719
-
720
826
  fig.colorbar(surf, ax=ax, label="Power (dB)", shrink=0.5)
721
827
 
722
- return fig, ax
723
-
724
828
 
725
829
  def plot_histogram(
726
830
  trace: WaveformTrace | NDArray[np.floating[Any]],
@@ -767,10 +871,30 @@ def plot_histogram(
767
871
  if not MATPLOTLIB_AVAILABLE:
768
872
  raise ImportError("matplotlib is required for interactive visualization")
769
873
 
770
- # Get data
771
874
  data = trace.data if isinstance(trace, WaveformTrace) else np.asarray(trace)
875
+ fig, ax = _setup_histogram_figure(ax)
876
+ stats = _calculate_histogram_statistics(data)
877
+ bin_edges = _plot_histogram_data(ax, data, bins, density, hist_kwargs)
878
+ stats["bins"] = len(bin_edges) - 1
879
+ _add_histogram_overlays(ax, data, stats, bin_edges, density, show_stats, show_kde)
880
+ _format_histogram_axes(ax, density, show_stats, show_kde)
881
+ _handle_histogram_output(fig, save_path, show)
772
882
 
773
- # Create figure if needed
883
+ return fig, ax, stats
884
+
885
+
886
+ def _setup_histogram_figure(ax: Axes | None) -> tuple[Figure, Axes]:
887
+ """Setup figure and axes for histogram.
888
+
889
+ Args:
890
+ ax: Existing axes or None.
891
+
892
+ Returns:
893
+ Tuple of (figure, axes).
894
+
895
+ Raises:
896
+ ValueError: If axes has no figure.
897
+ """
774
898
  if ax is None:
775
899
  fig, ax = plt.subplots(figsize=(10, 6))
776
900
  else:
@@ -778,72 +902,125 @@ def plot_histogram(
778
902
  if fig_temp is None:
779
903
  raise ValueError("Axes must have an associated figure")
780
904
  fig = cast("Figure", fig_temp)
905
+ return fig, ax
781
906
 
782
- # Calculate statistics
783
- mean = float(np.mean(data))
784
- std = float(np.std(data))
785
- median = float(np.median(data))
786
- min_val = float(np.min(data))
787
- max_val = float(np.max(data))
788
-
789
- stats = {
790
- "mean": mean,
791
- "std": std,
792
- "median": median,
793
- "min": min_val,
794
- "max": max_val,
907
+
908
+ def _calculate_histogram_statistics(data: NDArray[np.floating[Any]]) -> dict[str, Any]:
909
+ """Calculate histogram statistics.
910
+
911
+ Args:
912
+ data: Data array.
913
+
914
+ Returns:
915
+ Statistics dictionary.
916
+ """
917
+ return {
918
+ "mean": float(np.mean(data)),
919
+ "std": float(np.std(data)),
920
+ "median": float(np.median(data)),
921
+ "min": float(np.min(data)),
922
+ "max": float(np.max(data)),
795
923
  "count": len(data),
796
924
  }
797
925
 
798
- # Plot histogram
926
+
927
+ def _plot_histogram_data(
928
+ ax: Axes,
929
+ data: NDArray[np.floating[Any]],
930
+ bins: int | str | NDArray[np.floating[Any]],
931
+ density: bool,
932
+ hist_kwargs: dict[str, Any],
933
+ ) -> NDArray[Any]:
934
+ """Plot histogram data.
935
+
936
+ Args:
937
+ ax: Axes to plot on.
938
+ data: Data array.
939
+ bins: Bin specification.
940
+ density: Normalize to density.
941
+ hist_kwargs: Additional histogram arguments.
942
+
943
+ Returns:
944
+ Bin edges array.
945
+ """
799
946
  defaults: dict[str, Any] = {"alpha": 0.7, "edgecolor": "black", "linewidth": 0.5}
800
947
  defaults.update(hist_kwargs)
801
948
  _counts, bin_edges, _patches = ax.hist(data, bins=bins, density=density, **defaults) # type: ignore[arg-type]
949
+ return bin_edges
802
950
 
803
- stats["bins"] = len(bin_edges) - 1
804
951
 
805
- # Show statistics lines
952
+ def _add_histogram_overlays(
953
+ ax: Axes,
954
+ data: NDArray[np.floating[Any]],
955
+ stats: dict[str, Any],
956
+ bin_edges: NDArray[Any],
957
+ density: bool,
958
+ show_stats: bool,
959
+ show_kde: bool,
960
+ ) -> None:
961
+ """Add overlays to histogram.
962
+
963
+ Args:
964
+ ax: Axes object.
965
+ data: Data array.
966
+ stats: Statistics dict.
967
+ bin_edges: Bin edges.
968
+ density: Whether density normalized.
969
+ show_stats: Show statistics lines.
970
+ show_kde: Show KDE overlay.
971
+ """
806
972
  if show_stats:
807
- ax.get_ylim()
973
+ mean, std = stats["mean"], stats["std"]
808
974
  ax.axvline(mean, color="red", linestyle="--", linewidth=2, label=f"Mean: {mean:.3g}")
809
975
  ax.axvline(mean - std, color="orange", linestyle=":", linewidth=1.5, label="Mean - Std")
810
976
  ax.axvline(mean + std, color="orange", linestyle=":", linewidth=1.5, label="Mean + Std")
811
977
 
812
- # Show KDE
813
978
  if show_kde:
814
979
  from scipy.stats import gaussian_kde
815
980
 
816
981
  kde = gaussian_kde(data)
817
- x_kde = np.linspace(min_val, max_val, 200)
982
+ x_kde = np.linspace(stats["min"], stats["max"], 200)
818
983
  y_kde = kde(x_kde)
819
984
 
820
985
  if density:
821
986
  ax.plot(x_kde, y_kde, "r-", linewidth=2, label="KDE")
822
987
  else:
823
- # Scale KDE to histogram
824
988
  bin_width = bin_edges[1] - bin_edges[0]
825
989
  ax.plot(x_kde, y_kde * len(data) * bin_width, "r-", linewidth=2, label="KDE")
826
990
 
991
+
992
+ def _format_histogram_axes(ax: Axes, density: bool, show_stats: bool, show_kde: bool) -> None:
993
+ """Format histogram axes.
994
+
995
+ Args:
996
+ ax: Axes object.
997
+ density: Whether density normalized.
998
+ show_stats: Whether stats shown.
999
+ show_kde: Whether KDE shown.
1000
+ """
827
1001
  ax.set_xlabel("Amplitude")
828
1002
  ax.set_ylabel("Density" if density else "Count")
829
1003
  ax.set_title("Amplitude Distribution")
830
- # Only show legend if there are labeled artists
831
1004
  if show_stats or show_kde:
832
1005
  ax.legend(loc="upper right")
833
1006
  ax.grid(True, alpha=0.3)
834
1007
 
835
- # Save if requested
1008
+
1009
+ def _handle_histogram_output(fig: Figure, save_path: str | None, show: bool) -> None:
1010
+ """Handle histogram output.
1011
+
1012
+ Args:
1013
+ fig: Figure object.
1014
+ save_path: Save path.
1015
+ show: Whether to show.
1016
+ """
836
1017
  if save_path is not None:
837
1018
  fig.savefig(save_path, dpi=150, bbox_inches="tight")
838
-
839
- # Show or close
840
1019
  if show:
841
1020
  plt.show()
842
1021
  else:
843
1022
  plt.close(fig)
844
1023
 
845
- return fig, ax, stats
846
-
847
1024
 
848
1025
  __all__ = [
849
1026
  "CursorMeasurement",