oscura 0.5.1__py3-none-any.whl → 0.7.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 (497) 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/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.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
+ ]