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
@@ -316,25 +316,72 @@ class ArgumentPreparer:
316
316
  kwargs: dict[str, Any] = {}
317
317
  data_length = len(raw_data) if hasattr(raw_data, "__len__") else 0
318
318
 
319
+ # Add window_size if needed
320
+ kwargs.update(self._add_window_size_param(params, sig, data_length, kwargs))
321
+
322
+ # Add min_width if needed
323
+ kwargs.update(self._add_min_width_param(params, sig, sample_rate, kwargs))
324
+
325
+ # Add max_width if needed
326
+ kwargs.update(self._add_max_width_param(params, sig, data_length, sample_rate, kwargs))
327
+
328
+ # Add threshold if needed
329
+ kwargs.update(self._add_threshold_param(params, sig, raw_data, kwargs))
330
+
331
+ # Add window_duration if needed
332
+ kwargs.update(
333
+ self._add_window_duration_param(params, sig, data_length, sample_rate, kwargs)
334
+ )
335
+
336
+ return kwargs
337
+
338
+ def _add_window_size_param(
339
+ self, params: list[str], sig: inspect.Signature, data_length: int, kwargs: dict[str, Any]
340
+ ) -> dict[str, Any]:
341
+ """Add window_size parameter."""
342
+ result = {}
319
343
  if "window_size" in params:
320
344
  if self._param_needs_value(sig, "window_size") and "window_size" not in kwargs:
321
345
  window_size: Any = max(10, data_length // 10)
322
- kwargs["window_size"] = window_size
323
- logger.debug(f"Using auto-detected window_size: {kwargs['window_size']}")
346
+ result["window_size"] = window_size
347
+ logger.debug(f"Using auto-detected window_size: {window_size}")
348
+ return result
324
349
 
350
+ def _add_min_width_param(
351
+ self, params: list[str], sig: inspect.Signature, sample_rate: float, kwargs: dict[str, Any]
352
+ ) -> dict[str, Any]:
353
+ """Add min_width parameter."""
354
+ result = {}
325
355
  if "min_width" in params:
326
356
  if self._param_needs_value(sig, "min_width") and "min_width" not in kwargs:
327
357
  min_width: Any = max(1e-9, 10.0 / sample_rate)
328
- kwargs["min_width"] = min_width
329
- logger.debug(f"Using auto-detected min_width: {kwargs['min_width']:.2e}s")
358
+ result["min_width"] = min_width
359
+ logger.debug(f"Using auto-detected min_width: {min_width:.2e}s")
360
+ return result
330
361
 
362
+ def _add_max_width_param(
363
+ self,
364
+ params: list[str],
365
+ sig: inspect.Signature,
366
+ data_length: int,
367
+ sample_rate: float,
368
+ kwargs: dict[str, Any],
369
+ ) -> dict[str, Any]:
370
+ """Add max_width parameter."""
371
+ result = {}
331
372
  if "max_width" in params:
332
373
  if self._param_needs_value(sig, "max_width") and "max_width" not in kwargs:
333
374
  total_duration = data_length / sample_rate if data_length > 0 else 1e-3
334
375
  max_width: Any = min(1e-3, total_duration)
335
- kwargs["max_width"] = max_width
336
- logger.debug(f"Using auto-detected max_width: {kwargs['max_width']:.2e}s")
376
+ result["max_width"] = max_width
377
+ logger.debug(f"Using auto-detected max_width: {max_width:.2e}s")
378
+ return result
337
379
 
380
+ def _add_threshold_param(
381
+ self, params: list[str], sig: inspect.Signature, raw_data: Any, kwargs: dict[str, Any]
382
+ ) -> dict[str, Any]:
383
+ """Add threshold parameter."""
384
+ result = {}
338
385
  if "threshold" in params and "threshold" not in kwargs:
339
386
  param_info = sig.parameters.get("threshold")
340
387
  has_default = (
@@ -344,21 +391,29 @@ class ArgumentPreparer:
344
391
  try:
345
392
  if isinstance(raw_data, np.ndarray) and raw_data.size > 0:
346
393
  threshold: Any = float(np.median(raw_data))
347
- kwargs["threshold"] = threshold
348
- logger.debug(f"Using auto-detected threshold: {kwargs['threshold']:.3f}")
394
+ result["threshold"] = threshold
395
+ logger.debug(f"Using auto-detected threshold: {threshold:.3f}")
349
396
  except Exception as e:
350
397
  logger.debug(f"Could not auto-detect threshold: {e}")
398
+ return result
351
399
 
400
+ def _add_window_duration_param(
401
+ self,
402
+ params: list[str],
403
+ sig: inspect.Signature,
404
+ data_length: int,
405
+ sample_rate: float,
406
+ kwargs: dict[str, Any],
407
+ ) -> dict[str, Any]:
408
+ """Add window_duration parameter."""
409
+ result = {}
352
410
  if "window_duration" in params:
353
411
  if self._param_needs_value(sig, "window_duration") and "window_duration" not in kwargs:
354
412
  total_duration = data_length / sample_rate if data_length > 0 else 1.0
355
413
  window_duration: Any = min(1.0, total_duration / 10.0)
356
- kwargs["window_duration"] = window_duration
357
- logger.debug(
358
- f"Using auto-detected window_duration: {kwargs['window_duration']:.3f}s"
359
- )
360
-
361
- return kwargs
414
+ result["window_duration"] = window_duration
415
+ logger.debug(f"Using auto-detected window_duration: {window_duration:.3f}s")
416
+ return result
362
417
 
363
418
  def _param_needs_value(self, sig: inspect.Signature, param_name: str) -> bool:
364
419
  """Check if parameter needs a value (no default or default is None)."""
@@ -102,7 +102,9 @@ class Report:
102
102
  """
103
103
  self.output_path = path
104
104
 
105
- html_content = f"""<!DOCTYPE html>
105
+ # Use list + join for O(n) string building instead of O(n²) +=
106
+ html_parts = [
107
+ f"""<!DOCTYPE html>
106
108
  <html>
107
109
  <head>
108
110
  <meta charset="UTF-8">
@@ -155,26 +157,33 @@ class Report:
155
157
  <p><strong>Date:</strong> {self.metadata.date}</p>
156
158
  <p><strong>Author:</strong> {self.metadata.author}</p>
157
159
  """
160
+ ]
161
+
158
162
  if self.metadata.project:
159
- html_content += f" <p><strong>Project:</strong> {self.metadata.project}</p>\n"
163
+ html_parts.append(f" <p><strong>Project:</strong> {self.metadata.project}</p>\n")
160
164
 
161
165
  if self.metadata.tags:
162
- html_content += (
166
+ html_parts.append(
163
167
  f" <p><strong>Tags:</strong> {', '.join(self.metadata.tags)}</p>\n"
164
168
  )
165
169
 
166
- html_content += " </div>\n\n"
170
+ html_parts.append(" </div>\n\n")
167
171
 
172
+ # Build sections with list.append instead of += in loop
168
173
  for section in self.sections:
169
174
  if section in self.content:
170
175
  section_title = section.replace("_", " ").title()
171
- html_content += f""" <div class="section">
176
+ html_parts.append(
177
+ f""" <div class="section">
172
178
  <h2>{section_title}</h2>
173
179
  <p>{self.content[section]}</p>
174
180
  </div>
175
181
  """
176
- html_content += """</body>
177
- </html>"""
182
+ )
183
+
184
+ html_parts.append("</body>\n</html>")
185
+ html_content = "".join(html_parts)
186
+
178
187
  with open(path, "w") as f:
179
188
  f.write(html_content)
180
189
 
@@ -188,23 +197,30 @@ class Report:
188
197
  """
189
198
  self.output_path = path
190
199
 
191
- md_content = f"# {self.metadata.title}\n\n"
192
- md_content += f"**Date:** {self.metadata.date} \n"
193
- md_content += f"**Author:** {self.metadata.author} \n"
200
+ # Use list + join for O(n) string building instead of O(n²) +=
201
+ md_parts = [
202
+ f"# {self.metadata.title}\n\n",
203
+ f"**Date:** {self.metadata.date} \n",
204
+ f"**Author:** {self.metadata.author} \n",
205
+ ]
194
206
 
195
207
  if self.metadata.project:
196
- md_content += f"**Project:** {self.metadata.project} \n"
208
+ md_parts.append(f"**Project:** {self.metadata.project} \n")
197
209
 
198
210
  if self.metadata.tags:
199
- md_content += f"**Tags:** {', '.join(self.metadata.tags)} \n"
211
+ md_parts.append(f"**Tags:** {', '.join(self.metadata.tags)} \n")
200
212
 
201
- md_content += "\n---\n\n"
213
+ md_parts.append("\n---\n\n")
202
214
 
215
+ # Build sections with list.append instead of += in loop
203
216
  for section in self.sections:
204
217
  if section in self.content:
205
218
  section_title = section.replace("_", " ").title()
206
- md_content += f"## {section_title}\n\n"
207
- md_content += self.content[section] + "\n\n"
219
+ md_parts.append(f"## {section_title}\n\n")
220
+ md_parts.append(self.content[section])
221
+ md_parts.append("\n\n")
222
+
223
+ md_content = "".join(md_parts)
208
224
 
209
225
  with open(path, "w") as f:
210
226
  f.write(md_content)
@@ -397,6 +413,67 @@ def _generate_detailed_results(trace: WaveformTrace, context: dict[str, Any]) ->
397
413
  return results
398
414
 
399
415
 
416
+ def _generate_section_content(
417
+ sections: list[str], trace: WaveformTrace, context: dict[str, Any]
418
+ ) -> dict[str, str]:
419
+ """Generate content for requested sections.
420
+
421
+ Args:
422
+ sections: List of section names to generate
423
+ trace: Waveform trace
424
+ context: Analysis context
425
+
426
+ Returns:
427
+ Dictionary mapping section names to content
428
+ """
429
+ content = {}
430
+
431
+ if "executive_summary" in sections or "summary" in sections:
432
+ content["executive_summary"] = _generate_executive_summary(trace, context)
433
+
434
+ if "key_findings" in sections or "findings" in sections:
435
+ content["key_findings"] = _generate_key_findings(trace, context)
436
+
437
+ if "methodology" in sections:
438
+ content["methodology"] = _generate_methodology(trace, context)
439
+
440
+ if "detailed_results" in sections or "results" in sections:
441
+ content["detailed_results"] = _generate_detailed_results(trace, context)
442
+
443
+ if "recommendations" in sections:
444
+ content["recommendations"] = (
445
+ "Recommendations based on analysis:\n\n"
446
+ "1. Signal quality is acceptable for analysis\n"
447
+ "2. Consider additional captures for verification\n"
448
+ "3. Review anomalies if present\n"
449
+ )
450
+
451
+ return content
452
+
453
+
454
+ def _determine_plot_types(trace: WaveformTrace, options: dict[str, Any]) -> list[str]:
455
+ """Determine which plots to include in report.
456
+
457
+ Args:
458
+ trace: Waveform trace
459
+ options: Report options
460
+
461
+ Returns:
462
+ List of plot type names
463
+ """
464
+ configured_plot_types = options.get("plot_types", [])
465
+ if configured_plot_types:
466
+ # Cast to list[str] - we know it contains strings from config
467
+ return [str(p) for p in configured_plot_types]
468
+
469
+ # Auto-select based on signal characteristics
470
+ plot_types = ["time_domain_waveform"]
471
+ if len(trace.data) > 100:
472
+ plot_types.append("fft_spectrum")
473
+
474
+ return plot_types
475
+
476
+
400
477
  def generate_report(
401
478
  trace: WaveformTrace,
402
479
  *,
@@ -437,55 +514,15 @@ def generate_report(
437
514
  context = context or {}
438
515
  options = options or {}
439
516
 
440
- # Determine sections to include
441
- default_sections = [
442
- "executive_summary",
443
- "key_findings",
444
- "methodology",
445
- "detailed_results",
446
- ]
447
-
517
+ default_sections = ["executive_summary", "key_findings", "methodology", "detailed_results"]
448
518
  sections = options.get("select_sections", default_sections)
449
519
 
450
- # Generate content for each section
451
- content = {}
452
-
453
- if "executive_summary" in sections or "summary" in sections:
454
- content["executive_summary"] = _generate_executive_summary(trace, context)
455
-
456
- if "key_findings" in sections or "findings" in sections:
457
- content["key_findings"] = _generate_key_findings(trace, context)
458
-
459
- if "methodology" in sections:
460
- content["methodology"] = _generate_methodology(trace, context)
461
-
462
- if "detailed_results" in sections or "results" in sections:
463
- content["detailed_results"] = _generate_detailed_results(trace, context)
464
-
465
- if "recommendations" in sections:
466
- content["recommendations"] = (
467
- "Recommendations based on analysis:\n\n"
468
- "1. Signal quality is acceptable for analysis\n"
469
- "2. Consider additional captures for verification\n"
470
- "3. Review anomalies if present\n"
471
- )
472
-
473
- # Determine plot types to include
474
- plot_types = options.get("plot_types", [])
475
- if not plot_types:
476
- # Auto-select based on signal characteristics
477
- plot_types = ["time_domain_waveform"]
478
-
479
- # Add spectral if signal looks periodic
480
- if len(trace.data) > 100:
481
- plot_types.append("fft_spectrum")
520
+ content = _generate_section_content(sections, trace, context)
521
+ plot_types = _determine_plot_types(trace, options)
482
522
 
483
- # Estimate page count (rough estimate)
484
- page_count = 1 # Title page
485
- page_count += len(sections) # One page per section
486
- page_count += (len(plot_types) + 1) // 2 # 2 plots per page
523
+ # Estimate page count
524
+ page_count = 1 + len(sections) + (len(plot_types) + 1) // 2
487
525
 
488
- # Create report object
489
526
  report = Report(
490
527
  sections=list(sections),
491
528
  plots=plot_types,
@@ -493,7 +530,6 @@ def generate_report(
493
530
  content=content,
494
531
  )
495
532
 
496
- # Set custom metadata if provided
497
533
  if "custom_header" in options:
498
534
  report.metadata.title = options["custom_header"]
499
535
 
oscura/reporting/batch.py CHANGED
@@ -114,8 +114,6 @@ def batch_report(
114
114
  References:
115
115
  RPT-003: Batch Report Generation
116
116
  """
117
- import oscura as osc
118
- from oscura.reporting.template_system import load_template
119
117
 
120
118
  output_path = Path(output_dir)
121
119
  output_path.mkdir(parents=True, exist_ok=True)
@@ -124,81 +122,156 @@ def batch_report(
124
122
  batch_results: list[dict[str, Any]] = []
125
123
 
126
124
  # Load template
125
+ report_template = _load_report_template(template)
126
+
127
+ # Get DUT ID extractor
128
+ extractor = dut_id_extractor or _default_dut_id_extractor
129
+
130
+ # Process each file
131
+ batch_results = _process_files(
132
+ files,
133
+ extractor,
134
+ analyzer,
135
+ result,
136
+ output_path,
137
+ report_template,
138
+ output_format,
139
+ file_pattern,
140
+ generate_individual,
141
+ )
142
+
143
+ # Generate summary report if requested
144
+ if generate_summary and batch_results:
145
+ _create_summary_report(batch_results, output_path, summary_filename, output_format, result)
146
+
147
+ return result
148
+
149
+
150
+ def _load_report_template(template: str) -> Any:
151
+ """Load report template with fallback to default."""
152
+ from oscura.reporting.template_system import load_template
153
+
127
154
  try:
128
- report_template = load_template(template)
155
+ return load_template(template)
129
156
  except ValueError:
130
157
  logger.warning(f"Template '{template}' not found, using 'default'")
131
- report_template = load_template("default")
158
+ return load_template("default")
132
159
 
133
- # Default DUT ID extractor
134
- if dut_id_extractor is None:
135
160
 
136
- def dut_id_extractor(p: Path) -> str:
137
- return Path(p).stem
161
+ def _default_dut_id_extractor(path: Path) -> str:
162
+ """Extract DUT ID from file path (default: use stem)."""
163
+ return Path(path).stem
164
+
165
+
166
+ def _process_files(
167
+ files: list[str | Path],
168
+ dut_id_extractor: Callable[[Path], str],
169
+ analyzer: Callable[[Any], dict[str, Any]] | None,
170
+ result: BatchReportResult,
171
+ output_path: Path,
172
+ report_template: Any,
173
+ output_format: str,
174
+ file_pattern: str,
175
+ generate_individual: bool,
176
+ ) -> list[dict[str, Any]]:
177
+ """Process all files and generate individual reports."""
178
+
179
+ batch_results: list[dict[str, Any]] = []
138
180
 
139
- # Process each file
140
181
  for file_path in files:
141
182
  file_path = Path(file_path)
142
183
  dut_id = dut_id_extractor(file_path)
143
184
 
144
- try:
145
- # Load trace
146
- trace = osc.load(str(file_path))
185
+ dut_result = _process_single_file(file_path, dut_id, analyzer, result)
147
186
 
148
- # Run analysis
149
- if analyzer is not None:
150
- dut_result = analyzer(trace)
151
- else:
152
- # Default analysis
153
- dut_result = _default_analysis(trace)
187
+ if dut_result is not None:
188
+ batch_results.append(dut_result)
154
189
 
155
- # Add DUT metadata
156
- dut_result["dut_id"] = dut_id
157
- dut_result["source_file"] = str(file_path)
190
+ if generate_individual:
191
+ _save_individual_report(
192
+ dut_result,
193
+ dut_id,
194
+ output_path,
195
+ file_pattern,
196
+ output_format,
197
+ report_template,
198
+ result,
199
+ )
200
+
201
+ return batch_results
202
+
203
+
204
+ def _process_single_file(
205
+ file_path: Path,
206
+ dut_id: str,
207
+ analyzer: Callable[[Any], dict[str, Any]] | None,
208
+ result: BatchReportResult,
209
+ ) -> dict[str, Any] | None:
210
+ """Process a single DUT file and update result statistics."""
211
+ import oscura as osc
158
212
 
159
- batch_results.append(dut_result)
213
+ try:
214
+ trace = osc.load(str(file_path))
215
+ dut_result = analyzer(trace) if analyzer else _default_analysis(trace)
160
216
 
161
- # Check pass/fail
162
- if dut_result.get("pass_count", 0) == dut_result.get("total_count", 0):
163
- result.passed_duts += 1
164
- else:
165
- result.failed_duts += 1
217
+ dut_result["dut_id"] = dut_id
218
+ dut_result["source_file"] = str(file_path)
166
219
 
167
- result.total_duts += 1
220
+ # Update pass/fail counts
221
+ if dut_result.get("pass_count", 0) == dut_result.get("total_count", 0):
222
+ result.passed_duts += 1
223
+ else:
224
+ result.failed_duts += 1
168
225
 
169
- # Generate individual report if requested
170
- if generate_individual:
171
- ext = output_format.lower()
172
- individual_filename = file_pattern.format(dut_id=dut_id, ext=ext)
173
- individual_path = output_path / individual_filename
174
-
175
- try:
176
- _generate_individual_report(
177
- dut_result, individual_path, report_template, output_format
178
- )
179
- result.individual_report_paths.append(individual_path)
180
- except Exception as e:
181
- logger.error(f"Failed to generate report for {dut_id}: {e}")
182
- result.errors.append((dut_id, str(e)))
183
-
184
- except Exception as e:
185
- logger.error(f"Failed to process {file_path}: {e}")
186
- result.errors.append((str(file_path), str(e)))
226
+ result.total_duts += 1
227
+ return dut_result
187
228
 
188
- # Generate summary report if requested
189
- if generate_summary and batch_results:
190
- ext = output_format.lower()
191
- summary_path = output_path / summary_filename.format(ext=ext)
229
+ except Exception as e:
230
+ logger.error(f"Failed to process {file_path}: {e}")
231
+ result.errors.append((str(file_path), str(e)))
232
+ return None
192
233
 
193
- try:
194
- summary_report = generate_batch_report(batch_results)
195
- _save_report(summary_report, summary_path, output_format)
196
- result.summary_report_path = summary_path
197
- except Exception as e:
198
- logger.error(f"Failed to generate summary report: {e}")
199
- result.errors.append(("summary", str(e)))
200
234
 
201
- return result
235
+ def _save_individual_report(
236
+ dut_result: dict[str, Any],
237
+ dut_id: str,
238
+ output_path: Path,
239
+ file_pattern: str,
240
+ output_format: str,
241
+ report_template: Any,
242
+ result: BatchReportResult,
243
+ ) -> None:
244
+ """Save individual DUT report."""
245
+ ext = output_format.lower()
246
+ individual_filename = file_pattern.format(dut_id=dut_id, ext=ext)
247
+ individual_path = output_path / individual_filename
248
+
249
+ try:
250
+ _generate_individual_report(dut_result, individual_path, report_template, output_format)
251
+ result.individual_report_paths.append(individual_path)
252
+ except Exception as e:
253
+ logger.error(f"Failed to generate report for {dut_id}: {e}")
254
+ result.errors.append((dut_id, str(e)))
255
+
256
+
257
+ def _create_summary_report(
258
+ batch_results: list[dict[str, Any]],
259
+ output_path: Path,
260
+ summary_filename: str,
261
+ output_format: str,
262
+ result: BatchReportResult,
263
+ ) -> None:
264
+ """Create and save summary report."""
265
+ ext = output_format.lower()
266
+ summary_path = output_path / summary_filename.format(ext=ext)
267
+
268
+ try:
269
+ summary_report = generate_batch_report(batch_results)
270
+ _save_report(summary_report, summary_path, output_format)
271
+ result.summary_report_path = summary_path
272
+ except Exception as e:
273
+ logger.error(f"Failed to generate summary report: {e}")
274
+ result.errors.append(("summary", str(e)))
202
275
 
203
276
 
204
277
  def _default_analysis(trace: Any) -> dict[str, Any]: