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,338 +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 export_vintage_logic_json(
267
- result: Any,
268
- path: str | Path,
269
- *,
270
- indent: int = 2,
271
- include_metadata: bool = True,
272
- ) -> None:
273
- """Export vintage logic analysis results to JSON.
274
-
275
- Convenience function for exporting VintageLogicAnalysisResult objects.
276
- The dataclass is automatically serialized to JSON with all nested objects.
277
-
278
- Args:
279
- result: VintageLogicAnalysisResult object.
280
- path: Output JSON file path.
281
- indent: Indentation level for pretty printing (default: 2).
282
- include_metadata: Include export metadata (default: True).
283
-
284
- Example:
285
- >>> from oscura.analyzers.digital.vintage import analyze_vintage_logic
286
- >>> result = analyze_vintage_logic(traces)
287
- >>> export_vintage_logic_json(result, "analysis_results.json")
288
- """
289
- path = Path(path)
290
-
291
- output: dict[str, Any] = {}
292
-
293
- if include_metadata:
294
- output["_metadata"] = {
295
- "format": "oscura_vintage_logic",
296
- "version": "1.0",
297
- "exported_at": datetime.now().isoformat(),
298
- "analysis_timestamp": result.timestamp.isoformat(),
299
- }
300
-
301
- output["analysis_result"] = result
302
-
303
- # Sanitize to handle inf/nan
304
- from oscura.reporting.output import _sanitize_for_serialization
305
-
306
- output = _sanitize_for_serialization(output)
307
-
308
- with open(path, "w") as f:
309
- json.dump(output, f, cls=OscuraJSONEncoder, indent=indent)
310
-
311
-
312
- def load_json(path: str | Path) -> dict[str, Any]:
313
- """Load JSON data file.
314
-
315
- Args:
316
- path: Input file path.
317
-
318
- Returns:
319
- Loaded data dictionary.
320
-
321
- Example:
322
- >>> data = load_json("results.json")
323
- >>> measurements = data.get("measurements", data.get("data", {}))
324
- """
325
- path = Path(path)
326
-
327
- with open(path) as f:
328
- return json.load(f) # type: ignore[no-any-return]
329
-
330
-
331
- __all__ = [
332
- "OscuraJSONEncoder",
333
- "export_json",
334
- "export_measurements",
335
- "export_protocol_decode",
336
- "export_vintage_logic_json",
337
- "load_json",
338
- ]
@@ -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
- ]