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
@@ -0,0 +1,661 @@
1
+ """Comprehensive performance profiling for analysis workflows.
2
+
3
+ This module provides detailed profiling capabilities including CPU usage,
4
+ memory consumption, I/O operations, and bottleneck identification using
5
+ multiple profiling backends (cProfile, line_profiler, memory_profiler).
6
+
7
+ Example:
8
+ >>> profiler = PerformanceProfiler()
9
+ >>> profiler.start()
10
+ >>> # Run analysis code here
11
+ >>> result = profiler.stop()
12
+ >>> print(result.summary())
13
+ >>> result.export_json("profile.json")
14
+
15
+ References:
16
+ Python Profilers: https://docs.python.org/3/library/profile.html
17
+ IEEE 1685-2009: IP-XACT standard for profiling
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import cProfile
23
+ import functools
24
+ import io
25
+ import json
26
+ import logging
27
+ import pstats
28
+ import sys
29
+ import time
30
+ import tracemalloc
31
+ from contextlib import contextmanager
32
+ from dataclasses import asdict, dataclass, field
33
+ from enum import Enum
34
+ from pathlib import Path
35
+ from typing import TYPE_CHECKING, Any, TypeAlias
36
+
37
+ if TYPE_CHECKING:
38
+ from collections.abc import Callable, Iterator
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ __all__ = [
43
+ "FunctionStats",
44
+ "PerformanceProfiler",
45
+ "ProfilingMode",
46
+ "ProfilingResult",
47
+ ]
48
+
49
+ # Type aliases
50
+ HotspotDict: TypeAlias = dict[str, Any]
51
+ CallGraphDict: TypeAlias = dict[str, list[str]]
52
+
53
+
54
+ class ProfilingMode(Enum):
55
+ """Profiling mode selection.
56
+
57
+ Attributes:
58
+ FUNCTION: Function-level profiling (time per function, call counts)
59
+ LINE: Line-level profiling (time per line of code)
60
+ MEMORY: Memory profiling (allocation tracking, memory leaks)
61
+ IO: I/O profiling (disk reads/writes, network traffic)
62
+ FULL: All profiling modes combined
63
+ """
64
+
65
+ FUNCTION = "function"
66
+ LINE = "line"
67
+ MEMORY = "memory"
68
+ IO = "io"
69
+ FULL = "full"
70
+
71
+
72
+ @dataclass
73
+ class FunctionStats:
74
+ """Statistics for a profiled function.
75
+
76
+ Attributes:
77
+ name: Function name
78
+ calls: Number of times called
79
+ time: Total time spent in function (seconds)
80
+ cumulative_time: Cumulative time including sub-calls (seconds)
81
+ memory: Peak memory usage (bytes)
82
+ per_call_time: Average time per call (seconds)
83
+ per_call_memory: Average memory per call (bytes)
84
+ filename: Source file containing function
85
+ lineno: Line number where function is defined
86
+ """
87
+
88
+ name: str
89
+ calls: int
90
+ time: float
91
+ cumulative_time: float
92
+ memory: int = 0
93
+ per_call_time: float = 0.0
94
+ per_call_memory: int = 0
95
+ filename: str = ""
96
+ lineno: int = 0
97
+
98
+ def __post_init__(self) -> None:
99
+ """Calculate derived stats."""
100
+ if self.calls > 0:
101
+ self.per_call_time = self.time / self.calls
102
+ if self.memory > 0:
103
+ self.per_call_memory = self.memory // self.calls
104
+
105
+
106
+ @dataclass
107
+ class ProfilingResult:
108
+ """Complete profiling results.
109
+
110
+ Attributes:
111
+ function_stats: Statistics for all profiled functions
112
+ hotspots: Performance bottlenecks (top N slowest functions)
113
+ memory_stats: Memory usage statistics
114
+ call_graph: Function call hierarchy
115
+ total_time: Total execution time (seconds)
116
+ peak_memory: Peak memory usage (bytes)
117
+ mode: Profiling mode used
118
+ metadata: Additional metadata (timestamp, environment, etc.)
119
+ """
120
+
121
+ function_stats: dict[str, FunctionStats]
122
+ hotspots: list[HotspotDict]
123
+ memory_stats: dict[str, Any]
124
+ call_graph: CallGraphDict
125
+ total_time: float
126
+ peak_memory: int
127
+ mode: ProfilingMode
128
+ metadata: dict[str, Any] = field(default_factory=dict)
129
+
130
+ def summary(self) -> str:
131
+ """Generate text summary of profiling results.
132
+
133
+ Returns:
134
+ Human-readable summary string
135
+ """
136
+ lines = [
137
+ "=" * 80,
138
+ "Performance Profiling Report",
139
+ "=" * 80,
140
+ f"Mode: {self.mode.value}",
141
+ f"Total Time: {self.total_time:.4f}s",
142
+ f"Peak Memory: {self._format_bytes(self.peak_memory)}",
143
+ f"Functions Profiled: {len(self.function_stats)}",
144
+ "",
145
+ "Top 10 Hotspots (by cumulative time):",
146
+ "-" * 80,
147
+ ]
148
+
149
+ # Add hotspots
150
+ for i, hotspot in enumerate(self.hotspots[:10], 1):
151
+ func_name = hotspot["function"]
152
+ cum_time = hotspot["cumulative_time"]
153
+ calls = hotspot["calls"]
154
+ pct = hotspot["percent_time"]
155
+
156
+ lines.append(f"{i:2d}. {func_name:50s} {cum_time:8.4f}s ({calls:6d} calls) {pct:5.1f}%")
157
+
158
+ if self.mode == ProfilingMode.MEMORY or self.mode == ProfilingMode.FULL:
159
+ lines.extend(
160
+ [
161
+ "",
162
+ "Memory Statistics:",
163
+ "-" * 80,
164
+ f" Peak Usage: {self._format_bytes(self.memory_stats.get('peak', 0))}",
165
+ f" Current Usage: {self._format_bytes(self.memory_stats.get('current', 0))}",
166
+ f" Allocations: {self.memory_stats.get('allocations', 0):,}",
167
+ ]
168
+ )
169
+
170
+ lines.append("=" * 80)
171
+ return "\n".join(lines)
172
+
173
+ def export_json(self, filepath: str | Path) -> None:
174
+ """Export profiling results to JSON file.
175
+
176
+ Args:
177
+ filepath: Output file path
178
+ """
179
+ filepath = Path(filepath)
180
+
181
+ # Convert to serializable format
182
+ data = {
183
+ "function_stats": {name: asdict(stats) for name, stats in self.function_stats.items()},
184
+ "hotspots": self.hotspots,
185
+ "memory_stats": self.memory_stats,
186
+ "call_graph": self.call_graph,
187
+ "total_time": self.total_time,
188
+ "peak_memory": self.peak_memory,
189
+ "mode": self.mode.value,
190
+ "metadata": self.metadata,
191
+ }
192
+
193
+ with filepath.open("w") as f:
194
+ json.dump(data, f, indent=2)
195
+
196
+ logger.info(f"Profiling results exported to {filepath}")
197
+
198
+ def export_html(self, filepath: str | Path) -> None:
199
+ """Export profiling results to HTML file with flame graph.
200
+
201
+ Args:
202
+ filepath: Output file path
203
+ """
204
+ filepath = Path(filepath)
205
+
206
+ html = self._generate_html()
207
+
208
+ with filepath.open("w") as f:
209
+ f.write(html)
210
+
211
+ logger.info(f"HTML report exported to {filepath}")
212
+
213
+ def export_text(self, filepath: str | Path) -> None:
214
+ """Export profiling results to text file.
215
+
216
+ Args:
217
+ filepath: Output file path
218
+ """
219
+ filepath = Path(filepath)
220
+
221
+ with filepath.open("w") as f:
222
+ f.write(self.summary())
223
+
224
+ logger.info(f"Text report exported to {filepath}")
225
+
226
+ def _format_bytes(self, bytes_value: int) -> str:
227
+ """Format bytes as human-readable string.
228
+
229
+ Args:
230
+ bytes_value: Number of bytes
231
+
232
+ Returns:
233
+ Formatted string (e.g., "10.5 MB")
234
+ """
235
+ if bytes_value == 0:
236
+ return "0 B"
237
+
238
+ units = ["B", "KB", "MB", "GB", "TB"]
239
+ unit_index = 0
240
+ value = float(bytes_value)
241
+
242
+ while value >= 1024.0 and unit_index < len(units) - 1:
243
+ value /= 1024.0
244
+ unit_index += 1
245
+
246
+ return f"{value:.2f} {units[unit_index]}"
247
+
248
+ def _generate_html(self) -> str:
249
+ """Generate HTML report with styling.
250
+
251
+ Returns:
252
+ HTML string
253
+ """
254
+ html = f"""<!DOCTYPE html>
255
+ <html>
256
+ <head>
257
+ <title>Performance Profiling Report</title>
258
+ <style>
259
+ body {{ font-family: 'Courier New', monospace; margin: 20px; background: #1e1e1e; color: #d4d4d4; }}
260
+ h1 {{ color: #4ec9b0; }}
261
+ h2 {{ color: #569cd6; margin-top: 30px; }}
262
+ table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
263
+ th {{ background: #2d2d30; color: #4ec9b0; padding: 10px; text-align: left; }}
264
+ td {{ padding: 8px; border-bottom: 1px solid #3e3e42; }}
265
+ .hotspot {{ background: #3c2929; }}
266
+ .stats {{ margin: 10px 0; padding: 10px; background: #2d2d30; border-radius: 5px; }}
267
+ .metric {{ display: inline-block; margin: 10px 20px 10px 0; }}
268
+ .value {{ color: #ce9178; font-weight: bold; }}
269
+ </style>
270
+ </head>
271
+ <body>
272
+ <h1>Performance Profiling Report</h1>
273
+
274
+ <div class="stats">
275
+ <div class="metric">Mode: <span class="value">{self.mode.value}</span></div>
276
+ <div class="metric">Total Time: <span class="value">{self.total_time:.4f}s</span></div>
277
+ <div class="metric">Peak Memory: <span class="value">{self._format_bytes(self.peak_memory)}</span></div>
278
+ <div class="metric">Functions: <span class="value">{len(self.function_stats)}</span></div>
279
+ </div>
280
+
281
+ <h2>Top Hotspots</h2>
282
+ <table>
283
+ <tr>
284
+ <th>Rank</th>
285
+ <th>Function</th>
286
+ <th>Cumulative Time</th>
287
+ <th>Calls</th>
288
+ <th>% of Total</th>
289
+ </tr>
290
+ """
291
+
292
+ for i, hotspot in enumerate(self.hotspots[:20], 1):
293
+ css_class = "hotspot" if i <= 5 else ""
294
+ html += f""" <tr class="{css_class}">
295
+ <td>{i}</td>
296
+ <td>{hotspot["function"]}</td>
297
+ <td>{hotspot["cumulative_time"]:.4f}s</td>
298
+ <td>{hotspot["calls"]}</td>
299
+ <td>{hotspot["percent_time"]:.1f}%</td>
300
+ </tr>
301
+ """
302
+
303
+ html += """ </table>
304
+ </body>
305
+ </html>
306
+ """
307
+ return html
308
+
309
+
310
+ class PerformanceProfiler:
311
+ """Comprehensive performance profiler for analysis workflows.
312
+
313
+ Profiles CPU usage, memory consumption, I/O operations, and identifies
314
+ performance bottlenecks using multiple profiling backends.
315
+
316
+ Example:
317
+ Basic usage::
318
+
319
+ profiler = PerformanceProfiler()
320
+ profiler.start()
321
+ # Run analysis code
322
+ result = profiler.stop()
323
+ print(result.summary())
324
+
325
+ Context manager::
326
+
327
+ with PerformanceProfiler() as profiler:
328
+ # Run analysis code
329
+ pass
330
+ result = profiler.get_results()
331
+
332
+ Decorator::
333
+
334
+ @PerformanceProfiler.profile_function()
335
+ def my_analysis_function(data):
336
+ # Analysis code
337
+ pass
338
+
339
+ References:
340
+ Python cProfile: https://docs.python.org/3/library/profile.html
341
+ tracemalloc: https://docs.python.org/3/library/tracemalloc.html
342
+ """
343
+
344
+ def __init__(self, mode: ProfilingMode = ProfilingMode.FUNCTION) -> None:
345
+ """Initialize profiler.
346
+
347
+ Args:
348
+ mode: Profiling mode (FUNCTION, LINE, MEMORY, IO, FULL)
349
+ """
350
+ self.mode = mode
351
+ self._profiler: cProfile.Profile | None = None
352
+ self._start_time: float = 0.0
353
+ self._end_time: float = 0.0
354
+ self._memory_start: tuple[int, int] = (0, 0)
355
+ self._memory_peak: int = 0
356
+ self._result: ProfilingResult | None = None
357
+ self._is_running: bool = False
358
+
359
+ # Check optional dependencies
360
+ self._line_profiler_available = self._check_line_profiler()
361
+ self._memory_profiler_available = self._check_memory_profiler()
362
+
363
+ def _check_line_profiler(self) -> bool:
364
+ """Check if line_profiler is available.
365
+
366
+ Returns:
367
+ True if line_profiler is installed
368
+ """
369
+ try:
370
+ import line_profiler # type: ignore[import-not-found] # noqa: F401
371
+
372
+ return True
373
+ except ImportError:
374
+ if self.mode == ProfilingMode.LINE:
375
+ logger.warning(
376
+ "line_profiler not installed. Install with: pip install line_profiler"
377
+ )
378
+ return False
379
+
380
+ def _check_memory_profiler(self) -> bool:
381
+ """Check if memory_profiler is available.
382
+
383
+ Returns:
384
+ True if memory_profiler is installed
385
+ """
386
+ try:
387
+ import memory_profiler # type: ignore[import-not-found] # noqa: F401
388
+
389
+ return True
390
+ except ImportError:
391
+ if self.mode == ProfilingMode.MEMORY:
392
+ logger.info("memory_profiler not installed (optional). Using tracemalloc instead.")
393
+ return False
394
+
395
+ def start(self) -> None:
396
+ """Start profiling."""
397
+ if self._is_running:
398
+ logger.warning("Profiler is already running")
399
+ return
400
+
401
+ self._start_time = time.perf_counter()
402
+ self._is_running = True
403
+
404
+ # Start appropriate profiling backend
405
+ if self.mode in (ProfilingMode.FUNCTION, ProfilingMode.FULL):
406
+ self._profiler = cProfile.Profile()
407
+ try:
408
+ self._profiler.enable()
409
+ except ValueError as e:
410
+ # Handle nested profiler case
411
+ if "Another profiling tool is already active" in str(e):
412
+ logger.warning(
413
+ "Another profiler is active. This profiler will not collect detailed statistics."
414
+ )
415
+ self._profiler = None
416
+ else:
417
+ raise
418
+
419
+ if self.mode in (ProfilingMode.MEMORY, ProfilingMode.FULL):
420
+ if not tracemalloc.is_tracing():
421
+ tracemalloc.start()
422
+ self._memory_start = tracemalloc.get_traced_memory()
423
+
424
+ logger.info(f"Profiling started in {self.mode.value} mode")
425
+
426
+ def stop(self) -> ProfilingResult:
427
+ """Stop profiling and generate results.
428
+
429
+ Returns:
430
+ Profiling results
431
+
432
+ Raises:
433
+ RuntimeError: If profiler is not running
434
+ """
435
+ if not self._is_running:
436
+ raise RuntimeError("Profiler is not running. Call start() first.")
437
+
438
+ self._end_time = time.perf_counter()
439
+ total_time = self._end_time - self._start_time
440
+
441
+ # Stop profiling backends
442
+ if self._profiler is not None:
443
+ self._profiler.disable()
444
+
445
+ memory_stats: dict[str, Any] = {}
446
+ if self.mode in (ProfilingMode.MEMORY, ProfilingMode.FULL):
447
+ current, peak = tracemalloc.get_traced_memory()
448
+ self._memory_peak = peak
449
+ memory_stats = {
450
+ "current": current,
451
+ "peak": peak,
452
+ "allocations": tracemalloc.get_tracemalloc_memory(),
453
+ }
454
+ tracemalloc.stop()
455
+
456
+ # Extract statistics
457
+ function_stats = self._extract_function_stats()
458
+ hotspots = self._identify_hotspots(function_stats, total_time)
459
+ call_graph = self._build_call_graph()
460
+
461
+ # Create result
462
+ self._result = ProfilingResult(
463
+ function_stats=function_stats,
464
+ hotspots=hotspots,
465
+ memory_stats=memory_stats,
466
+ call_graph=call_graph,
467
+ total_time=total_time,
468
+ peak_memory=self._memory_peak,
469
+ mode=self.mode,
470
+ metadata={
471
+ "python_version": sys.version,
472
+ "platform": sys.platform,
473
+ "timestamp": time.time(),
474
+ },
475
+ )
476
+
477
+ self._is_running = False
478
+ logger.info(f"Profiling stopped. Total time: {total_time:.4f}s")
479
+
480
+ return self._result
481
+
482
+ def get_results(self) -> ProfilingResult | None:
483
+ """Get profiling results without stopping.
484
+
485
+ Returns:
486
+ Profiling results or None if not available
487
+ """
488
+ return self._result
489
+
490
+ @contextmanager
491
+ def profile(self) -> Iterator[PerformanceProfiler]:
492
+ """Context manager for profiling code blocks.
493
+
494
+ Yields:
495
+ PerformanceProfiler instance
496
+
497
+ Example:
498
+ >>> with PerformanceProfiler() as profiler:
499
+ ... # Code to profile
500
+ ... pass
501
+ >>> result = profiler.get_results()
502
+ """
503
+ self.start()
504
+ try:
505
+ yield self
506
+ finally:
507
+ self.stop()
508
+
509
+ def __enter__(self) -> PerformanceProfiler:
510
+ """Enter context manager."""
511
+ self.start()
512
+ return self
513
+
514
+ def __exit__(self, exc_type: type, exc_val: Exception, exc_tb: Any) -> None:
515
+ """Exit context manager."""
516
+ if self._is_running:
517
+ self.stop()
518
+
519
+ @staticmethod
520
+ def profile_function(
521
+ mode: ProfilingMode = ProfilingMode.FUNCTION,
522
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
523
+ """Decorator for profiling individual functions.
524
+
525
+ Args:
526
+ mode: Profiling mode
527
+
528
+ Returns:
529
+ Decorator function
530
+
531
+ Example:
532
+ >>> @PerformanceProfiler.profile_function()
533
+ >>> def my_function(x):
534
+ ... return x * 2
535
+ """
536
+
537
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
538
+ @functools.wraps(func)
539
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
540
+ profiler = PerformanceProfiler(mode=mode)
541
+ profiler.start()
542
+ try:
543
+ result = func(*args, **kwargs)
544
+ return result
545
+ finally:
546
+ profiling_result = profiler.stop()
547
+ logger.info(
548
+ f"\nProfiling results for {func.__name__}:\n{profiling_result.summary()}"
549
+ )
550
+
551
+ return wrapper
552
+
553
+ return decorator
554
+
555
+ def _extract_function_stats(self) -> dict[str, FunctionStats]:
556
+ """Extract function statistics from profiler.
557
+
558
+ Returns:
559
+ Dictionary mapping function names to their statistics
560
+ """
561
+ if self._profiler is None:
562
+ return {}
563
+
564
+ # Get statistics from cProfile
565
+ stats_stream = io.StringIO()
566
+ stats = pstats.Stats(self._profiler, stream=stats_stream)
567
+ stats.sort_stats(pstats.SortKey.CUMULATIVE)
568
+
569
+ function_stats: dict[str, FunctionStats] = {}
570
+
571
+ for func_key, (_cc, nc, tt, ct, _callers) in stats.stats.items(): # type: ignore[attr-defined]
572
+ filename, lineno, func_name = func_key
573
+
574
+ # Create readable function name
575
+ if filename == "~":
576
+ full_name = func_name
577
+ else:
578
+ full_name = f"{Path(filename).name}:{func_name}"
579
+
580
+ function_stats[full_name] = FunctionStats(
581
+ name=full_name,
582
+ calls=nc,
583
+ time=tt,
584
+ cumulative_time=ct,
585
+ filename=filename,
586
+ lineno=lineno,
587
+ )
588
+
589
+ return function_stats
590
+
591
+ def _identify_hotspots(
592
+ self, function_stats: dict[str, FunctionStats], total_time: float
593
+ ) -> list[HotspotDict]:
594
+ """Identify performance hotspots.
595
+
596
+ Args:
597
+ function_stats: Function statistics
598
+ total_time: Total execution time
599
+
600
+ Returns:
601
+ List of hotspot dictionaries sorted by cumulative time
602
+ """
603
+ hotspots: list[HotspotDict] = []
604
+
605
+ for func_name, stats in function_stats.items():
606
+ percent_time = (stats.cumulative_time / total_time * 100.0) if total_time > 0 else 0.0
607
+
608
+ hotspots.append(
609
+ {
610
+ "function": func_name,
611
+ "calls": stats.calls,
612
+ "time": stats.time,
613
+ "cumulative_time": stats.cumulative_time,
614
+ "percent_time": percent_time,
615
+ "per_call_time": stats.per_call_time,
616
+ }
617
+ )
618
+
619
+ # Sort by cumulative time (descending)
620
+ hotspots.sort(key=lambda x: x["cumulative_time"], reverse=True)
621
+
622
+ return hotspots
623
+
624
+ def _build_call_graph(self) -> CallGraphDict:
625
+ """Build function call graph.
626
+
627
+ Returns:
628
+ Dictionary mapping functions to list of called functions
629
+ """
630
+ if self._profiler is None:
631
+ return {}
632
+
633
+ call_graph: CallGraphDict = {}
634
+
635
+ stats = pstats.Stats(self._profiler)
636
+
637
+ for func_key in stats.stats: # type: ignore[attr-defined]
638
+ filename, lineno, func_name = func_key
639
+
640
+ # Create readable function name
641
+ if filename == "~":
642
+ full_name = func_name
643
+ else:
644
+ full_name = f"{Path(filename).name}:{func_name}"
645
+
646
+ # Get callees (functions called by this function)
647
+ # Note: all_callees may not be available in all Python versions
648
+ callees: list[str] = []
649
+ all_callees = getattr(stats, "all_callees", None)
650
+ if all_callees is not None and func_key in all_callees:
651
+ for callee_key in all_callees[func_key]:
652
+ callee_filename, callee_lineno, callee_name = callee_key
653
+ if callee_filename == "~":
654
+ callee_full = callee_name
655
+ else:
656
+ callee_full = f"{Path(callee_filename).name}:{callee_name}"
657
+ callees.append(callee_full)
658
+
659
+ call_graph[full_name] = callees
660
+
661
+ return call_graph
@@ -10,7 +10,7 @@ from abc import ABC, abstractmethod
10
10
  from typing import TYPE_CHECKING, Any
11
11
 
12
12
  if TYPE_CHECKING:
13
- from ..core.types import WaveformTrace
13
+ from oscura.core.types import WaveformTrace
14
14
 
15
15
 
16
16
  class TraceTransformer(ABC):
@@ -8,7 +8,7 @@ from collections.abc import Callable
8
8
  from functools import reduce, wraps
9
9
  from typing import Any, TypeVar
10
10
 
11
- from ..core.types import WaveformTrace
11
+ from oscura.core.types import WaveformTrace
12
12
 
13
13
  # Type variables for generic composition
14
14
  T = TypeVar("T")
@@ -60,8 +60,16 @@ def compose(*funcs: TraceFunc) -> TraceFunc:
60
60
  # Apply functions in reverse order (right to left)
61
61
  return reduce(lambda val, func: func(val), reversed(funcs), x)
62
62
 
63
- # Preserve function metadata
64
- composed.__name__ = "compose(" + ", ".join(f.__name__ for f in funcs) + ")"
63
+ # Preserve function metadata (handle functools.partial which lacks __name__)
64
+ func_names = []
65
+ for f in funcs:
66
+ if hasattr(f, "__name__"):
67
+ func_names.append(f.__name__)
68
+ elif hasattr(f, "func"): # functools.partial
69
+ func_names.append(f.func.__name__)
70
+ else:
71
+ func_names.append(repr(f))
72
+ composed.__name__ = "compose(" + ", ".join(func_names) + ")"
65
73
  composed.__doc__ = f"Composition of {len(funcs)} functions"
66
74
 
67
75
  return composed