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,654 @@
1
+ """Parallel processing for multi-core hardware analysis.
2
+
3
+ This module provides parallelization strategies for CPU-bound and I/O-bound tasks
4
+ in hardware reverse engineering workflows. It enables efficient processing of
5
+ multiple signals, protocols, files, and analysis operations across CPU cores.
6
+
7
+ Key capabilities:
8
+ - Process pool for CPU-intensive analysis (FFT, correlation, protocol decoding)
9
+ - Thread pool for I/O-bound operations (file loading, network requests)
10
+ - Batch processing (multiple files in parallel)
11
+ - Pipeline parallelism (different stages running concurrently)
12
+ - Data parallelism (split large datasets across workers)
13
+ - Automatic worker count based on CPU topology
14
+ - Progress tracking with tqdm integration
15
+ - Graceful error handling in worker processes
16
+
17
+ Typical use cases:
18
+ - Decode multiple protocol messages simultaneously
19
+ - Parallel FFT/spectral analysis on signal chunks
20
+ - Load multiple capture files at once
21
+ - Generate multiple export formats (Wireshark, Scapy, Kaitai) concurrently
22
+ - Batch CRC recovery across message sets
23
+ - Parallel side-channel analysis on traces
24
+
25
+ Performance expectations:
26
+ - CPU-bound tasks: ~(N-1)x speedup on N cores
27
+ - I/O-bound tasks: ~2-4x speedup with threading
28
+ - Mixed workloads: Automatic strategy selection
29
+
30
+ Example:
31
+ >>> from oscura.utils.performance.parallel import ParallelProcessor, ParallelConfig
32
+ >>> # Configure parallel processor
33
+ >>> config = ParallelConfig(num_workers=4, strategy="process")
34
+ >>> processor = ParallelProcessor(config)
35
+ >>>
36
+ >>> # Parallel protocol decoding
37
+ >>> messages = [...] # List of message bytes
38
+ >>> def decode_message(msg):
39
+ ... return protocol_decoder.decode(msg)
40
+ >>> result = processor.map(decode_message, messages)
41
+ >>> print(f"Decoded {len(result.results)} messages in {result.execution_time:.2f}s")
42
+ >>> print(f"Speedup: {result.speedup:.2f}x")
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import logging
48
+ import multiprocessing as mp
49
+ import time
50
+ from collections.abc import Callable
51
+ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
52
+ from dataclasses import dataclass, field
53
+ from typing import TYPE_CHECKING, Any, Literal
54
+
55
+ if TYPE_CHECKING:
56
+ from collections.abc import Iterable, Sequence
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+ # Strategy types for parallelization
61
+ StrategyType = Literal["process", "thread", "auto"]
62
+
63
+
64
+ @dataclass
65
+ class ParallelConfig:
66
+ """Configuration for parallel processing.
67
+
68
+ Attributes:
69
+ num_workers: Number of worker processes/threads. If None, uses CPU count.
70
+ For process strategy: Defaults to cpu_count() - 1 (leave one core free)
71
+ For thread strategy: Defaults to cpu_count() * 2 (I/O-bound tasks)
72
+ strategy: Parallelization strategy.
73
+ "process": Use multiprocessing (CPU-bound tasks like FFT, correlation)
74
+ "thread": Use threading (I/O-bound tasks like file loading)
75
+ "auto": Automatically select based on task characteristics
76
+ batch_size: Number of items per worker batch. If None, auto-calculated.
77
+ Larger batches reduce overhead but may cause load imbalance.
78
+ Smaller batches improve load balancing but increase overhead.
79
+ show_progress: Enable tqdm progress bar for long-running operations.
80
+ timeout: Maximum execution time per task in seconds. None for no timeout.
81
+
82
+ Example:
83
+ >>> # CPU-bound tasks (protocol decoding, FFT)
84
+ >>> config = ParallelConfig(num_workers=4, strategy="process")
85
+ >>>
86
+ >>> # I/O-bound tasks (file loading)
87
+ >>> config = ParallelConfig(num_workers=8, strategy="thread")
88
+ >>>
89
+ >>> # Auto-detect strategy
90
+ >>> config = ParallelConfig(strategy="auto")
91
+ """
92
+
93
+ num_workers: int | None = None
94
+ strategy: StrategyType = "auto"
95
+ batch_size: int | None = None
96
+ show_progress: bool = False
97
+ timeout: float | None = None
98
+
99
+ def __post_init__(self) -> None:
100
+ """Validate configuration and set defaults."""
101
+ if self.num_workers is not None and self.num_workers < 1:
102
+ raise ValueError(f"num_workers must be positive, got {self.num_workers}")
103
+
104
+ if self.batch_size is not None and self.batch_size < 1:
105
+ raise ValueError(f"batch_size must be positive, got {self.batch_size}")
106
+
107
+ if self.timeout is not None and self.timeout <= 0:
108
+ raise ValueError(f"timeout must be positive, got {self.timeout}")
109
+
110
+
111
+ @dataclass
112
+ class WorkerStats:
113
+ """Statistics for individual worker performance.
114
+
115
+ Attributes:
116
+ worker_id: Worker identifier (0-indexed).
117
+ tasks_completed: Number of tasks processed by this worker.
118
+ execution_time: Total execution time for this worker in seconds.
119
+ errors: Number of errors encountered by this worker.
120
+ """
121
+
122
+ worker_id: int
123
+ tasks_completed: int
124
+ execution_time: float
125
+ errors: int
126
+
127
+
128
+ @dataclass
129
+ class ParallelResult:
130
+ """Results from parallel processing operation.
131
+
132
+ Attributes:
133
+ results: List of results from each task (same order as input).
134
+ execution_time: Total wall-clock time in seconds.
135
+ speedup: Speedup factor vs sequential execution (estimated or measured).
136
+ worker_stats: Per-worker performance statistics.
137
+ num_workers: Number of workers used.
138
+ strategy: Parallelization strategy used ("process" or "thread").
139
+ errors: List of (index, exception) tuples for failed tasks.
140
+
141
+ Example:
142
+ >>> result = processor.map(decode_fn, messages)
143
+ >>> print(f"Processed {len(result.results)} items in {result.execution_time:.2f}s")
144
+ >>> print(f"Speedup: {result.speedup:.2f}x vs sequential")
145
+ >>> print(f"Workers: {result.num_workers}")
146
+ >>> if result.errors:
147
+ ... print(f"Failed tasks: {len(result.errors)}")
148
+ """
149
+
150
+ results: list[Any]
151
+ execution_time: float
152
+ speedup: float
153
+ worker_stats: list[WorkerStats] = field(default_factory=list)
154
+ num_workers: int = 1
155
+ strategy: str = "sequential"
156
+ errors: list[tuple[int, Exception]] = field(default_factory=list)
157
+
158
+
159
+ class ParallelProcessor:
160
+ """Parallel processing manager for hardware analysis tasks.
161
+
162
+ This class provides high-level parallelization APIs for common hardware
163
+ reverse engineering workflows. It automatically manages worker pools,
164
+ distributes work efficiently, aggregates results, and tracks progress.
165
+
166
+ Supported patterns:
167
+ - map: Apply function to each item independently (embarrassingly parallel)
168
+ - batch_process: Process batches of items with custom aggregation
169
+ - pipeline: Chain multiple processing stages with different strategies
170
+
171
+ Worker management:
172
+ - Automatic worker count based on CPU topology
173
+ - Process pools for CPU-bound tasks (bypass GIL)
174
+ - Thread pools for I/O-bound tasks
175
+ - Graceful error handling (tasks fail independently)
176
+ - Timeout support for long-running tasks
177
+
178
+ Example:
179
+ >>> from oscura.utils.performance.parallel import ParallelProcessor, ParallelConfig
180
+ >>> # CPU-bound: Parallel FFT analysis
181
+ >>> config = ParallelConfig(num_workers=4, strategy="process")
182
+ >>> processor = ParallelProcessor(config)
183
+ >>> signals = [...] # List of signal arrays
184
+ >>> result = processor.map(compute_fft, signals)
185
+ >>>
186
+ >>> # I/O-bound: Parallel file loading
187
+ >>> config = ParallelConfig(num_workers=8, strategy="thread")
188
+ >>> processor = ParallelProcessor(config)
189
+ >>> files = [Path("file1.wfm"), Path("file2.wfm")]
190
+ >>> result = processor.map(load_waveform, files)
191
+ >>>
192
+ >>> # Batch processing with custom aggregation
193
+ >>> def process_batch(messages):
194
+ ... return analyze_protocol_batch(messages)
195
+ >>> result = processor.batch_process(all_messages, batch_fn=process_batch)
196
+ """
197
+
198
+ def __init__(self, config: ParallelConfig | None = None) -> None:
199
+ """Initialize parallel processor with configuration.
200
+
201
+ Args:
202
+ config: Parallel processing configuration. If None, uses defaults.
203
+
204
+ Example:
205
+ >>> # Default configuration (auto-detect strategy)
206
+ >>> processor = ParallelProcessor()
207
+ >>>
208
+ >>> # Custom configuration
209
+ >>> config = ParallelConfig(num_workers=4, strategy="process")
210
+ >>> processor = ParallelProcessor(config)
211
+ """
212
+ self.config = config or ParallelConfig()
213
+ self._cpu_count = mp.cpu_count()
214
+
215
+ logger.debug(
216
+ f"ParallelProcessor initialized: strategy={self.config.strategy}, "
217
+ f"workers={self._get_worker_count()}, cpus={self._cpu_count}"
218
+ )
219
+
220
+ def map(
221
+ self,
222
+ func: Callable[[Any], Any],
223
+ items: Sequence[Any],
224
+ sequential_time: float | None = None,
225
+ ) -> ParallelResult:
226
+ """Apply function to each item in parallel.
227
+
228
+ This is the primary method for embarrassingly parallel tasks where
229
+ each item can be processed independently without shared state.
230
+
231
+ Args:
232
+ func: Function to apply to each item. Must be picklable for process strategy.
233
+ items: Sequence of items to process.
234
+ sequential_time: Optional baseline sequential execution time for
235
+ accurate speedup calculation. If None, speedup is estimated.
236
+
237
+ Returns:
238
+ ParallelResult with results, timing, and worker statistics.
239
+
240
+ Raises:
241
+ ValueError: If items is empty.
242
+
243
+ Example:
244
+ >>> # Parallel protocol decoding
245
+ >>> def decode(msg):
246
+ ... return protocol.decode(msg)
247
+ >>> messages = [b"\\x01\\x02", b"\\x03\\x04", b"\\x05\\x06"]
248
+ >>> result = processor.map(decode, messages)
249
+ >>> decoded = result.results
250
+ >>>
251
+ >>> # With progress tracking
252
+ >>> config = ParallelConfig(show_progress=True)
253
+ >>> processor = ParallelProcessor(config)
254
+ >>> result = processor.map(expensive_function, large_dataset)
255
+ """
256
+ if not items:
257
+ raise ValueError("Cannot process empty item list")
258
+
259
+ start_time = time.time()
260
+
261
+ # Determine strategy
262
+ strategy = self._resolve_strategy(func, items)
263
+ num_workers = self._get_worker_count(strategy)
264
+
265
+ logger.info(
266
+ f"Starting parallel map: {len(items)} items, {num_workers} workers, strategy={strategy}"
267
+ )
268
+
269
+ # Execute based on strategy
270
+ if strategy == "process":
271
+ results, errors = self._map_process(func, items, num_workers)
272
+ elif strategy == "thread":
273
+ results, errors = self._map_thread(func, items, num_workers)
274
+ else:
275
+ # Sequential fallback
276
+ results, errors = self._map_sequential(func, items)
277
+ num_workers = 1
278
+ strategy = "sequential"
279
+
280
+ execution_time = time.time() - start_time
281
+
282
+ # Calculate speedup
283
+ if sequential_time is not None:
284
+ speedup = sequential_time / execution_time
285
+ else:
286
+ # Estimate speedup (assumes linear scaling with slight overhead)
287
+ if strategy == "process":
288
+ speedup = min(num_workers * 0.85, num_workers) # 15% overhead
289
+ elif strategy == "thread":
290
+ speedup = min(num_workers * 0.6, 4.0) # I/O-bound limited to ~4x
291
+ else:
292
+ speedup = 1.0
293
+
294
+ logger.info(
295
+ f"Parallel map completed: {len(results)} results, "
296
+ f"time={execution_time:.2f}s, speedup={speedup:.2f}x, "
297
+ f"errors={len(errors)}"
298
+ )
299
+
300
+ return ParallelResult(
301
+ results=results,
302
+ execution_time=execution_time,
303
+ speedup=speedup,
304
+ num_workers=num_workers,
305
+ strategy=strategy,
306
+ errors=errors,
307
+ )
308
+
309
+ def batch_process(
310
+ self,
311
+ items: Sequence[Any],
312
+ batch_fn: Callable[[Sequence[Any]], Any],
313
+ batch_size: int | None = None,
314
+ ) -> ParallelResult:
315
+ """Process items in batches across workers.
316
+
317
+ Useful when:
318
+ - Items should be grouped for efficiency (e.g., database bulk inserts)
319
+ - Batch-level aggregation is needed (e.g., statistical analysis)
320
+ - Setup/teardown cost is high (e.g., model loading)
321
+
322
+ Args:
323
+ items: Sequence of items to process.
324
+ batch_fn: Function that processes a batch of items and returns result.
325
+ batch_size: Items per batch. If None, auto-calculated based on config.
326
+
327
+ Returns:
328
+ ParallelResult with batch results (one per batch).
329
+
330
+ Raises:
331
+ ValueError: If items is empty.
332
+
333
+ Example:
334
+ >>> # Batch CRC recovery
335
+ >>> def recover_batch_crc(messages):
336
+ ... return crc_reverser.analyze_batch(messages)
337
+ >>> all_messages = [...] # 10000 messages
338
+ >>> result = processor.batch_process(
339
+ ... all_messages,
340
+ ... batch_fn=recover_batch_crc,
341
+ ... batch_size=100
342
+ ... )
343
+ >>> # Result contains 100 batch results (100 messages each)
344
+ """
345
+ if not items:
346
+ raise ValueError("Cannot process empty item list")
347
+
348
+ # Determine batch size
349
+ if batch_size is None:
350
+ batch_size = self._calculate_batch_size(len(items))
351
+
352
+ # Create batches
353
+ batches = [items[i : i + batch_size] for i in range(0, len(items), batch_size)]
354
+
355
+ logger.info(
356
+ f"Starting batch processing: {len(items)} items, "
357
+ f"{len(batches)} batches of size ~{batch_size}"
358
+ )
359
+
360
+ # Process batches in parallel
361
+ return self.map(batch_fn, batches)
362
+
363
+ def pipeline(
364
+ self,
365
+ stages: Sequence[tuple[Callable[[Any], Any], StrategyType]],
366
+ items: Sequence[Any],
367
+ ) -> ParallelResult:
368
+ """Execute multi-stage pipeline with per-stage parallelization.
369
+
370
+ Each stage can use a different parallelization strategy based on
371
+ whether it's CPU-bound or I/O-bound. Results flow from one stage
372
+ to the next.
373
+
374
+ Args:
375
+ stages: Sequence of (function, strategy) tuples defining pipeline.
376
+ Each function receives output from previous stage.
377
+ items: Initial items to process.
378
+
379
+ Returns:
380
+ ParallelResult from final stage.
381
+
382
+ Raises:
383
+ ValueError: If stages or items are empty.
384
+
385
+ Example:
386
+ >>> # Multi-stage analysis pipeline
387
+ >>> stages = [
388
+ ... (load_signal, "thread"), # I/O-bound
389
+ ... (compute_fft, "process"), # CPU-bound
390
+ ... (detect_peaks, "process"), # CPU-bound
391
+ ... (export_results, "thread"), # I/O-bound
392
+ ... ]
393
+ >>> files = [Path("sig1.bin"), Path("sig2.bin")]
394
+ >>> result = processor.pipeline(stages, files)
395
+ """
396
+ if not stages:
397
+ raise ValueError("Pipeline must have at least one stage")
398
+ if not items:
399
+ raise ValueError("Cannot process empty item list")
400
+
401
+ current_items = items
402
+ total_time = 0.0
403
+
404
+ for i, (func, strategy) in enumerate(stages):
405
+ logger.info(f"Executing pipeline stage {i + 1}/{len(stages)}: strategy={strategy}")
406
+
407
+ # Save original strategy and override for this stage
408
+ original_strategy = self.config.strategy
409
+ self.config.strategy = strategy
410
+
411
+ result = self.map(func, current_items)
412
+
413
+ # Restore original strategy
414
+ self.config.strategy = original_strategy
415
+
416
+ current_items = result.results
417
+ total_time += result.execution_time
418
+
419
+ if result.errors:
420
+ logger.warning(f"Stage {i + 1} completed with {len(result.errors)} errors")
421
+
422
+ logger.info(f"Pipeline completed: {len(stages)} stages, total_time={total_time:.2f}s")
423
+
424
+ # Return result from final stage with cumulative timing
425
+ result.execution_time = total_time
426
+ return result
427
+
428
+ # =========================================================================
429
+ # Internal Helper Methods
430
+ # =========================================================================
431
+
432
+ def _resolve_strategy(self, func: Callable[[Any], Any], items: Sequence[Any]) -> str:
433
+ """Determine appropriate parallelization strategy.
434
+
435
+ Args:
436
+ func: Function to be parallelized.
437
+ items: Items to process.
438
+
439
+ Returns:
440
+ Strategy name: "process", "thread", or "sequential".
441
+ """
442
+ if self.config.strategy != "auto":
443
+ return self.config.strategy
444
+
445
+ # Auto-detection heuristics
446
+ # 1. Small datasets (<10 items) - sequential is faster
447
+ if len(items) < 10:
448
+ logger.debug("Auto-selecting sequential (small dataset)")
449
+ return "sequential"
450
+
451
+ # 2. Function name heuristics
452
+ func_name = func.__name__.lower()
453
+ if any(
454
+ keyword in func_name for keyword in ["fft", "correlate", "decode", "analyze", "compute"]
455
+ ):
456
+ logger.debug(f"Auto-selecting process (CPU-bound function: {func_name})")
457
+ return "process"
458
+
459
+ if any(keyword in func_name for keyword in ["load", "read", "fetch", "download"]):
460
+ logger.debug(f"Auto-selecting thread (I/O-bound function: {func_name})")
461
+ return "thread"
462
+
463
+ # 3. Default to process for medium+ datasets
464
+ logger.debug("Auto-selecting process (default for parallel workload)")
465
+ return "process"
466
+
467
+ def _get_worker_count(self, strategy: str = "process") -> int:
468
+ """Get appropriate worker count for strategy.
469
+
470
+ Args:
471
+ strategy: Parallelization strategy.
472
+
473
+ Returns:
474
+ Number of workers to use.
475
+ """
476
+ if self.config.num_workers is not None:
477
+ return self.config.num_workers
478
+
479
+ # Auto-calculate based on strategy
480
+ if strategy == "process":
481
+ # Leave one core free for OS/orchestration
482
+ return max(1, self._cpu_count - 1)
483
+ elif strategy == "thread":
484
+ # I/O-bound tasks can use more threads than cores
485
+ return self._cpu_count * 2
486
+ else:
487
+ return 1
488
+
489
+ def _calculate_batch_size(self, total_items: int) -> int:
490
+ """Calculate optimal batch size for dataset.
491
+
492
+ Args:
493
+ total_items: Total number of items to process.
494
+
495
+ Returns:
496
+ Optimal batch size.
497
+ """
498
+ if self.config.batch_size is not None:
499
+ return self.config.batch_size
500
+
501
+ num_workers = self._get_worker_count()
502
+
503
+ # Target: 4 batches per worker (allows good load balancing)
504
+ optimal_batches = num_workers * 4
505
+ batch_size = max(1, total_items // optimal_batches)
506
+
507
+ logger.debug(
508
+ f"Auto-calculated batch_size={batch_size} "
509
+ f"({total_items} items / {optimal_batches} batches)"
510
+ )
511
+
512
+ return batch_size
513
+
514
+ def _map_process(
515
+ self,
516
+ func: Callable[[Any], Any],
517
+ items: Sequence[Any],
518
+ num_workers: int,
519
+ ) -> tuple[list[Any], list[tuple[int, Exception]]]:
520
+ """Execute map using process pool.
521
+
522
+ Args:
523
+ func: Function to apply.
524
+ items: Items to process.
525
+ num_workers: Number of worker processes.
526
+
527
+ Returns:
528
+ Tuple of (results list, errors list).
529
+ """
530
+ results: list[Any] = [None] * len(items)
531
+ errors: list[tuple[int, Exception]] = []
532
+
533
+ try:
534
+ with ProcessPoolExecutor(max_workers=num_workers) as executor:
535
+ # Submit all tasks
536
+ futures = {executor.submit(func, item): idx for idx, item in enumerate(items)}
537
+
538
+ # Collect results with optional progress bar
539
+ iterator: Iterable[Any]
540
+ if self.config.show_progress:
541
+ try:
542
+ from tqdm import tqdm
543
+
544
+ iterator = tqdm(futures, total=len(items), desc="Processing")
545
+ except ImportError:
546
+ logger.warning("tqdm not available, progress bar disabled")
547
+ iterator = futures
548
+ else:
549
+ iterator = futures
550
+
551
+ for future in iterator:
552
+ idx = futures[future]
553
+ try:
554
+ result = future.result(timeout=self.config.timeout)
555
+ results[idx] = result
556
+ except Exception as e:
557
+ logger.warning(f"Task {idx} failed: {e}")
558
+ errors.append((idx, e))
559
+ results[idx] = None
560
+
561
+ except Exception as e:
562
+ logger.error(f"Process pool execution failed: {e}")
563
+ raise
564
+
565
+ return results, errors
566
+
567
+ def _map_thread(
568
+ self,
569
+ func: Callable[[Any], Any],
570
+ items: Sequence[Any],
571
+ num_workers: int,
572
+ ) -> tuple[list[Any], list[tuple[int, Exception]]]:
573
+ """Execute map using thread pool.
574
+
575
+ Args:
576
+ func: Function to apply.
577
+ items: Items to process.
578
+ num_workers: Number of worker threads.
579
+
580
+ Returns:
581
+ Tuple of (results list, errors list).
582
+ """
583
+ results: list[Any] = [None] * len(items)
584
+ errors: list[tuple[int, Exception]] = []
585
+
586
+ try:
587
+ with ThreadPoolExecutor(max_workers=num_workers) as executor:
588
+ # Submit all tasks
589
+ futures = {executor.submit(func, item): idx for idx, item in enumerate(items)}
590
+
591
+ # Collect results with optional progress bar
592
+ iterator: Iterable[Any]
593
+ if self.config.show_progress:
594
+ try:
595
+ from tqdm import tqdm
596
+
597
+ iterator = tqdm(futures, total=len(items), desc="Processing")
598
+ except ImportError:
599
+ logger.warning("tqdm not available, progress bar disabled")
600
+ iterator = futures
601
+ else:
602
+ iterator = futures
603
+
604
+ for future in iterator:
605
+ idx = futures[future]
606
+ try:
607
+ result = future.result(timeout=self.config.timeout)
608
+ results[idx] = result
609
+ except Exception as e:
610
+ logger.warning(f"Task {idx} failed: {e}")
611
+ errors.append((idx, e))
612
+ results[idx] = None
613
+
614
+ except Exception as e:
615
+ logger.error(f"Thread pool execution failed: {e}")
616
+ raise
617
+
618
+ return results, errors
619
+
620
+ def _map_sequential(
621
+ self,
622
+ func: Callable[[Any], Any],
623
+ items: Sequence[Any],
624
+ ) -> tuple[list[Any], list[tuple[int, Exception]]]:
625
+ """Execute map sequentially (fallback/baseline).
626
+
627
+ Args:
628
+ func: Function to apply.
629
+ items: Items to process.
630
+
631
+ Returns:
632
+ Tuple of (results list, errors list).
633
+ """
634
+ results: list[Any] = []
635
+ errors: list[tuple[int, Exception]] = []
636
+
637
+ for idx, item in enumerate(items):
638
+ try:
639
+ result = func(item)
640
+ results.append(result)
641
+ except Exception as e:
642
+ logger.warning(f"Task {idx} failed: {e}")
643
+ errors.append((idx, e))
644
+ results.append(None)
645
+
646
+ return results, errors
647
+
648
+
649
+ __all__ = [
650
+ "ParallelConfig",
651
+ "ParallelProcessor",
652
+ "ParallelResult",
653
+ "WorkerStats",
654
+ ]