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
@@ -1,701 +0,0 @@
1
- """HTML report export for Oscura.
2
-
3
- This module provides interactive HTML report generation with embedded Plotly charts,
4
- measurement tables, and custom styling/theming.
5
-
6
-
7
- Example:
8
- >>> from oscura.exporters.html_export import export_html
9
- >>> export_html(measurements, "report.html", title="Analysis Report")
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import base64
15
- from datetime import datetime
16
- from io import BytesIO
17
- from pathlib import Path
18
- from typing import Any
19
-
20
- # HTML template with modern styling
21
- HTML_TEMPLATE = """<!DOCTYPE html>
22
- <html lang="en">
23
- <head>
24
- <meta charset="UTF-8">
25
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
26
- <meta name="generator" content="Oscura Export">
27
- <title>{title}</title>
28
- {plotly_script}
29
- <style>
30
- :root {{
31
- --primary-color: #2c3e50;
32
- --secondary-color: #3498db;
33
- --success-color: #27ae60;
34
- --warning-color: #f39c12;
35
- --danger-color: #e74c3c;
36
- --bg-color: #ffffff;
37
- --text-color: #333333;
38
- --border-color: #dddddd;
39
- --table-header-bg: #f2f2f2;
40
- --table-alt-row-bg: #f9f9f9;
41
- }}
42
-
43
- {dark_mode_styles}
44
-
45
- * {{
46
- box-sizing: border-box;
47
- margin: 0;
48
- padding: 0;
49
- }}
50
-
51
- body {{
52
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
53
- font-size: 14px;
54
- line-height: 1.6;
55
- color: var(--text-color);
56
- background-color: var(--bg-color);
57
- padding: 20px;
58
- }}
59
-
60
- .container {{
61
- max-width: 1200px;
62
- margin: 0 auto;
63
- }}
64
-
65
- header {{
66
- margin-bottom: 30px;
67
- border-bottom: 3px solid var(--primary-color);
68
- padding-bottom: 20px;
69
- }}
70
-
71
- h1 {{
72
- font-size: 28px;
73
- color: var(--primary-color);
74
- margin-bottom: 10px;
75
- }}
76
-
77
- h2 {{
78
- font-size: 20px;
79
- color: var(--primary-color);
80
- margin-top: 30px;
81
- margin-bottom: 15px;
82
- border-bottom: 1px solid var(--border-color);
83
- padding-bottom: 8px;
84
- }}
85
-
86
- h3 {{
87
- font-size: 16px;
88
- color: var(--primary-color);
89
- margin-top: 20px;
90
- margin-bottom: 10px;
91
- }}
92
-
93
- .metadata {{
94
- background-color: var(--table-alt-row-bg);
95
- padding: 15px;
96
- border-radius: 5px;
97
- font-size: 13px;
98
- color: #666;
99
- }}
100
-
101
- .metadata span {{
102
- margin-right: 20px;
103
- }}
104
-
105
- table {{
106
- width: 100%;
107
- border-collapse: collapse;
108
- margin: 15px 0;
109
- }}
110
-
111
- th, td {{
112
- padding: 10px 12px;
113
- text-align: left;
114
- border: 1px solid var(--border-color);
115
- }}
116
-
117
- th {{
118
- background-color: var(--table-header-bg);
119
- font-weight: 600;
120
- }}
121
-
122
- tr:nth-child(even) {{
123
- background-color: var(--table-alt-row-bg);
124
- }}
125
-
126
- tr:hover {{
127
- background-color: rgba(52, 152, 219, 0.1);
128
- }}
129
-
130
- .pass {{
131
- color: var(--success-color);
132
- font-weight: 600;
133
- }}
134
-
135
- .pass::before {{
136
- content: '\\2713 ';
137
- }}
138
-
139
- .fail {{
140
- color: var(--danger-color);
141
- font-weight: 600;
142
- }}
143
-
144
- .fail::before {{
145
- content: '\\2717 ';
146
- }}
147
-
148
- .warning {{
149
- color: var(--warning-color);
150
- font-weight: 600;
151
- }}
152
-
153
- .summary {{
154
- background-color: rgba(52, 152, 219, 0.1);
155
- border-left: 4px solid var(--secondary-color);
156
- padding: 15px;
157
- margin: 20px 0;
158
- }}
159
-
160
- .plot-container {{
161
- margin: 20px 0;
162
- padding: 15px;
163
- background-color: var(--bg-color);
164
- border: 1px solid var(--border-color);
165
- border-radius: 5px;
166
- }}
167
-
168
- .plot-container img {{
169
- max-width: 100%;
170
- height: auto;
171
- display: block;
172
- margin: 0 auto;
173
- }}
174
-
175
- .plot-caption {{
176
- text-align: center;
177
- font-style: italic;
178
- margin-top: 10px;
179
- color: #666;
180
- }}
181
-
182
- footer {{
183
- margin-top: 40px;
184
- padding-top: 20px;
185
- border-top: 1px solid var(--border-color);
186
- text-align: center;
187
- font-size: 12px;
188
- color: #888;
189
- }}
190
-
191
- @media (max-width: 768px) {{
192
- body {{
193
- padding: 10px;
194
- }}
195
-
196
- h1 {{
197
- font-size: 22px;
198
- }}
199
-
200
- table {{
201
- font-size: 12px;
202
- }}
203
-
204
- th, td {{
205
- padding: 6px 8px;
206
- }}
207
- }}
208
-
209
- @media print {{
210
- body {{
211
- padding: 0;
212
- }}
213
-
214
- .container {{
215
- max-width: 100%;
216
- }}
217
- }}
218
- </style>
219
- </head>
220
- <body{body_class}>
221
- <div class="container">
222
- <header>
223
- <h1>{title}</h1>
224
- <div class="metadata">
225
- {metadata_html}
226
- </div>
227
- </header>
228
-
229
- {summary_html}
230
-
231
- {measurements_html}
232
-
233
- {plots_html}
234
-
235
- {conclusions_html}
236
-
237
- <footer>
238
- Generated by Oscura &middot; {timestamp}
239
- </footer>
240
- </div>
241
- </body>
242
- </html>
243
- """
244
- DARK_MODE_CSS = """
245
- @media (prefers-color-scheme: dark) {
246
- :root {
247
- --bg-color: #1e1e1e;
248
- --text-color: #e0e0e0;
249
- --border-color: #444444;
250
- --table-header-bg: #2d2d2d;
251
- --table-alt-row-bg: #252525;
252
- }
253
- }
254
-
255
- body.dark-mode {
256
- --bg-color: #1e1e1e;
257
- --text-color: #e0e0e0;
258
- --border-color: #444444;
259
- --table-header-bg: #2d2d2d;
260
- --table-alt-row-bg: #252525;
261
- }
262
- """
263
-
264
-
265
- def export_html(
266
- data: dict[str, Any],
267
- path: str | Path,
268
- *,
269
- title: str = "Oscura Analysis Report",
270
- author: str | None = None,
271
- include_plots: bool = True,
272
- self_contained: bool = True,
273
- interactive: bool = True,
274
- dark_mode: bool = False,
275
- theme: str | None = None,
276
- ) -> None:
277
- """Export measurement results to interactive HTML format.
278
-
279
- Args:
280
- data: Dictionary containing measurement results, plots, and metadata.
281
- Expected keys:
282
- - "measurements": dict of name -> value pairs
283
- - "plots": list of matplotlib/plotly figures or paths
284
- - "metadata": optional dict of metadata
285
- - "summary": optional executive summary text
286
- - "conclusions": optional conclusions text
287
- path: Output file path.
288
- title: Report title.
289
- author: Author name (optional).
290
- include_plots: Include plots in report.
291
- self_contained: Embed all resources inline (True) or save separately.
292
- interactive: Use Plotly for interactive charts when available.
293
- dark_mode: Enable dark mode styling.
294
- theme: Custom theme name (reserved for future use).
295
-
296
- References:
297
- EXP-007
298
- """
299
- html_content = generate_html_report(
300
- data,
301
- title=title,
302
- author=author,
303
- include_plots=include_plots,
304
- self_contained=self_contained,
305
- interactive=interactive,
306
- dark_mode=dark_mode,
307
- theme=theme,
308
- )
309
-
310
- Path(path).write_text(html_content, encoding="utf-8")
311
-
312
-
313
- def generate_html_report(
314
- data: dict[str, Any],
315
- *,
316
- title: str = "Oscura Analysis Report",
317
- author: str | None = None,
318
- include_plots: bool = True,
319
- self_contained: bool = True,
320
- interactive: bool = True,
321
- dark_mode: bool = False,
322
- theme: str | None = None,
323
- ) -> str:
324
- """Generate HTML report as string.
325
-
326
- Args:
327
- data: Dictionary containing measurement results, plots, and metadata.
328
- title: Report title.
329
- author: Author name (optional).
330
- include_plots: Include plots in report.
331
- self_contained: Embed all resources inline.
332
- interactive: Use Plotly for interactive charts when available.
333
- dark_mode: Enable dark mode styling.
334
- theme: Custom theme name (reserved for future use).
335
-
336
- Returns:
337
- HTML content as string.
338
-
339
- References:
340
- EXP-007
341
- """
342
- # Metadata HTML
343
- metadata_parts = []
344
- metadata = data.get("metadata", {})
345
-
346
- if author:
347
- metadata_parts.append(f"<span><strong>Author:</strong> {author}</span>")
348
-
349
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
350
- metadata_parts.append(f"<span><strong>Generated:</strong> {timestamp}</span>")
351
-
352
- if "filename" in metadata:
353
- metadata_parts.append(
354
- f"<span><strong>Source:</strong> {_html_escape(metadata['filename'])}</span>"
355
- )
356
-
357
- if "sample_rate" in metadata:
358
- sr = metadata["sample_rate"]
359
- sr_str = _format_sample_rate(sr)
360
- metadata_parts.append(f"<span><strong>Sample Rate:</strong> {sr_str}</span>")
361
-
362
- if "samples" in metadata:
363
- metadata_parts.append(f"<span><strong>Samples:</strong> {metadata['samples']:,}</span>")
364
-
365
- metadata_html = "\n ".join(metadata_parts)
366
-
367
- # Summary HTML
368
- summary_html = ""
369
- if "summary" in data:
370
- summary_html = f"""
371
- <section id="summary">
372
- <h2>Executive Summary</h2>
373
- <div class="summary">
374
- <p>{_html_escape(data["summary"])}</p>
375
- </div>
376
- </section>
377
- """
378
- # Measurements HTML
379
- measurements_html = ""
380
- if "measurements" in data:
381
- measurements_html = _generate_measurements_html(data["measurements"])
382
-
383
- # Plots HTML
384
- plots_html = ""
385
- plotly_script = ""
386
- if include_plots and "plots" in data:
387
- plots_html, plotly_script = _generate_plots_html(data["plots"], self_contained, interactive)
388
-
389
- # Conclusions HTML
390
- conclusions_html = ""
391
- if "conclusions" in data:
392
- conclusions_html = f"""
393
- <section id="conclusions">
394
- <h2>Conclusions</h2>
395
- <p>{_html_escape(data["conclusions"])}</p>
396
- </section>
397
- """
398
- # Dark mode styles
399
- dark_mode_styles = DARK_MODE_CSS if dark_mode else ""
400
-
401
- # Body class for dark mode
402
- body_class = ' class="dark-mode"' if dark_mode else ""
403
-
404
- # Generate final HTML
405
- html = HTML_TEMPLATE.format(
406
- title=_html_escape(title),
407
- plotly_script=plotly_script,
408
- dark_mode_styles=dark_mode_styles,
409
- body_class=body_class,
410
- metadata_html=metadata_html,
411
- summary_html=summary_html,
412
- measurements_html=measurements_html,
413
- plots_html=plots_html,
414
- conclusions_html=conclusions_html,
415
- timestamp=timestamp,
416
- )
417
-
418
- return html
419
-
420
-
421
- def _html_escape(text: str) -> str:
422
- """Escape HTML special characters."""
423
- return (
424
- text.replace("&", "&amp;")
425
- .replace("<", "&lt;")
426
- .replace(">", "&gt;")
427
- .replace('"', "&quot;")
428
- .replace("'", "&#39;")
429
- )
430
-
431
-
432
- def _format_sample_rate(sr: float) -> str:
433
- """Format sample rate with SI prefix."""
434
- if sr >= 1e9:
435
- return f"{sr / 1e9:.3f} GS/s"
436
- elif sr >= 1e6:
437
- return f"{sr / 1e6:.3f} MS/s"
438
- elif sr >= 1e3:
439
- return f"{sr / 1e3:.3f} kS/s"
440
- else:
441
- return f"{sr:.3f} S/s"
442
-
443
-
444
- def _format_value(value: float, unit: str) -> str:
445
- """Format value with appropriate precision."""
446
- if value == 0:
447
- return "0"
448
-
449
- abs_val = abs(value)
450
-
451
- # Time units
452
- if unit in ("s", "sec", "seconds"):
453
- if abs_val >= 1.0:
454
- return f"{value:.6g} s"
455
- elif abs_val >= 1e-3:
456
- return f"{value * 1e3:.6g} ms"
457
- elif abs_val >= 1e-6:
458
- return f"{value * 1e6:.6g} us"
459
- elif abs_val >= 1e-9:
460
- return f"{value * 1e9:.6g} ns"
461
- else:
462
- return f"{value * 1e12:.6g} ps"
463
-
464
- # Frequency units
465
- if unit in ("Hz", "hz"):
466
- if abs_val >= 1e9:
467
- return f"{value / 1e9:.6g} GHz"
468
- elif abs_val >= 1e6:
469
- return f"{value / 1e6:.6g} MHz"
470
- elif abs_val >= 1e3:
471
- return f"{value / 1e3:.6g} kHz"
472
- else:
473
- return f"{value:.6g} Hz"
474
-
475
- # Default formatting
476
- if unit:
477
- return f"{value:.6g} {unit}"
478
- return f"{value:.6g}"
479
-
480
-
481
- def _generate_measurements_html(measurements: dict[str, Any]) -> str:
482
- """Generate measurements table HTML."""
483
- if not measurements:
484
- return ""
485
-
486
- rows = []
487
- for name, value in measurements.items():
488
- if isinstance(value, dict):
489
- val = value.get("value", "N/A")
490
- unit = value.get("unit", "")
491
- status = value.get("status", "")
492
-
493
- val_str = _format_value(val, unit) if isinstance(val, float) else str(val)
494
-
495
- # Status class and formatting
496
- status_upper = str(status).upper()
497
- if status_upper == "PASS":
498
- status_html = '<span class="pass">PASS</span>'
499
- elif status_upper == "FAIL":
500
- status_html = '<span class="fail">FAIL</span>'
501
- elif status_upper == "WARNING":
502
- status_html = '<span class="warning">WARNING</span>'
503
- else:
504
- status_html = _html_escape(str(status))
505
-
506
- rows.append(
507
- f"<tr><td>{_html_escape(name)}</td>"
508
- f"<td>{_html_escape(val_str)}</td>"
509
- f"<td>{_html_escape(unit)}</td>"
510
- f"<td>{status_html}</td></tr>"
511
- )
512
- else:
513
- val_str = f"{value:.6g}" if isinstance(value, float) else str(value)
514
-
515
- rows.append(
516
- f"<tr><td>{_html_escape(name)}</td>"
517
- f"<td>{_html_escape(val_str)}</td>"
518
- f"<td>-</td><td>-</td></tr>"
519
- )
520
-
521
- return f"""
522
- <section id="measurements">
523
- <h2>Measurement Results</h2>
524
- <table>
525
- <thead>
526
- <tr>
527
- <th>Parameter</th>
528
- <th>Value</th>
529
- <th>Unit</th>
530
- <th>Status</th>
531
- </tr>
532
- </thead>
533
- <tbody>
534
- {"".join(rows)}
535
- </tbody>
536
- </table>
537
- </section>
538
- """
539
-
540
-
541
- def _generate_plots_html(
542
- plots: list[Any],
543
- self_contained: bool,
544
- interactive: bool,
545
- ) -> tuple[str, str]:
546
- """Generate plots HTML and Plotly script if needed.
547
-
548
- Args:
549
- plots: List of plot objects (matplotlib figures, plotly figures, or paths).
550
- self_contained: Embed all resources inline (True) or reference externally.
551
- interactive: Use Plotly for interactive charts when available.
552
-
553
- Returns:
554
- Tuple of (plots_html, plotly_script_tag)
555
- """
556
- if not plots:
557
- return "", ""
558
-
559
- plot_divs = []
560
- has_plotly = False
561
-
562
- for i, plot in enumerate(plots, start=1):
563
- if isinstance(plot, dict):
564
- fig = plot.get("figure")
565
- caption = plot.get("caption", f"Figure {i}")
566
- else:
567
- fig = plot
568
- caption = f"Figure {i}"
569
-
570
- if fig is None:
571
- continue
572
-
573
- # Check if it's a Plotly figure
574
- plotly_html = _try_render_plotly(fig, interactive)
575
- if plotly_html:
576
- has_plotly = True
577
- plot_divs.append(
578
- f'<div class="plot-container"><h3>{_html_escape(caption)}</h3>{plotly_html}</div>'
579
- )
580
- continue
581
-
582
- # Try matplotlib figure
583
- img_html = _try_render_matplotlib(fig, self_contained)
584
- if img_html:
585
- plot_divs.append(
586
- f'<div class="plot-container">'
587
- f"<h3>{_html_escape(caption)}</h3>"
588
- f"{img_html}"
589
- f'<div class="plot-caption">{_html_escape(caption)}</div>'
590
- f"</div>"
591
- )
592
- continue
593
-
594
- # Image path
595
- if isinstance(fig, str | Path):
596
- if self_contained:
597
- try:
598
- img_data = Path(fig).read_bytes()
599
- img_ext = Path(fig).suffix.lower()
600
- mime_type = {
601
- ".png": "image/png",
602
- ".jpg": "image/jpeg",
603
- ".jpeg": "image/jpeg",
604
- ".svg": "image/svg+xml",
605
- }.get(img_ext, "image/png")
606
-
607
- b64 = base64.b64encode(img_data).decode("utf-8")
608
- img_html = (
609
- f'<img src="data:{mime_type};base64,{b64}" alt="{_html_escape(caption)}">'
610
- )
611
- except Exception:
612
- img_html = f"<p><em>Unable to embed image: {fig}</em></p>"
613
- else:
614
- img_html = f'<img src="{_html_escape(str(fig))}" alt="{_html_escape(caption)}">'
615
-
616
- plot_divs.append(
617
- f'<div class="plot-container">'
618
- f"<h3>{_html_escape(caption)}</h3>"
619
- f"{img_html}"
620
- f'<div class="plot-caption">{_html_escape(caption)}</div>'
621
- f"</div>"
622
- )
623
-
624
- # Plotly CDN script (only included if we have Plotly figures)
625
- plotly_script = ""
626
- if has_plotly:
627
- plotly_script = '<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>'
628
-
629
- plots_html = (
630
- f"""
631
- <section id="plots">
632
- <h2>Plots and Visualizations</h2>
633
- {"".join(plot_divs)}
634
- </section>
635
- """
636
- if plot_divs
637
- else ""
638
- )
639
-
640
- return plots_html, plotly_script
641
-
642
-
643
- def _try_render_plotly(fig: Any, interactive: bool) -> str | None:
644
- """Try to render a Plotly figure to HTML.
645
-
646
- Args:
647
- fig: Figure object to render (may be Plotly figure or other type).
648
- interactive: Enable interactive Plotly rendering.
649
-
650
- Returns:
651
- HTML string if successful, None if not a Plotly figure.
652
- """
653
- if not interactive:
654
- return None
655
-
656
- try:
657
- import plotly.graph_objects as go # type: ignore[import-not-found]
658
-
659
- if isinstance(fig, go.Figure):
660
- return fig.to_html( # type: ignore[no-any-return]
661
- full_html=False,
662
- include_plotlyjs=False,
663
- config={"displayModeBar": True, "responsive": True},
664
- )
665
- except ImportError:
666
- pass
667
-
668
- return None
669
-
670
-
671
- def _try_render_matplotlib(fig: Any, self_contained: bool) -> str | None:
672
- """Try to render a Matplotlib figure to HTML.
673
-
674
- Args:
675
- fig: Figure object to render (may be matplotlib figure or other type).
676
- self_contained: Embed image as base64 data URI.
677
-
678
- Returns:
679
- HTML img tag if successful, None if not a Matplotlib figure.
680
- """
681
- try:
682
- import matplotlib.pyplot as plt
683
-
684
- if hasattr(fig, "savefig"):
685
- buf = BytesIO()
686
- fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
687
- buf.seek(0)
688
- b64 = base64.b64encode(buf.read()).decode("utf-8")
689
- return f'<img src="data:image/png;base64,{b64}" alt="Figure">'
690
- except ImportError:
691
- pass
692
- except Exception:
693
- pass
694
-
695
- return None
696
-
697
-
698
- __all__ = [
699
- "export_html",
700
- "generate_html_report",
701
- ]