oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,531 @@
1
+ """Result aggregation for batch analysis.
2
+
3
+
4
+ This module provides statistical aggregation and reporting for batch
5
+ analysis results, including outlier detection and export capabilities.
6
+
7
+ **Requires pandas:**
8
+ This module requires pandas for DataFrame operations. Install with:
9
+ pip install oscura[dataframes] # Pandas + Excel export
10
+ pip install oscura[standard] # Recommended
11
+ """
12
+
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ import numpy as np
17
+
18
+ try:
19
+ import pandas as pd
20
+ except ImportError as e:
21
+ raise ImportError(
22
+ "Batch aggregation requires pandas.\n\n"
23
+ "Install with:\n"
24
+ " pip install oscura[dataframes] # Pandas + Excel export\n"
25
+ " pip install oscura[standard] # Recommended\n"
26
+ " pip install oscura[all] # Everything\n"
27
+ ) from e
28
+
29
+
30
+ def aggregate_results(
31
+ results: pd.DataFrame,
32
+ *,
33
+ metrics: list[str] | None = None,
34
+ outlier_threshold: float = 3.0,
35
+ include_plots: bool = False,
36
+ output_format: str = "dict",
37
+ output_file: str | Path | None = None,
38
+ ) -> dict[str, Any] | pd.DataFrame:
39
+ """Aggregate results from batch analysis into summary statistics.
40
+
41
+ : Computes comprehensive statistics (mean, std, min, max,
42
+ outliers) for each metric in the batch results. Supports export to various
43
+ formats and optional visualization generation.
44
+
45
+ Args:
46
+ results: DataFrame from batch_analyze() containing analysis results
47
+ metrics: List of column names to aggregate (default: all numeric columns)
48
+ outlier_threshold: Z-score threshold for outlier detection (default: 3.0)
49
+ include_plots: Generate comparison plots across files (default: False)
50
+ output_format: Output format - 'dict', 'dataframe', 'csv', 'excel', 'html'
51
+ output_file: Optional output file path for export formats
52
+
53
+ Returns:
54
+ Dictionary or DataFrame with summary statistics:
55
+ - count: Number of valid values
56
+ - mean: Mean value
57
+ - std: Standard deviation
58
+ - min: Minimum value
59
+ - max: Maximum value
60
+ - median: Median value
61
+ - q25: 25th percentile
62
+ - q75: 75th percentile
63
+ - outliers: List of outlier values
64
+ - outlier_files: List of files containing outliers
65
+
66
+ Raises:
67
+ ValueError: If no numeric metrics are found in results.
68
+
69
+ Examples:
70
+ >>> results = osc.batch_analyze(files, osc.characterize_buffer)
71
+ >>> summary = osc.aggregate_results(
72
+ ... results,
73
+ ... metrics=['rise_time', 'fall_time'],
74
+ ... outlier_threshold=2.5
75
+ ... )
76
+ >>> print(summary['rise_time']['mean'])
77
+ >>> print(summary['rise_time']['outlier_files'])
78
+
79
+ Notes:
80
+ - Outliers detected using IQR method: values outside [Q1 - k*IQR, Q3 + k*IQR]
81
+ where k = (threshold / 3.0) * 1.5 (more robust than z-score for heavy-tailed data)
82
+ - Non-numeric columns are automatically skipped
83
+ - Missing values (NaN) are excluded from statistics
84
+ - CSV/Excel/HTML export requires output_file parameter
85
+
86
+ References:
87
+ BATCH-002: Result Aggregation
88
+ """
89
+ if results.empty:
90
+ return {} if output_format == "dict" else pd.DataFrame()
91
+
92
+ # Select and validate metrics
93
+ metrics_to_use = _select_metrics(results, metrics)
94
+
95
+ # Compute statistics for each metric
96
+ aggregated = _compute_metric_statistics(results, metrics_to_use, outlier_threshold)
97
+
98
+ # Generate plots if requested
99
+ if include_plots:
100
+ _generate_metric_plots(results, aggregated, metrics_to_use, output_file)
101
+
102
+ # Format and return results
103
+ return _format_output(aggregated, output_format, output_file, results, metrics_to_use)
104
+
105
+
106
+ def _select_metrics(results: pd.DataFrame, metrics: list[str] | None) -> list[str]:
107
+ """Select and validate metrics to aggregate.
108
+
109
+ Args:
110
+ results: DataFrame containing results.
111
+ metrics: User-specified metrics or None for auto-selection.
112
+
113
+ Returns:
114
+ List of metric column names.
115
+
116
+ Raises:
117
+ ValueError: If no numeric metrics found.
118
+ """
119
+ if metrics is None:
120
+ metrics = results.select_dtypes(include=[np.number]).columns.tolist()
121
+ metrics = [m for m in metrics if m not in ["file", "error"]]
122
+
123
+ if not metrics:
124
+ raise ValueError("No numeric metrics found in results")
125
+
126
+ return metrics
127
+
128
+
129
+ def _compute_metric_statistics(
130
+ results: pd.DataFrame,
131
+ metrics: list[str],
132
+ outlier_threshold: float,
133
+ ) -> dict[str, dict[str, Any]]:
134
+ """Compute statistics for all metrics.
135
+
136
+ Args:
137
+ results: DataFrame containing results.
138
+ metrics: List of metrics to compute statistics for.
139
+ outlier_threshold: Threshold for outlier detection.
140
+
141
+ Returns:
142
+ Dictionary mapping metrics to statistics.
143
+ """
144
+ aggregated: dict[str, dict[str, Any]] = {}
145
+
146
+ for metric in metrics:
147
+ if metric not in results.columns:
148
+ continue
149
+
150
+ values = results[metric].dropna()
151
+
152
+ if values.empty:
153
+ aggregated[metric] = _create_empty_stats()
154
+ continue
155
+
156
+ aggregated[metric] = _compute_single_metric_stats(values, results, outlier_threshold)
157
+
158
+ return aggregated
159
+
160
+
161
+ def _create_empty_stats() -> dict[str, Any]:
162
+ """Create empty statistics dictionary.
163
+
164
+ Returns:
165
+ Dictionary with NaN values and empty lists.
166
+ """
167
+ return {
168
+ "count": 0,
169
+ "mean": np.nan,
170
+ "std": np.nan,
171
+ "min": np.nan,
172
+ "max": np.nan,
173
+ "median": np.nan,
174
+ "q25": np.nan,
175
+ "q75": np.nan,
176
+ "outliers": [],
177
+ "outlier_files": [],
178
+ }
179
+
180
+
181
+ def _compute_single_metric_stats(
182
+ values: pd.Series,
183
+ results: pd.DataFrame,
184
+ outlier_threshold: float,
185
+ ) -> dict[str, Any]:
186
+ """Compute statistics for a single metric.
187
+
188
+ Args:
189
+ values: Series of non-null metric values.
190
+ results: Full results DataFrame (for file lookup).
191
+ outlier_threshold: Threshold for outlier detection.
192
+
193
+ Returns:
194
+ Dictionary with computed statistics.
195
+ """
196
+ stats = _compute_basic_statistics(values)
197
+ outliers_info = _detect_outliers(values, stats, outlier_threshold)
198
+ stats.update(outliers_info)
199
+
200
+ # Add outlier files if available
201
+ if outliers_info["outliers"]:
202
+ outlier_files: Any = _get_outlier_files(results, outliers_info["outlier_indices"])
203
+ stats["outlier_files"] = outlier_files
204
+ else:
205
+ stats["outlier_files"] = []
206
+
207
+ return stats
208
+
209
+
210
+ def _compute_basic_statistics(values: pd.Series) -> dict[str, Any]:
211
+ """Compute basic statistics for metric values.
212
+
213
+ Args:
214
+ values: Series of metric values.
215
+
216
+ Returns:
217
+ Dictionary with basic statistics.
218
+ """
219
+ return {
220
+ "count": len(values),
221
+ "mean": float(values.mean()),
222
+ "std": float(values.std()),
223
+ "min": float(values.min()),
224
+ "max": float(values.max()),
225
+ "median": float(values.median()),
226
+ "q25": float(values.quantile(0.25)),
227
+ "q75": float(values.quantile(0.75)),
228
+ }
229
+
230
+
231
+ def _detect_outliers(
232
+ values: pd.Series,
233
+ stats: dict[str, Any],
234
+ threshold: float,
235
+ ) -> dict[str, Any]:
236
+ """Detect outliers using IQR method.
237
+
238
+ Args:
239
+ values: Series of metric values.
240
+ stats: Basic statistics containing q25 and q75.
241
+ threshold: IQR threshold multiplier.
242
+
243
+ Returns:
244
+ Dictionary with outlier information.
245
+ """
246
+ if len(values) <= 3:
247
+ return {"outliers": [], "outlier_indices": []}
248
+
249
+ q1, q3 = stats["q25"], stats["q75"]
250
+ iqr = q3 - q1
251
+ k = (threshold / 3.0) * 1.5
252
+
253
+ lower_bound = q1 - k * iqr
254
+ upper_bound = q3 + k * iqr
255
+
256
+ outlier_mask = (values < lower_bound) | (values > upper_bound)
257
+ outlier_indices = values[outlier_mask].index.tolist()
258
+
259
+ return {
260
+ "outliers": values[outlier_mask].tolist(),
261
+ "outlier_indices": outlier_indices,
262
+ }
263
+
264
+
265
+ def _get_outlier_files(
266
+ results: pd.DataFrame,
267
+ outlier_indices: list[Any],
268
+ ) -> Any:
269
+ """Get filenames for outlier indices.
270
+
271
+ Args:
272
+ results: Full results DataFrame.
273
+ outlier_indices: Indices of outlier values.
274
+
275
+ Returns:
276
+ List of filenames or indices.
277
+ """
278
+ if "file" in results.columns:
279
+ return results.loc[outlier_indices, "file"].tolist()
280
+ return outlier_indices
281
+
282
+
283
+ def _generate_metric_plots(
284
+ results: pd.DataFrame,
285
+ aggregated: dict[str, dict[str, Any]],
286
+ metrics: list[str],
287
+ output_file: str | Path | None,
288
+ ) -> None:
289
+ """Generate comparison plots for metrics.
290
+
291
+ Args:
292
+ results: DataFrame with results.
293
+ aggregated: Aggregated statistics.
294
+ metrics: List of metrics to plot.
295
+ output_file: Output file path for saving plots.
296
+ """
297
+ try:
298
+ import matplotlib.pyplot as plt
299
+
300
+ for metric in metrics:
301
+ if metric not in aggregated:
302
+ continue
303
+
304
+ _create_metric_plot(results, aggregated, metric, output_file)
305
+ plt.close()
306
+
307
+ except ImportError:
308
+ pass # Skip if matplotlib unavailable
309
+
310
+
311
+ def _create_metric_plot(
312
+ results: pd.DataFrame,
313
+ aggregated: dict[str, dict[str, Any]],
314
+ metric: str,
315
+ output_file: str | Path | None,
316
+ ) -> None:
317
+ """Create histogram and box plot for a metric.
318
+
319
+ Args:
320
+ results: DataFrame with results.
321
+ aggregated: Aggregated statistics.
322
+ metric: Metric name.
323
+ output_file: Output file path.
324
+ """
325
+ import matplotlib.pyplot as plt
326
+
327
+ _fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
328
+
329
+ # Histogram
330
+ _plot_histogram(results, aggregated, metric, ax1)
331
+
332
+ # Box plot
333
+ _plot_boxplot(results, metric, ax2)
334
+
335
+ plt.tight_layout()
336
+
337
+ # Save or show
338
+ if output_file:
339
+ plot_file = Path(output_file).with_suffix("") / f"{metric}_plot.png"
340
+ plot_file.parent.mkdir(parents=True, exist_ok=True)
341
+ plt.savefig(plot_file)
342
+ else:
343
+ plt.show()
344
+
345
+
346
+ def _plot_histogram(
347
+ results: pd.DataFrame,
348
+ aggregated: dict[str, dict[str, Any]],
349
+ metric: str,
350
+ ax: Any,
351
+ ) -> None:
352
+ """Plot histogram with mean and median lines.
353
+
354
+ Args:
355
+ results: DataFrame with results.
356
+ aggregated: Aggregated statistics.
357
+ metric: Metric name.
358
+ ax: Matplotlib axes.
359
+ """
360
+ results[metric].dropna().hist(ax=ax, bins=30)
361
+ ax.axvline(aggregated[metric]["mean"], color="r", linestyle="--", label="Mean")
362
+ ax.axvline(aggregated[metric]["median"], color="g", linestyle="--", label="Median")
363
+ ax.set_xlabel(metric)
364
+ ax.set_ylabel("Count")
365
+ ax.legend()
366
+ ax.set_title(f"{metric} Distribution")
367
+
368
+
369
+ def _plot_boxplot(results: pd.DataFrame, metric: str, ax: Any) -> None:
370
+ """Plot box plot for metric.
371
+
372
+ Args:
373
+ results: DataFrame with results.
374
+ metric: Metric name.
375
+ ax: Matplotlib axes.
376
+ """
377
+ ax.boxplot(results[metric].dropna())
378
+ ax.set_ylabel(metric)
379
+ ax.set_title(f"{metric} Box Plot")
380
+
381
+
382
+ def _format_output(
383
+ aggregated: dict[str, dict[str, Any]],
384
+ output_format: str,
385
+ output_file: str | Path | None,
386
+ results: pd.DataFrame,
387
+ metrics: list[str],
388
+ ) -> dict[str, Any] | pd.DataFrame:
389
+ """Format aggregated results based on output format.
390
+
391
+ Args:
392
+ aggregated: Aggregated statistics.
393
+ output_format: Desired output format.
394
+ output_file: Output file path.
395
+ results: Original results DataFrame.
396
+ metrics: List of metrics.
397
+
398
+ Returns:
399
+ Formatted results.
400
+
401
+ Raises:
402
+ ValueError: If unknown format or missing output_file.
403
+ """
404
+ if output_format == "dict":
405
+ return aggregated
406
+
407
+ if output_format == "dataframe":
408
+ return _convert_to_dataframe(aggregated)
409
+
410
+ if output_format in ["csv", "excel", "html"]:
411
+ return _export_to_file(aggregated, output_format, output_file, results, metrics)
412
+
413
+ raise ValueError(f"Unknown output_format: {output_format}")
414
+
415
+
416
+ def _convert_to_dataframe(aggregated: dict[str, dict[str, Any]]) -> pd.DataFrame:
417
+ """Convert aggregated dict to DataFrame.
418
+
419
+ Args:
420
+ aggregated: Aggregated statistics dictionary.
421
+
422
+ Returns:
423
+ DataFrame with metrics as rows.
424
+ """
425
+ df = pd.DataFrame(aggregated).T
426
+ return df.drop(columns=["outliers", "outlier_files"], errors="ignore")
427
+
428
+
429
+ def _export_to_file(
430
+ aggregated: dict[str, dict[str, Any]],
431
+ output_format: str,
432
+ output_file: str | Path | None,
433
+ results: pd.DataFrame,
434
+ metrics: list[str],
435
+ ) -> pd.DataFrame:
436
+ """Export aggregated results to file.
437
+
438
+ Args:
439
+ aggregated: Aggregated statistics.
440
+ output_format: Format (csv, excel, html).
441
+ output_file: Output file path.
442
+ results: Original results DataFrame.
443
+ metrics: List of metrics.
444
+
445
+ Returns:
446
+ DataFrame with aggregated results.
447
+
448
+ Raises:
449
+ ValueError: If output_file not provided.
450
+ """
451
+ if not output_file:
452
+ raise ValueError(f"{output_format} format requires output_file parameter")
453
+
454
+ df = _convert_to_dataframe(aggregated)
455
+
456
+ if output_format == "csv":
457
+ df.to_csv(output_file)
458
+ elif output_format == "excel":
459
+ df.to_excel(output_file)
460
+ elif output_format == "html":
461
+ html = _generate_html_report(results, aggregated, metrics)
462
+ Path(output_file).write_text(html)
463
+
464
+ return df
465
+
466
+
467
+ def _generate_html_report(
468
+ results: pd.DataFrame,
469
+ aggregated: dict[str, dict[str, Any]],
470
+ metrics: list[str],
471
+ ) -> str:
472
+ """Generate HTML report for batch analysis results."""
473
+ html = """
474
+ <!DOCTYPE html>
475
+ <html>
476
+ <head>
477
+ <title>Batch Analysis Report</title>
478
+ <style>
479
+ body { font-family: Arial, sans-serif; margin: 20px; }
480
+ h1 { color: #333; }
481
+ h2 { color: #666; margin-top: 30px; }
482
+ table { border-collapse: collapse; width: 100%; margin: 20px 0; }
483
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
484
+ th { background-color: #4CAF50; color: white; }
485
+ tr:nth-child(even) { background-color: #f2f2f2; }
486
+ .outlier { background-color: #ffcccc; }
487
+ </style>
488
+ </head>
489
+ <body>
490
+ <h1>Batch Analysis Report</h1>
491
+ """
492
+ # Summary statistics table
493
+ html += "<h2>Summary Statistics</h2>\n<table>\n"
494
+ html += "<tr><th>Metric</th><th>Count</th><th>Mean</th><th>Std</th>"
495
+ html += "<th>Min</th><th>Median</th><th>Max</th><th>Outliers</th></tr>\n"
496
+
497
+ for metric in metrics:
498
+ if metric not in aggregated:
499
+ continue
500
+ stats = aggregated[metric]
501
+ html += "<tr>"
502
+ html += f"<td>{metric}</td>"
503
+ html += f"<td>{stats['count']}</td>"
504
+ html += f"<td>{stats['mean']:.4g}</td>"
505
+ html += f"<td>{stats['std']:.4g}</td>"
506
+ html += f"<td>{stats['min']:.4g}</td>"
507
+ html += f"<td>{stats['median']:.4g}</td>"
508
+ html += f"<td>{stats['max']:.4g}</td>"
509
+ html += f"<td>{len(stats['outliers'])}</td>"
510
+ html += "</tr>\n"
511
+
512
+ html += "</table>\n"
513
+
514
+ # Outlier details
515
+ has_outliers = any(len(aggregated[m]["outliers"]) > 0 for m in metrics if m in aggregated)
516
+
517
+ if has_outliers:
518
+ html += "<h2>Outliers Detected</h2>\n"
519
+ for metric in metrics:
520
+ if metric not in aggregated:
521
+ continue
522
+ stats = aggregated[metric]
523
+ if stats["outliers"]:
524
+ html += f"<h3>{metric}</h3>\n<table>\n"
525
+ html += "<tr><th>File</th><th>Value</th></tr>\n"
526
+ for file, value in zip(stats["outlier_files"], stats["outliers"], strict=False):
527
+ html += f"<tr class='outlier'><td>{file}</td><td>{value:.4g}</td></tr>\n"
528
+ html += "</table>\n"
529
+
530
+ html += "</body>\n</html>"
531
+ return html