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
@@ -1,291 +0,0 @@
1
- """JSON export functionality.
2
-
3
- This module provides measurement results export to JSON format.
4
-
5
-
6
- Example:
7
- >>> from oscura.exporters.json_export import export_json
8
- >>> export_json(measurements, "results.json")
9
-
10
- References:
11
- RFC 8259 (JSON format)
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import json
17
- import math
18
- from dataclasses import asdict, is_dataclass
19
- from datetime import datetime
20
- from pathlib import Path
21
- from typing import Any
22
-
23
- import numpy as np
24
-
25
- from oscura.core.types import DigitalTrace, TraceMetadata, WaveformTrace
26
-
27
-
28
- class OscuraJSONEncoder(json.JSONEncoder):
29
- """JSON encoder with numpy, datetime, and Oscura object support."""
30
-
31
- def default(self, obj: Any) -> Any:
32
- if isinstance(obj, WaveformTrace):
33
- return {
34
- "_type": "WaveformTrace",
35
- "data": obj.data.tolist(),
36
- "metadata": self.default(obj.metadata),
37
- }
38
- if isinstance(obj, DigitalTrace):
39
- return {
40
- "_type": "DigitalTrace",
41
- "data": obj.data.tolist(),
42
- "metadata": self.default(obj.metadata),
43
- "edges": obj.edges,
44
- }
45
- if isinstance(obj, TraceMetadata):
46
- return {
47
- "_type": "TraceMetadata",
48
- "sample_rate": obj.sample_rate,
49
- "time_base": obj.time_base,
50
- "vertical_scale": obj.vertical_scale,
51
- "vertical_offset": obj.vertical_offset,
52
- "acquisition_time": obj.acquisition_time.isoformat()
53
- if obj.acquisition_time
54
- else None,
55
- "trigger_info": obj.trigger_info,
56
- "source_file": obj.source_file,
57
- "channel_name": obj.channel_name,
58
- }
59
- if isinstance(obj, np.ndarray):
60
- return obj.tolist()
61
- if isinstance(obj, np.integer | np.floating):
62
- val = float(obj)
63
- # Handle Infinity and NaN - convert to null for JSON compliance (RFC 8259)
64
- if math.isinf(val) or math.isnan(val):
65
- return None
66
- return val
67
- if isinstance(obj, np.bool_):
68
- return bool(obj)
69
- if isinstance(obj, float):
70
- # Also handle Python float inf/nan
71
- if math.isinf(obj) or math.isnan(obj):
72
- return None
73
- return obj
74
- if isinstance(obj, datetime):
75
- return obj.isoformat()
76
- if isinstance(obj, complex):
77
- # Handle complex with inf/nan components
78
- if (
79
- math.isinf(obj.real)
80
- or math.isnan(obj.real)
81
- or math.isinf(obj.imag)
82
- or math.isnan(obj.imag)
83
- ):
84
- return None
85
- return {"real": obj.real, "imag": obj.imag}
86
- if isinstance(obj, bytes):
87
- return obj.hex()
88
- if is_dataclass(obj):
89
- # Convert dataclasses to dict, then recursively encode
90
- return asdict(obj) # type: ignore[arg-type]
91
- return super().default(obj)
92
-
93
-
94
- def export_json(
95
- data: WaveformTrace | DigitalTrace | dict[str, Any] | list[Any],
96
- path: str | Path,
97
- *,
98
- pretty: bool = True,
99
- include_metadata: bool = True,
100
- compress: bool = False,
101
- ) -> None:
102
- """Export data to JSON format.
103
-
104
- Args:
105
- data: Data to export. Can be:
106
- - WaveformTrace or DigitalTrace (full trace with metadata)
107
- - Dictionary of measurements or data
108
- - List of data
109
- path: Output file path.
110
- pretty: Use pretty printing with indentation.
111
- include_metadata: Include export metadata.
112
- compress: Compress output (save as .json.gz).
113
-
114
- Example:
115
- >>> results = measure(trace)
116
- >>> export_json(results, "measurements.json")
117
- >>> export_json(trace, "waveform.json", pretty=True)
118
- >>> export_json(trace, "waveform.json.gz", compress=True)
119
-
120
- References:
121
- EXP-003
122
- """
123
- path = Path(path)
124
-
125
- output: dict[str, Any] = {}
126
-
127
- if include_metadata:
128
- output["_metadata"] = {
129
- "format": "oscura_json",
130
- "version": "1.0",
131
- "exported_at": datetime.now().isoformat(),
132
- }
133
-
134
- output["data"] = data
135
-
136
- # Sanitize to handle inf/nan in nested dictionaries (Python float inf/nan)
137
- # are handled directly by json encoder before calling default()
138
- from oscura.reporting.output import _sanitize_for_serialization
139
-
140
- output = _sanitize_for_serialization(output)
141
-
142
- # Serialize to JSON string
143
- if pretty:
144
- json_str = json.dumps(output, cls=OscuraJSONEncoder, indent=2)
145
- else:
146
- json_str = json.dumps(output, cls=OscuraJSONEncoder)
147
-
148
- # Write to file (with optional compression)
149
- if compress:
150
- import gzip
151
-
152
- # Ensure .gz extension
153
- if not str(path).endswith(".gz"):
154
- path = path.with_suffix(path.suffix + ".gz")
155
-
156
- with gzip.open(path, "wt", encoding="utf-8") as f:
157
- f.write(json_str)
158
- else:
159
- with open(path, "w", encoding="utf-8") as f:
160
- f.write(json_str)
161
-
162
-
163
- def export_measurements(
164
- measurements: dict[str, Any],
165
- path: str | Path,
166
- *,
167
- trace_info: dict[str, Any] | None = None,
168
- pretty: bool = True,
169
- ) -> None:
170
- """Export measurement results to JSON.
171
-
172
- Specialized function for measurement export with trace info.
173
-
174
- Args:
175
- measurements: Dictionary of measurements.
176
- path: Output file path.
177
- trace_info: Optional trace metadata.
178
- pretty: Use pretty printing.
179
-
180
- Example:
181
- >>> measurements = measure(trace)
182
- >>> trace_info = {
183
- ... "source_file": "scope_capture.wfm",
184
- ... "sample_rate": 1e9,
185
- ... "duration": 0.001
186
- ... }
187
- >>> export_measurements(measurements, "results.json", trace_info=trace_info)
188
- """
189
- path = Path(path)
190
-
191
- output = {
192
- "_metadata": {
193
- "format": "oscura_measurements",
194
- "version": "1.0",
195
- "exported_at": datetime.now().isoformat(),
196
- },
197
- "measurements": measurements,
198
- }
199
-
200
- if trace_info:
201
- output["trace_info"] = trace_info
202
-
203
- # Sanitize to ensure inf/nan handling
204
- from oscura.reporting.output import _sanitize_for_serialization
205
-
206
- output = _sanitize_for_serialization(output)
207
-
208
- with open(path, "w") as f:
209
- if pretty:
210
- json.dump(output, f, cls=OscuraJSONEncoder, indent=2)
211
- else:
212
- json.dump(output, f, cls=OscuraJSONEncoder)
213
-
214
-
215
- def export_protocol_decode(
216
- packets: list[dict[str, Any]],
217
- path: str | Path,
218
- *,
219
- protocol: str = "unknown",
220
- trace_info: dict[str, Any] | None = None,
221
- pretty: bool = True,
222
- ) -> None:
223
- """Export protocol decode results to JSON.
224
-
225
- Args:
226
- packets: List of decoded packets.
227
- path: Output file path.
228
- protocol: Protocol name.
229
- trace_info: Optional trace metadata.
230
- pretty: Use pretty printing.
231
-
232
- Example:
233
- >>> packets = [{"timestamp": 0.001, "data": "0x48"}]
234
- >>> export_protocol_decode(packets, "uart_decode.json", protocol="uart")
235
- """
236
- path = Path(path)
237
-
238
- output = {
239
- "_metadata": {
240
- "format": "oscura_protocol",
241
- "version": "1.0",
242
- "exported_at": datetime.now().isoformat(),
243
- "protocol": protocol,
244
- },
245
- "packets": packets,
246
- "summary": {
247
- "total_packets": len(packets),
248
- },
249
- }
250
-
251
- if trace_info:
252
- output["trace_info"] = trace_info
253
-
254
- # Sanitize to ensure inf/nan handling
255
- from oscura.reporting.output import _sanitize_for_serialization
256
-
257
- output = _sanitize_for_serialization(output)
258
-
259
- with open(path, "w") as f:
260
- if pretty:
261
- json.dump(output, f, cls=OscuraJSONEncoder, indent=2)
262
- else:
263
- json.dump(output, f, cls=OscuraJSONEncoder)
264
-
265
-
266
- def load_json(path: str | Path) -> dict[str, Any]:
267
- """Load JSON data file.
268
-
269
- Args:
270
- path: Input file path.
271
-
272
- Returns:
273
- Loaded data dictionary.
274
-
275
- Example:
276
- >>> data = load_json("results.json")
277
- >>> measurements = data.get("measurements", data.get("data", {}))
278
- """
279
- path = Path(path)
280
-
281
- with open(path) as f:
282
- return json.load(f) # type: ignore[no-any-return]
283
-
284
-
285
- __all__ = [
286
- "OscuraJSONEncoder",
287
- "export_json",
288
- "export_measurements",
289
- "export_protocol_decode",
290
- "load_json",
291
- ]
@@ -1,367 +0,0 @@
1
- """Markdown report export for Oscura.
2
-
3
- This module provides Markdown report generation with measurement tables,
4
- plot references, and configurable sections.
5
-
6
-
7
- Example:
8
- >>> from oscura.exporters.markdown_export import export_markdown
9
- >>> export_markdown(measurements, "report.md", 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
-
21
- def export_markdown(
22
- data: dict[str, Any],
23
- path: str | Path,
24
- *,
25
- title: str = "Oscura Analysis Report",
26
- author: str | None = None,
27
- include_plots: bool = True,
28
- embed_images: bool = True,
29
- sections: list[str] | None = None,
30
- ) -> None:
31
- """Export measurement results to Markdown format.
32
-
33
- Args:
34
- data: Dictionary containing measurement results, plots, and metadata.
35
- Expected keys:
36
- - "measurements": dict of name -> value pairs
37
- - "plots": list of matplotlib figures or paths
38
- - "metadata": optional dict of metadata
39
- - "summary": optional executive summary text
40
- path: Output file path.
41
- title: Report title.
42
- author: Author name (optional).
43
- include_plots: Include plots in report.
44
- embed_images: Embed images as base64 (True) or save separately (False).
45
- sections: List of sections to include. If None, includes all available.
46
- Options: "metadata", "summary", "measurements", "plots", "conclusions"
47
-
48
- References:
49
- EXP-006
50
- """
51
- lines: list[str] = []
52
-
53
- # Header
54
- lines.append(f"# {title}\n")
55
- lines.append("")
56
-
57
- # Metadata section
58
- if sections is None or "metadata" in sections:
59
- lines.extend(_generate_metadata_section(data, author))
60
-
61
- # Executive summary
62
- if (sections is None or "summary" in sections) and "summary" in data:
63
- lines.append("## Executive Summary\n")
64
- lines.append(data["summary"])
65
- lines.append("")
66
-
67
- # Measurements table
68
- if (sections is None or "measurements" in sections) and "measurements" in data:
69
- lines.extend(_generate_measurements_section(data["measurements"]))
70
-
71
- # Plots
72
- if include_plots and (sections is None or "plots" in sections) and "plots" in data:
73
- lines.extend(_generate_plots_section(data["plots"], path, embed_images))
74
-
75
- # Conclusions
76
- if (sections is None or "conclusions" in sections) and "conclusions" in data:
77
- lines.append("## Conclusions\n")
78
- lines.append(data["conclusions"])
79
- lines.append("")
80
-
81
- # Write to file
82
- content = "\n".join(lines)
83
- Path(path).write_text(content, encoding="utf-8")
84
-
85
-
86
- def _generate_metadata_section(data: dict[str, Any], author: str | None) -> list[str]:
87
- """Generate metadata section."""
88
- lines = ["## Report Information\n", ""]
89
-
90
- metadata = data.get("metadata", {})
91
-
92
- lines.append(f"- **Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
93
-
94
- if author:
95
- lines.append(f"- **Author**: {author}")
96
-
97
- if "filename" in metadata:
98
- lines.append(f"- **Source File**: `{metadata['filename']}`")
99
-
100
- if "sample_rate" in metadata:
101
- sr = metadata["sample_rate"]
102
- if sr >= 1e9:
103
- sr_str = f"{sr / 1e9:.3f} GS/s"
104
- elif sr >= 1e6:
105
- sr_str = f"{sr / 1e6:.3f} MS/s"
106
- elif sr >= 1e3:
107
- sr_str = f"{sr / 1e3:.3f} kS/s"
108
- else:
109
- sr_str = f"{sr:.3f} S/s"
110
- lines.append(f"- **Sample Rate**: {sr_str}")
111
-
112
- if "samples" in metadata:
113
- lines.append(f"- **Samples**: {metadata['samples']:,}")
114
-
115
- if "duration" in metadata:
116
- dur = metadata["duration"]
117
- if dur >= 1.0:
118
- dur_str = f"{dur:.3f} s"
119
- elif dur >= 1e-3:
120
- dur_str = f"{dur * 1e3:.3f} ms"
121
- elif dur >= 1e-6:
122
- dur_str = f"{dur * 1e6:.3f} us"
123
- else:
124
- dur_str = f"{dur * 1e9:.3f} ns"
125
- lines.append(f"- **Duration**: {dur_str}")
126
-
127
- lines.append("")
128
- return lines
129
-
130
-
131
- def _generate_measurements_section(measurements: dict[str, Any]) -> list[str]:
132
- """Generate measurements table section."""
133
- lines = ["## Measurement Results\n", ""]
134
-
135
- if not measurements:
136
- lines.append("*No measurements available.*\n")
137
- return lines
138
-
139
- # Create table header
140
- lines.append("| Parameter | Value | Unit | Status |")
141
- lines.append("|-----------|-------|------|--------|")
142
-
143
- for name, value in measurements.items():
144
- if isinstance(value, dict):
145
- # Structured measurement with value, unit, status
146
- val = value.get("value", "N/A")
147
- unit = value.get("unit", "")
148
- status = value.get("status", "")
149
-
150
- # Format value
151
- val_str = _format_value(val, unit) if isinstance(val, float) else str(val)
152
-
153
- # Format status with emoji
154
- if status.upper() == "PASS":
155
- status_str = "PASS"
156
- elif status.upper() == "FAIL":
157
- status_str = "FAIL"
158
- elif status.upper() == "WARNING":
159
- status_str = "WARNING"
160
- else:
161
- status_str = status
162
-
163
- lines.append(f"| {name} | {val_str} | {unit} | {status_str} |")
164
- else:
165
- # Simple value
166
- val_str = f"{value:.6g}" if isinstance(value, float) else str(value)
167
- lines.append(f"| {name} | {val_str} | - | - |")
168
-
169
- lines.append("")
170
- return lines
171
-
172
-
173
- def _format_value(value: float, unit: str) -> str:
174
- """Format value with appropriate SI prefix."""
175
- if value == 0:
176
- return "0"
177
-
178
- abs_val = abs(value)
179
-
180
- # Time units
181
- if unit in ("s", "sec", "seconds"):
182
- if abs_val >= 1.0:
183
- return f"{value:.6g}"
184
- elif abs_val >= 1e-3:
185
- return f"{value * 1e3:.6g} m"
186
- elif abs_val >= 1e-6:
187
- return f"{value * 1e6:.6g} u"
188
- elif abs_val >= 1e-9:
189
- return f"{value * 1e9:.6g} n"
190
- else:
191
- return f"{value * 1e12:.6g} p"
192
-
193
- # Frequency units
194
- if unit in ("Hz", "hz"):
195
- if abs_val >= 1e9:
196
- return f"{value / 1e9:.6g} G"
197
- elif abs_val >= 1e6:
198
- return f"{value / 1e6:.6g} M"
199
- elif abs_val >= 1e3:
200
- return f"{value / 1e3:.6g} k"
201
- else:
202
- return f"{value:.6g}"
203
-
204
- # Voltage units
205
- if unit in ("V", "v", "volts"):
206
- if abs_val >= 1.0:
207
- return f"{value:.6g}"
208
- elif abs_val >= 1e-3:
209
- return f"{value * 1e3:.6g} m"
210
- elif abs_val >= 1e-6:
211
- return f"{value * 1e6:.6g} u"
212
- else:
213
- return f"{value * 1e9:.6g} n"
214
-
215
- # Default formatting
216
- return f"{value:.6g}"
217
-
218
-
219
- def _generate_plots_section(
220
- plots: list[Any],
221
- report_path: str | Path,
222
- embed_images: bool,
223
- ) -> list[str]:
224
- """Generate plots section."""
225
- lines = ["## Plots and Visualizations\n", ""]
226
-
227
- report_path = Path(report_path)
228
- plots_dir = report_path.parent / f"{report_path.stem}_plots"
229
-
230
- for i, plot in enumerate(plots, start=1):
231
- if isinstance(plot, dict):
232
- # Plot with metadata
233
- fig = plot.get("figure")
234
- caption = plot.get("caption", f"Figure {i}")
235
- alt_text = plot.get("alt_text", caption)
236
- else:
237
- fig = plot
238
- caption = f"Figure {i}"
239
- alt_text = caption
240
-
241
- if fig is None:
242
- continue
243
-
244
- if isinstance(fig, str | Path):
245
- # Path to existing image
246
- if embed_images:
247
- # Read and embed as base64
248
- try:
249
- img_data = Path(fig).read_bytes()
250
- img_ext = Path(fig).suffix.lower()
251
- mime_type = {
252
- ".png": "image/png",
253
- ".jpg": "image/jpeg",
254
- ".jpeg": "image/jpeg",
255
- ".svg": "image/svg+xml",
256
- }.get(img_ext, "image/png")
257
-
258
- b64 = base64.b64encode(img_data).decode("utf-8")
259
- lines.append(f"### {caption}\n")
260
- lines.append(f"![{alt_text}](data:{mime_type};base64,{b64})\n")
261
- except Exception:
262
- lines.append(f"### {caption}\n")
263
- lines.append(f"*Unable to embed image: {fig}*\n")
264
- else:
265
- lines.append(f"### {caption}\n")
266
- lines.append(f"![{alt_text}]({fig})\n")
267
- else:
268
- # Matplotlib figure
269
- try:
270
- if embed_images:
271
- # Embed as base64 PNG
272
- buf = BytesIO()
273
- fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
274
- buf.seek(0)
275
- b64 = base64.b64encode(buf.read()).decode("utf-8")
276
- lines.append(f"### {caption}\n")
277
- lines.append(f"![{alt_text}](data:image/png;base64,{b64})\n")
278
- else:
279
- # Save to separate file
280
- plots_dir.mkdir(exist_ok=True)
281
- plot_path = plots_dir / f"figure_{i}.png"
282
- fig.savefig(plot_path, format="png", dpi=150, bbox_inches="tight")
283
- rel_path = plot_path.relative_to(report_path.parent)
284
- lines.append(f"### {caption}\n")
285
- lines.append(f"![{alt_text}]({rel_path})\n")
286
- except Exception as e:
287
- lines.append(f"### {caption}\n")
288
- lines.append(f"*Unable to render figure: {e}*\n")
289
-
290
- lines.append("")
291
-
292
- return lines
293
-
294
-
295
- def generate_markdown_report(
296
- data: dict[str, Any],
297
- *,
298
- title: str = "Oscura Analysis Report",
299
- author: str | None = None,
300
- include_plots: bool = True,
301
- embed_images: bool = True,
302
- sections: list[str] | None = None,
303
- ) -> str:
304
- """Generate Markdown report as string.
305
-
306
- Args:
307
- data: Dictionary containing measurement results, plots, and metadata.
308
- title: Report title.
309
- author: Author name (optional).
310
- include_plots: Include plots in report.
311
- embed_images: Embed images as base64.
312
- sections: List of sections to include.
313
-
314
- Returns:
315
- Markdown content as string.
316
-
317
- References:
318
- EXP-006
319
- """
320
- lines: list[str] = []
321
-
322
- # Header
323
- lines.append(f"# {title}\n")
324
- lines.append("")
325
-
326
- # Metadata section
327
- if sections is None or "metadata" in sections:
328
- lines.extend(_generate_metadata_section(data, author))
329
-
330
- # Executive summary
331
- if (sections is None or "summary" in sections) and "summary" in data:
332
- lines.append("## Executive Summary\n")
333
- lines.append(data["summary"])
334
- lines.append("")
335
-
336
- # Measurements table
337
- if (sections is None or "measurements" in sections) and "measurements" in data:
338
- lines.extend(_generate_measurements_section(data["measurements"]))
339
-
340
- # For string generation, only include plots if embed_images is True
341
- if include_plots and embed_images and (sections is None or "plots" in sections):
342
- if "plots" in data:
343
- # Simplified plot handling for string output
344
- lines.append("## Plots and Visualizations\n")
345
- lines.append("")
346
- for i, plot in enumerate(data["plots"], start=1):
347
- if isinstance(plot, dict):
348
- caption = plot.get("caption", f"Figure {i}")
349
- else:
350
- caption = f"Figure {i}"
351
- lines.append(f"### {caption}\n")
352
- lines.append("*[Embedded plot - save to file to view]*\n")
353
- lines.append("")
354
-
355
- # Conclusions
356
- if (sections is None or "conclusions" in sections) and "conclusions" in data:
357
- lines.append("## Conclusions\n")
358
- lines.append(data["conclusions"])
359
- lines.append("")
360
-
361
- return "\n".join(lines)
362
-
363
-
364
- __all__ = [
365
- "export_markdown",
366
- "generate_markdown_report",
367
- ]