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,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