oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
oscura/cli/compare.py CHANGED
@@ -28,33 +28,33 @@ if TYPE_CHECKING:
28
28
  logger = logging.getLogger("oscura.cli.compare")
29
29
 
30
30
 
31
- @click.command() # type: ignore[misc]
32
- @click.argument("file1", type=click.Path(exists=True)) # type: ignore[misc]
33
- @click.argument("file2", type=click.Path(exists=True)) # type: ignore[misc]
34
- @click.option( # type: ignore[misc]
31
+ @click.command()
32
+ @click.argument("file1", type=click.Path(exists=True))
33
+ @click.argument("file2", type=click.Path(exists=True))
34
+ @click.option(
35
35
  "--threshold",
36
36
  type=float,
37
37
  default=5.0,
38
38
  help="Report differences greater than this percentage (default: 5%).",
39
39
  )
40
- @click.option( # type: ignore[misc]
40
+ @click.option(
41
41
  "--output",
42
42
  type=click.Choice(["json", "csv", "html", "table"], case_sensitive=False),
43
43
  default="table",
44
44
  help="Output format (default: table).",
45
45
  )
46
- @click.option( # type: ignore[misc]
46
+ @click.option(
47
47
  "--save-report",
48
48
  type=click.Path(),
49
49
  default=None,
50
50
  help="Save detailed HTML comparison report.",
51
51
  )
52
- @click.option( # type: ignore[misc]
52
+ @click.option(
53
53
  "--align",
54
54
  is_flag=True,
55
55
  help="Align signals using cross-correlation before comparison.",
56
56
  )
57
- @click.pass_context # type: ignore[misc]
57
+ @click.pass_context
58
58
  def compare(
59
59
  ctx: click.Context,
60
60
  file1: str,
@@ -381,60 +381,84 @@ def _perform_comparison(
381
381
  sample_rate1 = trace1.metadata.sample_rate
382
382
  sample_rate2 = trace2.metadata.sample_rate
383
383
 
384
- # Check sample rate compatibility
385
- rate_mismatch = False
386
- if sample_rate1 != sample_rate2:
387
- logger.warning(f"Sample rates differ: {sample_rate1:.2e} vs {sample_rate2:.2e} Hz")
388
- rate_mismatch = True
384
+ rate_mismatch = _check_sample_rate_mismatch(sample_rate1, sample_rate2)
389
385
 
390
- # Initialize results
391
386
  results: dict[str, Any] = {
392
387
  "threshold_percent": threshold,
393
388
  "aligned": align_signals,
394
389
  "sample_rate_mismatch": rate_mismatch,
395
390
  }
396
391
 
397
- # Basic statistics for each trace
398
- results["trace1_stats"] = {
399
- "samples": len(trace1.data),
400
- "sample_rate": f"{sample_rate1 / 1e6:.2f} MHz",
401
- "duration_ms": f"{len(trace1.data) / sample_rate1 * 1e3:.3f} ms",
402
- "mean": f"{float(trace1.data.mean()):.6f} V",
403
- "rms": f"{float(np.sqrt((trace1.data**2).mean())):.6f} V",
404
- "peak_to_peak": f"{float(trace1.data.max() - trace1.data.min()):.6f} V",
405
- "min": f"{float(trace1.data.min()):.6f} V",
406
- "max": f"{float(trace1.data.max()):.6f} V",
407
- }
392
+ # Add trace statistics
393
+ results["trace1_stats"] = _compute_trace_stats(trace1, sample_rate1)
394
+ results["trace2_stats"] = _compute_trace_stats(trace2, sample_rate2)
395
+
396
+ # Prepare and align data
397
+ data1, data2 = _prepare_comparison_data(trace1, trace2, sample_rate1, align_signals, results)
398
+
399
+ # Perform analyses
400
+ results["timing_drift"] = _compute_timing_drift(data1, data2, sample_rate1)
401
+ results["amplitude_difference"] = _compute_amplitude_difference(data1, data2, threshold)
402
+ results["noise_change"] = _compute_noise_change(data1, data2, sample_rate1, threshold)
403
+ results["correlation"] = _compute_correlation(data1, data2)
404
+ results["spectral_difference"] = _compute_spectral_difference(
405
+ data1, data2, sample_rate1, threshold
406
+ )
407
+
408
+ # Overall assessment
409
+ results["summary"] = _compute_summary(results)
410
+
411
+ return results
412
+
413
+
414
+ def _check_sample_rate_mismatch(sample_rate1: float, sample_rate2: float) -> bool:
415
+ """Check if sample rates differ between traces."""
416
+ if sample_rate1 != sample_rate2:
417
+ logger.warning(f"Sample rates differ: {sample_rate1:.2e} vs {sample_rate2:.2e} Hz")
418
+ return True
419
+ return False
420
+
408
421
 
409
- results["trace2_stats"] = {
410
- "samples": len(trace2.data),
411
- "sample_rate": f"{sample_rate2 / 1e6:.2f} MHz",
412
- "duration_ms": f"{len(trace2.data) / sample_rate2 * 1e3:.3f} ms",
413
- "mean": f"{float(trace2.data.mean()):.6f} V",
414
- "rms": f"{float(np.sqrt((trace2.data**2).mean())):.6f} V",
415
- "peak_to_peak": f"{float(trace2.data.max() - trace2.data.min()):.6f} V",
416
- "min": f"{float(trace2.data.min()):.6f} V",
417
- "max": f"{float(trace2.data.max()):.6f} V",
422
+ def _compute_trace_stats(trace: WaveformTrace, sample_rate: float) -> dict[str, str | int]:
423
+ """Compute statistics for a single trace."""
424
+ return {
425
+ "samples": len(trace.data),
426
+ "sample_rate": f"{sample_rate / 1e6:.2f} MHz",
427
+ "duration_ms": f"{len(trace.data) / sample_rate * 1e3:.3f} ms",
428
+ "mean": f"{float(trace.data.mean()):.6f} V",
429
+ "rms": f"{float(np.sqrt((trace.data**2).mean())):.6f} V",
430
+ "peak_to_peak": f"{float(trace.data.max() - trace.data.min()):.6f} V",
431
+ "min": f"{float(trace.data.min()):.6f} V",
432
+ "max": f"{float(trace.data.max()):.6f} V",
418
433
  }
419
434
 
420
- # Prepare data for comparison
435
+
436
+ def _prepare_comparison_data(
437
+ trace1: WaveformTrace,
438
+ trace2: WaveformTrace,
439
+ sample_rate: float,
440
+ align_signals: bool,
441
+ results: dict[str, Any],
442
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
443
+ """Prepare data arrays for comparison, with optional alignment."""
421
444
  data1 = trace1.data.astype(np.float64)
422
445
  data2 = trace2.data.astype(np.float64)
423
446
 
424
- # Signal alignment using cross-correlation
425
447
  if align_signals:
426
- data1, data2, alignment_info = _align_signals(data1, data2, sample_rate1)
448
+ data1, data2, alignment_info = _align_signals(data1, data2, sample_rate)
427
449
  results["alignment"] = alignment_info
428
450
  else:
429
- # Ensure equal length
430
451
  min_len = min(len(data1), len(data2))
431
452
  data1 = data1[:min_len]
432
453
  data2 = data2[:min_len]
433
454
 
434
- # Timing drift analysis
435
- results["timing_drift"] = _compute_timing_drift(data1, data2, sample_rate1)
455
+ return data1, data2
456
+
436
457
 
437
- # Amplitude difference analysis
458
+ def _compute_amplitude_difference(
459
+ data1: NDArray[np.float64], data2: NDArray[np.float64], threshold: float
460
+ ) -> dict[str, Any]:
461
+ """Compute amplitude differences between signals."""
438
462
  diff = data2 - data1
439
463
  abs_diff = np.abs(diff)
440
464
 
@@ -444,11 +468,10 @@ def _perform_comparison(
444
468
  max_diff = float(abs_diff.max())
445
469
  rms_diff = float(np.sqrt((diff**2).mean()))
446
470
 
447
- # Compare to reference RMS
448
471
  rms1 = float(np.sqrt((data1**2).mean()))
449
472
  rms_diff_percent = rms_diff / rms1 * 100 if rms1 > 0 else 0
450
473
 
451
- results["amplitude_difference"] = {
474
+ return {
452
475
  "mean_diff_v": f"{mean_diff:.6f}",
453
476
  "mean_diff_percent": f"{mean_diff_percent:.2f}%",
454
477
  "max_diff_v": f"{max_diff:.6f}",
@@ -457,10 +480,13 @@ def _perform_comparison(
457
480
  "significant": bool(mean_diff_percent > threshold),
458
481
  }
459
482
 
460
- # Noise analysis
461
- # Use high-pass filter to extract noise component
462
- nyquist = sample_rate1 / 2
463
- cutoff = min(1000, nyquist * 0.9) # 1kHz or 90% of Nyquist
483
+
484
+ def _compute_noise_change(
485
+ data1: NDArray[np.float64], data2: NDArray[np.float64], sample_rate: float, threshold: float
486
+ ) -> dict[str, Any]:
487
+ """Compute noise level changes between signals."""
488
+ nyquist = sample_rate / 2
489
+ cutoff = min(1000, nyquist * 0.9)
464
490
  b, a = signal.butter(4, cutoff / nyquist, btype="high")
465
491
 
466
492
  try:
@@ -469,40 +495,44 @@ def _perform_comparison(
469
495
  noise_std1 = float(np.std(noise1))
470
496
  noise_std2 = float(np.std(noise2))
471
497
  except Exception:
472
- # Fallback to simple std if filter fails
473
498
  noise_std1 = float(np.std(data1))
474
499
  noise_std2 = float(np.std(data2))
475
500
 
476
501
  noise_change = ((noise_std2 - noise_std1) / noise_std1 * 100) if noise_std1 != 0 else 0
477
502
 
478
- results["noise_change"] = {
503
+ return {
479
504
  "noise1_v": f"{noise_std1:.6f}",
480
505
  "noise2_v": f"{noise_std2:.6f}",
481
506
  "change_percent": f"{noise_change:.2f}%",
482
507
  "significant": bool(abs(noise_change) > threshold),
483
508
  }
484
509
 
485
- # Correlation coefficient
510
+
511
+ def _compute_correlation(data1: NDArray[np.float64], data2: NDArray[np.float64]) -> dict[str, str]:
512
+ """Compute correlation coefficient between signals."""
486
513
  if len(data1) > 1 and len(data2) > 1:
487
514
  with np.errstate(divide="ignore", invalid="ignore"):
488
515
  correlation = float(np.corrcoef(data1, data2)[0, 1])
489
- results["correlation"] = {
516
+
517
+ if correlation > 0.99:
518
+ quality = "excellent"
519
+ elif correlation > 0.95:
520
+ quality = "good"
521
+ elif correlation > 0.8:
522
+ quality = "fair"
523
+ else:
524
+ quality = "poor"
525
+
526
+ return {
490
527
  "coefficient": f"{correlation:.6f}",
491
- "quality": "excellent"
492
- if correlation > 0.99
493
- else "good"
494
- if correlation > 0.95
495
- else "fair"
496
- if correlation > 0.8
497
- else "poor",
528
+ "quality": quality,
498
529
  }
499
530
 
500
- # Spectral differences
501
- results["spectral_difference"] = _compute_spectral_difference(
502
- data1, data2, sample_rate1, threshold
503
- )
531
+ return {"coefficient": "N/A", "quality": "unknown"}
504
532
 
505
- # Overall assessment
533
+
534
+ def _compute_summary(results: dict[str, Any]) -> dict[str, Any]:
535
+ """Compute overall comparison summary."""
506
536
  significant_count = sum(
507
537
  [
508
538
  results.get("amplitude_difference", {}).get("significant", False),
@@ -521,7 +551,7 @@ def _perform_comparison(
521
551
  else:
522
552
  match_quality = "poor"
523
553
 
524
- results["summary"] = {
554
+ return {
525
555
  "significant_differences": significant_count,
526
556
  "overall_match": match_quality,
527
557
  "categories_with_differences": [
@@ -536,8 +566,6 @@ def _perform_comparison(
536
566
  ],
537
567
  }
538
568
 
539
- return results
540
-
541
569
 
542
570
  def _generate_html_report(
543
571
  results: dict[str, Any],
@@ -554,27 +582,49 @@ def _generate_html_report(
554
582
  Returns:
555
583
  HTML content as string.
556
584
  """
557
- # Get summary info
558
585
  summary = results.get("summary", {})
559
586
  match_quality = summary.get("overall_match", "unknown")
560
587
  significant_diffs = summary.get("significant_differences", 0)
561
588
 
562
- # Color based on quality
589
+ quality_color = _get_quality_color(match_quality)
590
+ css = _generate_comparison_report_css(quality_color)
591
+ header = _generate_report_header(file1, file2, match_quality, significant_diffs)
592
+ sections = _generate_report_sections(results)
593
+
594
+ return f"""<!DOCTYPE html>
595
+ <html lang="en">
596
+ <head>
597
+ <meta charset="UTF-8">
598
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
599
+ <title>Oscura Signal Comparison Report</title>
600
+ {css}
601
+ </head>
602
+ <body>
603
+ {header}
604
+ <div class="content">
605
+ {sections}
606
+ <footer style="margin-top: 30px; text-align: center; color: #6c757d;">
607
+ <p>Generated by Oscura - Signal Analysis Toolkit</p>
608
+ </footer>
609
+ </div>
610
+ </body>
611
+ </html>"""
612
+
613
+
614
+ def _get_quality_color(match_quality: str) -> str:
615
+ """Get color code for match quality level."""
563
616
  quality_colors = {
564
617
  "excellent": "#28a745",
565
618
  "good": "#17a2b8",
566
619
  "fair": "#ffc107",
567
620
  "poor": "#dc3545",
568
621
  }
569
- quality_color = quality_colors.get(match_quality, "#6c757d")
622
+ return quality_colors.get(match_quality, "#6c757d")
570
623
 
571
- html = f"""<!DOCTYPE html>
572
- <html lang="en">
573
- <head>
574
- <meta charset="UTF-8">
575
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
576
- <title>Oscura Signal Comparison Report</title>
577
- <style>
624
+
625
+ def _generate_comparison_report_css(quality_color: str) -> str:
626
+ """Generate CSS for comparison report."""
627
+ return f"""<style>
578
628
  body {{
579
629
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
580
630
  max-width: 1200px;
@@ -630,10 +680,14 @@ def _generate_html_report(
630
680
  .ok {{
631
681
  color: #28a745;
632
682
  }}
633
- </style>
634
- </head>
635
- <body>
636
- <div class="header">
683
+ </style>"""
684
+
685
+
686
+ def _generate_report_header(
687
+ file1: str, file2: str, match_quality: str, significant_diffs: int
688
+ ) -> str:
689
+ """Generate report header with summary."""
690
+ return f"""<div class="header">
637
691
  <h1>Oscura Signal Comparison Report</h1>
638
692
  <p>File 1: {Path(file1).name}</p>
639
693
  <p>File 2: {Path(file2).name}</p>
@@ -643,9 +697,27 @@ def _generate_html_report(
643
697
  <div class="summary">
644
698
  <h2>Overall Match: {match_quality.upper()}</h2>
645
699
  <p>{significant_diffs} significant difference(s) detected</p>
646
- </div>
700
+ </div>"""
701
+
702
+
703
+ def _generate_report_sections(results: dict[str, Any]) -> str:
704
+ """Generate all report sections."""
705
+ stats = _generate_trace_stats_section(results)
706
+ amplitude = _generate_amplitude_diff_section(results)
707
+ timing = _generate_timing_drift_section(results)
708
+ noise = _generate_noise_change_section(results)
709
+ spectral = _generate_spectral_diff_section(results)
710
+ correlation = _generate_correlation_section(results)
711
+
712
+ return f"{stats}\n{amplitude}\n{timing}\n{noise}\n{spectral}\n{correlation}"
713
+
714
+
715
+ def _generate_trace_stats_section(results: dict[str, Any]) -> str:
716
+ """Generate trace statistics comparison table."""
717
+ t1 = results.get("trace1_stats", {})
718
+ t2 = results.get("trace2_stats", {})
647
719
 
648
- <div class="section">
720
+ return f"""<div class="section">
649
721
  <h3>Trace Statistics</h3>
650
722
  <table>
651
723
  <tr>
@@ -655,121 +727,142 @@ def _generate_html_report(
655
727
  </tr>
656
728
  <tr>
657
729
  <td>Samples</td>
658
- <td>{results.get("trace1_stats", {}).get("samples", "N/A")}</td>
659
- <td>{results.get("trace2_stats", {}).get("samples", "N/A")}</td>
730
+ <td>{t1.get("samples", "N/A")}</td>
731
+ <td>{t2.get("samples", "N/A")}</td>
660
732
  </tr>
661
733
  <tr>
662
734
  <td>Sample Rate</td>
663
- <td>{results.get("trace1_stats", {}).get("sample_rate", "N/A")}</td>
664
- <td>{results.get("trace2_stats", {}).get("sample_rate", "N/A")}</td>
735
+ <td>{t1.get("sample_rate", "N/A")}</td>
736
+ <td>{t2.get("sample_rate", "N/A")}</td>
665
737
  </tr>
666
738
  <tr>
667
739
  <td>Mean</td>
668
- <td>{results.get("trace1_stats", {}).get("mean", "N/A")}</td>
669
- <td>{results.get("trace2_stats", {}).get("mean", "N/A")}</td>
740
+ <td>{t1.get("mean", "N/A")}</td>
741
+ <td>{t2.get("mean", "N/A")}</td>
670
742
  </tr>
671
743
  <tr>
672
744
  <td>RMS</td>
673
- <td>{results.get("trace1_stats", {}).get("rms", "N/A")}</td>
674
- <td>{results.get("trace2_stats", {}).get("rms", "N/A")}</td>
745
+ <td>{t1.get("rms", "N/A")}</td>
746
+ <td>{t2.get("rms", "N/A")}</td>
675
747
  </tr>
676
748
  <tr>
677
749
  <td>Peak-to-Peak</td>
678
- <td>{results.get("trace1_stats", {}).get("peak_to_peak", "N/A")}</td>
679
- <td>{results.get("trace2_stats", {}).get("peak_to_peak", "N/A")}</td>
750
+ <td>{t1.get("peak_to_peak", "N/A")}</td>
751
+ <td>{t2.get("peak_to_peak", "N/A")}</td>
680
752
  </tr>
681
753
  </table>
682
- </div>
754
+ </div>"""
683
755
 
684
- <div class="section">
756
+
757
+ def _generate_amplitude_diff_section(results: dict[str, Any]) -> str:
758
+ """Generate amplitude difference section."""
759
+ amp = results.get("amplitude_difference", {})
760
+ sig_class = "significant" if amp.get("significant") else "ok"
761
+
762
+ return f"""<div class="section">
685
763
  <h3>Amplitude Difference</h3>
686
764
  <table>
687
765
  <tr>
688
766
  <td>Mean Difference</td>
689
- <td>{results.get("amplitude_difference", {}).get("mean_diff_v", "N/A")}</td>
690
- <td class="{"significant" if results.get("amplitude_difference", {}).get("significant") else "ok"}">
691
- {results.get("amplitude_difference", {}).get("mean_diff_percent", "N/A")}
767
+ <td>{amp.get("mean_diff_v", "N/A")}</td>
768
+ <td class="{sig_class}">
769
+ {amp.get("mean_diff_percent", "N/A")}
692
770
  </td>
693
771
  </tr>
694
772
  <tr>
695
773
  <td>RMS Difference</td>
696
- <td>{results.get("amplitude_difference", {}).get("rms_diff_v", "N/A")}</td>
697
- <td>{results.get("amplitude_difference", {}).get("rms_diff_percent", "N/A")}</td>
774
+ <td>{amp.get("rms_diff_v", "N/A")}</td>
775
+ <td>{amp.get("rms_diff_percent", "N/A")}</td>
698
776
  </tr>
699
777
  <tr>
700
778
  <td>Max Difference</td>
701
- <td colspan="2">{results.get("amplitude_difference", {}).get("max_diff_v", "N/A")}</td>
779
+ <td colspan="2">{amp.get("max_diff_v", "N/A")}</td>
702
780
  </tr>
703
781
  </table>
704
- </div>
782
+ </div>"""
783
+
705
784
 
706
- <div class="section">
785
+ def _generate_timing_drift_section(results: dict[str, Any]) -> str:
786
+ """Generate timing drift section."""
787
+ drift = results.get("timing_drift", {})
788
+ sig_class = "significant" if drift.get("significant") else "ok"
789
+
790
+ return f"""<div class="section">
707
791
  <h3>Timing Drift</h3>
708
792
  <table>
709
793
  <tr>
710
794
  <td>Mean Drift</td>
711
- <td>{results.get("timing_drift", {}).get("value_ns", "N/A")} ns</td>
712
- <td class="{"significant" if results.get("timing_drift", {}).get("significant") else "ok"}">
713
- {results.get("timing_drift", {}).get("percentage", "N/A")}
795
+ <td>{drift.get("value_ns", "N/A")} ns</td>
796
+ <td class="{sig_class}">
797
+ {drift.get("percentage", "N/A")}
714
798
  </td>
715
799
  </tr>
716
800
  </table>
717
- </div>
801
+ </div>"""
802
+
803
+
804
+ def _generate_noise_change_section(results: dict[str, Any]) -> str:
805
+ """Generate noise change section."""
806
+ noise = results.get("noise_change", {})
807
+ sig_class = "significant" if noise.get("significant") else "ok"
718
808
 
719
- <div class="section">
809
+ return f"""<div class="section">
720
810
  <h3>Noise Change</h3>
721
811
  <table>
722
812
  <tr>
723
813
  <td>Trace 1 Noise</td>
724
- <td>{results.get("noise_change", {}).get("noise1_v", "N/A")}</td>
814
+ <td>{noise.get("noise1_v", "N/A")}</td>
725
815
  </tr>
726
816
  <tr>
727
817
  <td>Trace 2 Noise</td>
728
- <td>{results.get("noise_change", {}).get("noise2_v", "N/A")}</td>
818
+ <td>{noise.get("noise2_v", "N/A")}</td>
729
819
  </tr>
730
820
  <tr>
731
821
  <td>Change</td>
732
- <td class="{"significant" if results.get("noise_change", {}).get("significant") else "ok"}">
733
- {results.get("noise_change", {}).get("change_percent", "N/A")}
822
+ <td class="{sig_class}">
823
+ {noise.get("change_percent", "N/A")}
734
824
  </td>
735
825
  </tr>
736
826
  </table>
737
- </div>
827
+ </div>"""
738
828
 
739
- <div class="section">
829
+
830
+ def _generate_spectral_diff_section(results: dict[str, Any]) -> str:
831
+ """Generate spectral difference section."""
832
+ spec = results.get("spectral_difference", {})
833
+ sig_class = "significant" if spec.get("significant") else "ok"
834
+
835
+ return f"""<div class="section">
740
836
  <h3>Spectral Difference</h3>
741
837
  <table>
742
838
  <tr>
743
839
  <td>Dominant Frequency 1</td>
744
- <td>{results.get("spectral_difference", {}).get("dominant_freq1_hz", "N/A")} Hz</td>
840
+ <td>{spec.get("dominant_freq1_hz", "N/A")} Hz</td>
745
841
  </tr>
746
842
  <tr>
747
843
  <td>Dominant Frequency 2</td>
748
- <td>{results.get("spectral_difference", {}).get("dominant_freq2_hz", "N/A")} Hz</td>
844
+ <td>{spec.get("dominant_freq2_hz", "N/A")} Hz</td>
749
845
  </tr>
750
846
  <tr>
751
847
  <td>Frequency Difference</td>
752
- <td class="{"significant" if results.get("spectral_difference", {}).get("significant") else "ok"}">
753
- {results.get("spectral_difference", {}).get("freq_diff_percent", "N/A")}
848
+ <td class="{sig_class}">
849
+ {spec.get("freq_diff_percent", "N/A")}
754
850
  </td>
755
851
  </tr>
756
852
  <tr>
757
853
  <td>Max Magnitude Difference</td>
758
- <td>{results.get("spectral_difference", {}).get("max_magnitude_diff_db", "N/A")} dB</td>
854
+ <td>{spec.get("max_magnitude_diff_db", "N/A")} dB</td>
759
855
  </tr>
760
856
  </table>
761
- </div>
857
+ </div>"""
762
858
 
763
- <div class="section">
764
- <h3>Correlation</h3>
765
- <p>Coefficient: {results.get("correlation", {}).get("coefficient", "N/A")}</p>
766
- <p>Quality: {results.get("correlation", {}).get("quality", "N/A")}</p>
767
- </div>
768
859
 
769
- <footer style="margin-top: 30px; text-align: center; color: #6c757d;">
770
- <p>Generated by Oscura - Signal Analysis Toolkit</p>
771
- </footer>
772
- </div>
773
- </body>
774
- </html>"""
775
- return html
860
+ def _generate_correlation_section(results: dict[str, Any]) -> str:
861
+ """Generate correlation section."""
862
+ corr = results.get("correlation", {})
863
+
864
+ return f"""<div class="section">
865
+ <h3>Correlation</h3>
866
+ <p>Coefficient: {corr.get("coefficient", "N/A")}</p>
867
+ <p>Quality: {corr.get("quality", "N/A")}</p>
868
+ </div>"""