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
@@ -142,68 +142,111 @@ def _export_docx(
142
142
  References:
143
143
  REPORT-019
144
144
  """
145
+ doc = _create_docx_document()
146
+ path = output_path.with_suffix(".docx")
147
+
148
+ _add_docx_header(doc, report)
149
+ _add_docx_sections(doc, report)
150
+
151
+ doc.save(str(path))
152
+ return path
153
+
154
+
155
+ def _create_docx_document() -> Any:
156
+ """Create and configure DOCX document.
157
+
158
+ Returns:
159
+ Document object from python-docx.
160
+
161
+ Raises:
162
+ ImportError: If python-docx not installed.
163
+ """
145
164
  try:
146
- from docx import Document # type: ignore[import-not-found]
147
- from docx.enum.text import ( # type: ignore[import-not-found]
148
- WD_ALIGN_PARAGRAPH, # type: ignore[import-not-found]
149
- )
150
- from docx.shared import ( # noqa: F401 # type: ignore[import-not-found]
151
- Inches,
152
- Pt,
153
- RGBColor,
154
- )
165
+ from docx import Document
155
166
  except ImportError:
156
- raise ImportError( # noqa: B904
167
+ raise ImportError(
157
168
  "python-docx is required for DOCX export. Install with: pip install python-docx"
158
169
  )
170
+ return Document()
159
171
 
160
- path = output_path.with_suffix(".docx")
161
- doc = Document()
162
172
 
163
- # Add title
173
+ def _add_docx_header(doc: Any, report: Report) -> None:
174
+ """Add title and metadata to DOCX document.
175
+
176
+ Args:
177
+ doc: Document object.
178
+ report: Report to extract metadata from.
179
+ """
180
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
181
+
164
182
  title = doc.add_heading(report.config.title, level=0)
165
183
  title.alignment = WD_ALIGN_PARAGRAPH.CENTER
166
184
 
167
- # Add metadata
168
185
  if report.config.author:
169
186
  doc.add_paragraph(f"Author: {report.config.author}")
170
187
  doc.add_paragraph(f"Date: {report.config.created.strftime('%Y-%m-%d %H:%M')}")
171
- doc.add_paragraph() # Blank line
188
+ doc.add_paragraph()
189
+
190
+
191
+ def _add_docx_sections(doc: Any, report: Report) -> None:
192
+ """Add all sections to DOCX document.
172
193
 
173
- # Add sections
194
+ Args:
195
+ doc: Document object.
196
+ report: Report with sections to add.
197
+ """
174
198
  for section in report.sections:
175
199
  if not section.visible:
176
200
  continue
177
201
 
178
- # Section heading
179
202
  doc.add_heading(section.title, level=section.level)
203
+ _add_docx_section_content(doc, section)
204
+ _add_docx_subsections(doc, section)
180
205
 
181
- # Section content
182
- if isinstance(section.content, str):
183
- doc.add_paragraph(section.content)
184
-
185
- elif isinstance(section.content, list):
186
- for item in section.content:
187
- if isinstance(item, dict):
188
- if item.get("type") == "table":
189
- _add_table_to_docx(doc, item)
190
- elif item.get("type") == "figure":
191
- # Placeholder for figures
192
- doc.add_paragraph(f"[Figure: {item.get('caption', 'N/A')}]")
193
- else:
194
- doc.add_paragraph(str(item))
195
-
196
- # Subsections
197
- for subsec in section.subsections:
198
- if not subsec.visible:
199
- continue
200
- doc.add_heading(subsec.title, level=subsec.level)
201
- if isinstance(subsec.content, str):
202
- doc.add_paragraph(subsec.content)
203
-
204
- # Save document
205
- doc.save(str(path))
206
- return path
206
+
207
+ def _add_docx_section_content(doc: Any, section: Any) -> None:
208
+ """Add section content to DOCX.
209
+
210
+ Args:
211
+ doc: Document object.
212
+ section: Section with content to add.
213
+ """
214
+ if isinstance(section.content, str):
215
+ doc.add_paragraph(section.content)
216
+ elif isinstance(section.content, list):
217
+ for item in section.content:
218
+ _add_docx_content_item(doc, item)
219
+
220
+
221
+ def _add_docx_content_item(doc: Any, item: Any) -> None:
222
+ """Add single content item to DOCX.
223
+
224
+ Args:
225
+ doc: Document object.
226
+ item: Content item (dict or other).
227
+ """
228
+ if isinstance(item, dict):
229
+ if item.get("type") == "table":
230
+ _add_table_to_docx(doc, item)
231
+ elif item.get("type") == "figure":
232
+ doc.add_paragraph(f"[Figure: {item.get('caption', 'N/A')}]")
233
+ else:
234
+ doc.add_paragraph(str(item))
235
+
236
+
237
+ def _add_docx_subsections(doc: Any, section: Any) -> None:
238
+ """Add subsections to DOCX document.
239
+
240
+ Args:
241
+ doc: Document object.
242
+ section: Section with subsections.
243
+ """
244
+ for subsec in section.subsections:
245
+ if not subsec.visible:
246
+ continue
247
+ doc.add_heading(subsec.title, level=subsec.level)
248
+ if isinstance(subsec.content, str):
249
+ doc.add_paragraph(subsec.content)
207
250
 
208
251
 
209
252
  def _add_table_to_docx(doc: Any, table_dict: dict[str, Any]) -> None:
@@ -70,6 +70,68 @@ class NumberFormatter:
70
70
  self.precision = self.sig_figs
71
71
  self.use_si = self.auto_scale
72
72
 
73
+ def _format_without_scaling(self, value: float, places: int, unit: str) -> str:
74
+ """Format value without SI scaling.
75
+
76
+ Args:
77
+ value: Value to format
78
+ places: Decimal places
79
+ unit: Unit string
80
+
81
+ Returns:
82
+ Formatted string
83
+ """
84
+ abs_val = abs(value)
85
+
86
+ if abs_val != 0 and abs_val < 1:
87
+ from math import floor, log10
88
+
89
+ order = floor(log10(abs_val))
90
+ decimal_places_needed = max(places, abs(order) + 1)
91
+ return f"{value:.{decimal_places_needed}f} {unit}".strip()
92
+
93
+ if abs_val >= 1e6:
94
+ return f"{value:.{places}e} {unit}".strip()
95
+
96
+ return f"{value:.{places}f} {unit}".strip()
97
+
98
+ def _get_si_scale(self, abs_val: float) -> tuple[float, int] | None:
99
+ """Get SI scale and exponent for value.
100
+
101
+ Args:
102
+ abs_val: Absolute value
103
+
104
+ Returns:
105
+ Tuple of (scale_factor, exponent) or None for extreme values
106
+ """
107
+ # Find the appropriate SI prefix by checking value ranges
108
+ # Each range is [lower_bound, upper_bound) for the prefix
109
+ if abs_val >= 1e15:
110
+ return (1e-15, 15)
111
+ if abs_val >= 1e12:
112
+ return (1e-12, 12)
113
+ if abs_val >= 1e9:
114
+ return (1e-9, 9)
115
+ if abs_val >= 1e6:
116
+ return (1e-6, 6)
117
+ if abs_val >= 1e3:
118
+ return (1e-3, 3)
119
+ if abs_val >= 1:
120
+ return (1, 0)
121
+ if abs_val >= 1e-3:
122
+ return (1e3, -3)
123
+ if abs_val >= 1e-6:
124
+ return (1e6, -6)
125
+ if abs_val >= 1e-9:
126
+ return (1e9, -9)
127
+ if abs_val >= 1e-12:
128
+ return (1e12, -12)
129
+ if abs_val >= 1e-15:
130
+ return (1e15, -15)
131
+
132
+ # Value too small, use scientific notation
133
+ return None
134
+
73
135
  def format(
74
136
  self,
75
137
  value: float,
@@ -93,64 +155,29 @@ class NumberFormatter:
93
155
  >>> fmt.format(1500000, "Hz")
94
156
  '1.500 MHz'
95
157
  """
96
- # Handle special float values
97
158
  if math.isnan(value):
98
159
  return f"NaN {unit}".strip()
99
160
  if math.isinf(value):
100
161
  sign = "-" if value < 0 else ""
101
162
  return f"{sign}Inf {unit}".strip()
102
163
 
103
- # Determine precision
104
164
  places = decimal_places if decimal_places is not None else self.sig_figs
105
165
 
106
166
  if not self.auto_scale:
107
- # No scaling - show full value with appropriate precision
108
- # Use more decimal places for small numbers to show actual value
109
- if abs(value) != 0 and abs(value) < 1:
110
- # Calculate needed decimal places to show significant figures
111
- from math import floor, log10
112
-
113
- order = floor(log10(abs(value)))
114
- # For value like 0.0023, order=-3, need at least 4 decimals
115
- decimal_places_needed = max(places, abs(order) + 1)
116
- return f"{value:.{decimal_places_needed}f} {unit}".strip()
117
- elif abs(value) >= 1e6:
118
- return f"{value:.{places}e} {unit}".strip()
119
- return f"{value:.{places}f} {unit}".strip()
120
-
121
- # Find appropriate SI prefix
167
+ return self._format_without_scaling(value, places, unit)
168
+
122
169
  if value == 0:
123
170
  return f"0.{'0' * places} {unit}".strip()
124
171
 
125
172
  abs_val = abs(value)
173
+ scale_result = self._get_si_scale(abs_val)
126
174
 
127
- # Determine the appropriate power of 10
128
- if abs_val < 1e-15:
129
- return f"{value:.{places}e} {unit}".strip()
130
- elif abs_val < 1e-12:
131
- scaled, exp = value * 1e15, -15
132
- elif abs_val < 1e-9:
133
- scaled, exp = value * 1e12, -12
134
- elif abs_val < 1e-6:
135
- scaled, exp = value * 1e9, -9
136
- elif abs_val < 1e-3:
137
- scaled, exp = value * 1e6, -6
138
- elif abs_val < 1:
139
- scaled, exp = value * 1e3, -3
140
- elif abs_val < 1e3:
141
- scaled, exp = value, 0
142
- elif abs_val < 1e6:
143
- scaled, exp = value / 1e3, 3
144
- elif abs_val < 1e9:
145
- scaled, exp = value / 1e6, 6
146
- elif abs_val < 1e12:
147
- scaled, exp = value / 1e9, 9
148
- elif abs_val < 1e15:
149
- scaled, exp = value / 1e12, 12
150
- else:
175
+ if scale_result is None:
151
176
  return f"{value:.{places}e} {unit}".strip()
152
177
 
153
- # Get prefix (unicode or ascii)
178
+ scale, exp = scale_result
179
+ scaled = value * scale
180
+
154
181
  prefix_idx = 0 if self.unicode_prefixes else 1
155
182
  prefix = self.SI_PREFIXES.get(exp, ("", ""))[prefix_idx]
156
183
 
oscura/reporting/html.py CHANGED
@@ -80,9 +80,23 @@ def _generate_html_header(report: Report, dark_mode: bool, responsive: bool) ->
80
80
 
81
81
  def _generate_html_styles(dark_mode: bool, responsive: bool) -> str:
82
82
  """Generate CSS styles for HTML report."""
83
- styles = """
83
+ base_styles = _generate_base_styles()
84
+ typography_styles = _generate_typography_styles()
85
+ component_styles = _generate_component_styles()
86
+ media_query_styles = _generate_media_query_styles()
87
+
88
+ return f"""
84
89
  <style>
85
- /* Professional Formatting Standards */
90
+ {base_styles}
91
+ {typography_styles}
92
+ {component_styles}
93
+ {media_query_styles}
94
+ </style>"""
95
+
96
+
97
+ def _generate_base_styles() -> str:
98
+ """Generate base CSS variables and reset."""
99
+ return """/* Professional Formatting Standards */
86
100
  :root {
87
101
  --primary-color: #2c3e50;
88
102
  --secondary-color: #3498db;
@@ -96,7 +110,6 @@ def _generate_html_styles(dark_mode: bool, responsive: bool) -> str:
96
110
  --table-alt-row-bg: #f9f9f9;
97
111
  }
98
112
 
99
- /* Dark mode support */
100
113
  @media (prefers-color-scheme: dark) {
101
114
  body.dark-mode {
102
115
  --bg-color: #1e1e1e;
@@ -127,8 +140,12 @@ body {
127
140
  max-width: 1200px;
128
141
  margin: 0 auto;
129
142
  padding: 1in;
130
- }
143
+ }"""
144
+
131
145
 
146
+ def _generate_typography_styles() -> str:
147
+ """Generate typography and text styling."""
148
+ return """
132
149
  /* Typography */
133
150
  h1, h2, h3, h4, h5, h6 {
134
151
  font-family: Arial, Helvetica, sans-serif;
@@ -153,8 +170,23 @@ code, pre {
153
170
  pre {
154
171
  padding: 10px;
155
172
  overflow-x: auto;
156
- }
173
+ }"""
174
+
175
+
176
+ def _generate_component_styles() -> str:
177
+ """Generate component CSS (tables, severity, navigation, etc)."""
178
+ emphasis = _generate_emphasis_styles()
179
+ tables = _generate_table_styles()
180
+ collapsible = _generate_collapsible_styles()
181
+ metadata = _generate_metadata_styles()
182
+ navigation = _generate_navigation_styles()
157
183
 
184
+ return f"{emphasis}\n{tables}\n{collapsible}\n{metadata}\n{navigation}"
185
+
186
+
187
+ def _generate_emphasis_styles() -> str:
188
+ """Generate visual emphasis and severity styles."""
189
+ return """
158
190
  /* Visual Emphasis */
159
191
  .pass {
160
192
  color: var(--success-color);
@@ -174,7 +206,6 @@ pre {
174
206
  .pass::before { content: '\\2713 '; }
175
207
  .fail::before { content: '\\2717 '; }
176
208
 
177
- /* Severity indicators */
178
209
  .severity-critical {
179
210
  background-color: rgba(231, 76, 60, 0.2);
180
211
  border-left: 4px solid var(--danger-color);
@@ -196,7 +227,6 @@ pre {
196
227
  margin: 10px 0;
197
228
  }
198
229
 
199
- /* Callout boxes */
200
230
  .callout {
201
231
  background-color: rgba(241, 196, 15, 0.15);
202
232
  border: 1px solid var(--warning-color);
@@ -208,8 +238,12 @@ pre {
208
238
  .callout-title {
209
239
  font-weight: bold;
210
240
  margin-bottom: 10px;
211
- }
241
+ }"""
242
+
212
243
 
244
+ def _generate_table_styles() -> str:
245
+ """Generate table CSS."""
246
+ return """
213
247
  /* Tables */
214
248
  table {
215
249
  border-collapse: collapse;
@@ -243,8 +277,12 @@ caption {
243
277
  font-style: italic;
244
278
  padding: 8px;
245
279
  text-align: left;
246
- }
280
+ }"""
247
281
 
282
+
283
+ def _generate_collapsible_styles() -> str:
284
+ """Generate collapsible section CSS."""
285
+ return """
248
286
  /* Collapsible sections */
249
287
  .collapsible {
250
288
  cursor: pointer;
@@ -272,8 +310,12 @@ caption {
272
310
 
273
311
  .collapsible-content.collapsed {
274
312
  max-height: 0;
275
- }
313
+ }"""
276
314
 
315
+
316
+ def _generate_metadata_styles() -> str:
317
+ """Generate metadata section CSS."""
318
+ return """
277
319
  /* Metadata section */
278
320
  .metadata {
279
321
  background-color: var(--table-alt-row-bg);
@@ -286,8 +328,12 @@ caption {
286
328
  .metadata-item {
287
329
  display: inline-block;
288
330
  margin-right: 20px;
289
- }
331
+ }"""
290
332
 
333
+
334
+ def _generate_navigation_styles() -> str:
335
+ """Generate navigation CSS."""
336
+ return """
291
337
  /* Navigation */
292
338
  nav {
293
339
  background-color: var(--primary-color);
@@ -312,8 +358,12 @@ nav a {
312
358
 
313
359
  nav a:hover {
314
360
  text-decoration: underline;
315
- }
361
+ }"""
316
362
 
363
+
364
+ def _generate_media_query_styles() -> str:
365
+ """Generate responsive and print media queries."""
366
+ return """
317
367
  /* Responsive design */
318
368
  @media (max-width: 768px) {
319
369
  .container {
@@ -348,9 +398,7 @@ nav a:hover {
348
398
  .collapsible-content {
349
399
  max-height: none !important;
350
400
  }
351
- }
352
- </style>"""
353
- return styles
401
+ }"""
354
402
 
355
403
 
356
404
  def _generate_html_scripts() -> str:
@@ -458,48 +506,80 @@ def _generate_html_content(report: Report, collapsible: bool) -> str:
458
506
  content = []
459
507
 
460
508
  for section in report.sections:
461
- if not section.visible:
462
- continue
509
+ if section.visible:
510
+ section_html = _render_section(section, collapsible)
511
+ content.append(section_html)
463
512
 
464
- section_id = section.title.lower().replace(" ", "-")
465
- content.append(f'<section id="{section_id}">')
513
+ return "\n".join(content)
466
514
 
467
- # Section header
468
- tag = f"h{min(section.level + 1, 6)}"
469
- if collapsible and section.collapsible:
470
- content.append(f'<{tag} class="collapsible">{section.title}</{tag}>')
471
- content.append('<div class="collapsible-content">')
472
- else:
473
- content.append(f"<{tag}>{section.title}</{tag}>")
474
-
475
- # Section content
476
- if isinstance(section.content, str):
477
- content.append(f"<p>{section.content}</p>")
478
- elif isinstance(section.content, list):
479
- for item in section.content:
480
- if isinstance(item, dict):
481
- if item.get("type") == "table":
482
- content.append(_table_to_html(item))
483
- elif item.get("type") == "figure":
484
- content.append(_figure_to_html(item))
485
- else:
486
- content.append(f"<p>{item}</p>")
487
-
488
- # Subsections
489
- for subsec in section.subsections:
490
- if not subsec.visible:
491
- continue
492
- sub_tag = f"h{min(subsec.level + 1, 6)}"
493
- content.append(f"<{sub_tag}>{subsec.title}</{sub_tag}>")
494
- if isinstance(subsec.content, str):
495
- content.append(f"<p>{subsec.content}</p>")
496
515
 
497
- if collapsible and section.collapsible:
498
- content.append("</div>")
516
+ def _render_section(section: Any, collapsible: bool) -> str:
517
+ """Render a single section with header and content."""
518
+ section_id = section.title.lower().replace(" ", "-")
519
+ parts = [f'<section id="{section_id}">']
499
520
 
500
- content.append("</section>")
521
+ # Add section header
522
+ parts.append(_render_section_header(section, collapsible))
501
523
 
502
- return "\n".join(content)
524
+ # Add section content
525
+ parts.append(_render_section_content(section))
526
+
527
+ # Add subsections
528
+ parts.append(_render_subsections(section))
529
+
530
+ # Close collapsible wrapper if needed
531
+ if collapsible and section.collapsible:
532
+ parts.append("</div>")
533
+
534
+ parts.append("</section>")
535
+ return "\n".join(parts)
536
+
537
+
538
+ def _render_section_header(section: Any, collapsible: bool) -> str:
539
+ """Render section header with optional collapsible class."""
540
+ tag = f"h{min(section.level + 1, 6)}"
541
+ if collapsible and section.collapsible:
542
+ return (
543
+ f'<{tag} class="collapsible">{section.title}</{tag}>\n<div class="collapsible-content">'
544
+ )
545
+ return f"<{tag}>{section.title}</{tag}>"
546
+
547
+
548
+ def _render_section_content(section: Any) -> str:
549
+ """Render section content (text, tables, figures)."""
550
+ if isinstance(section.content, str):
551
+ return f"<p>{section.content}</p>"
552
+
553
+ if isinstance(section.content, list):
554
+ return _render_content_list(section.content)
555
+
556
+ return ""
557
+
558
+
559
+ def _render_content_list(content_list: list[Any]) -> str:
560
+ """Render list of content items (tables, figures, text)."""
561
+ rendered = []
562
+ for item in content_list:
563
+ if isinstance(item, dict):
564
+ if item.get("type") == "table":
565
+ rendered.append(_table_to_html(item))
566
+ elif item.get("type") == "figure":
567
+ rendered.append(_figure_to_html(item))
568
+ else:
569
+ rendered.append(f"<p>{item}</p>")
570
+ return "\n".join(rendered)
571
+
572
+
573
+ def _render_subsections(section: Any) -> str:
574
+ """Render all subsections."""
575
+ subsection_html = []
576
+ for subsec in section.subsections:
577
+ if subsec.visible:
578
+ sub_tag = f"h{min(subsec.level + 1, 6)}"
579
+ subsection_html.append(f"<{sub_tag}>{subsec.title}</{sub_tag}>")
580
+ if isinstance(subsec.content, str):
581
+ subsection_html.append(f"<p>{subsec.content}</p>")
582
+ return "\n".join(subsection_html)
503
583
 
504
584
 
505
585
  def _table_to_html(table: dict[str, Any]) -> str:
@@ -544,22 +624,23 @@ def _figure_to_html(figure: dict[str, Any]) -> str:
544
624
  width = figure.get("width", "100%")
545
625
  caption = figure.get("caption", "")
546
626
 
547
- html = f'<figure style="max-width: {width}; margin: 20px auto;">'
627
+ # Use list + join for O(n) string building instead of O(n²) +=
628
+ html_parts = [f'<figure style="max-width: {width}; margin: 20px auto;">']
548
629
 
549
630
  # Handle different figure types
550
631
  fig_obj = figure.get("figure")
551
632
  if isinstance(fig_obj, str):
552
633
  # Assume it's a path to an image
553
- html += f'<img src="{fig_obj}" alt="{caption}" style="width: 100%;">'
634
+ html_parts.append(f'<img src="{fig_obj}" alt="{caption}" style="width: 100%;">')
554
635
  else:
555
636
  # Placeholder for matplotlib figures
556
- html += f'<div class="figure-placeholder">[Figure: {caption}]</div>'
637
+ html_parts.append(f'<div class="figure-placeholder">[Figure: {caption}]</div>')
557
638
 
558
639
  if caption:
559
- html += f"<figcaption>{caption}</figcaption>"
640
+ html_parts.append(f"<figcaption>{caption}</figcaption>")
560
641
 
561
- html += "</figure>"
562
- return html
642
+ html_parts.append("</figure>")
643
+ return "".join(html_parts)
563
644
 
564
645
 
565
646
  def save_html_report(