oscura 0.0.1__py3-none-any.whl → 0.1.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 (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
oscura/cli/compare.py ADDED
@@ -0,0 +1,775 @@
1
+ """TraceKit Compare Command implementing CLI-005.
2
+
3
+ Provides CLI for comparing two signal captures with timing, noise, and
4
+ spectral difference analysis.
5
+
6
+
7
+ Example:
8
+ $ oscura compare before.wfm after.wfm
9
+ $ oscura compare golden.wfm measured.wfm --threshold 5 --save-report diff.html
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ import click
19
+ import numpy as np
20
+ from numpy.typing import NDArray
21
+ from scipy import fft, signal
22
+
23
+ from oscura.cli.main import format_output
24
+
25
+ if TYPE_CHECKING:
26
+ from oscura.core.types import WaveformTrace
27
+
28
+ logger = logging.getLogger("oscura.cli.compare")
29
+
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]
35
+ "--threshold",
36
+ type=float,
37
+ default=5.0,
38
+ help="Report differences greater than this percentage (default: 5%).",
39
+ )
40
+ @click.option( # type: ignore[misc]
41
+ "--output",
42
+ type=click.Choice(["json", "csv", "html", "table"], case_sensitive=False),
43
+ default="table",
44
+ help="Output format (default: table).",
45
+ )
46
+ @click.option( # type: ignore[misc]
47
+ "--save-report",
48
+ type=click.Path(),
49
+ default=None,
50
+ help="Save detailed HTML comparison report.",
51
+ )
52
+ @click.option( # type: ignore[misc]
53
+ "--align",
54
+ is_flag=True,
55
+ help="Align signals using cross-correlation before comparison.",
56
+ )
57
+ @click.pass_context # type: ignore[misc]
58
+ def compare(
59
+ ctx: click.Context,
60
+ file1: str,
61
+ file2: str,
62
+ threshold: float,
63
+ output: str,
64
+ save_report: str | None,
65
+ align: bool,
66
+ ) -> None:
67
+ """Compare two signal captures.
68
+
69
+ Analyzes differences between two waveforms including timing drift,
70
+ amplitude changes, noise variations, and spectral differences.
71
+
72
+ Args:
73
+ ctx: Click context object.
74
+ file1: Path to first waveform file.
75
+ file2: Path to second waveform file.
76
+ threshold: Percentage threshold for reporting differences.
77
+ output: Output format (json, csv, html, table).
78
+ save_report: Path to save HTML comparison report.
79
+ align: Align signals using cross-correlation before comparison.
80
+
81
+ Raises:
82
+ Exception: If comparison fails or files cannot be loaded.
83
+
84
+ Examples:
85
+
86
+ \b
87
+ # Simple comparison
88
+ $ oscura compare before.wfm after.wfm
89
+
90
+ \b
91
+ # Report only significant differences (>10%)
92
+ $ oscura compare golden.wfm measured.wfm --threshold 10
93
+
94
+ \b
95
+ # Full comparison with alignment and HTML report
96
+ $ oscura compare reference.wfm test.wfm \\
97
+ --align \\
98
+ --save-report comparison.html
99
+
100
+ \b
101
+ # JSON output for automation
102
+ $ oscura compare before.wfm after.wfm --output json
103
+ """
104
+ verbose = ctx.obj.get("verbose", 0)
105
+
106
+ if verbose:
107
+ logger.info(f"Comparing: {file1} vs {file2}")
108
+ logger.info(f"Threshold: {threshold}%")
109
+ logger.info(f"Align signals: {align}")
110
+
111
+ try:
112
+ # Import here to avoid circular imports
113
+ from oscura.loaders import load
114
+
115
+ # Load both traces
116
+ logger.debug(f"Loading first trace from {file1}")
117
+ trace1 = load(file1)
118
+
119
+ logger.debug(f"Loading second trace from {file2}")
120
+ trace2 = load(file2)
121
+
122
+ # Perform comparison
123
+ results = _perform_comparison(
124
+ trace1=trace1, # type: ignore[arg-type]
125
+ trace2=trace2, # type: ignore[arg-type]
126
+ threshold=threshold,
127
+ align_signals=align,
128
+ )
129
+
130
+ # Add metadata
131
+ results["file1"] = str(Path(file1).name)
132
+ results["file2"] = str(Path(file2).name)
133
+
134
+ # Generate HTML report if requested
135
+ if save_report:
136
+ html_content = _generate_html_report(results, file1, file2)
137
+ with open(save_report, "w") as f:
138
+ f.write(html_content)
139
+ logger.info(f"Comparison report saved to {save_report}")
140
+ results["report_saved"] = str(save_report)
141
+
142
+ # Output results
143
+ formatted = format_output(results, output)
144
+ click.echo(formatted)
145
+
146
+ except Exception as e:
147
+ logger.error(f"Comparison failed: {e}")
148
+ if verbose > 1:
149
+ raise
150
+ click.echo(f"Error: {e}", err=True)
151
+ ctx.exit(1)
152
+
153
+
154
+ def _align_signals(
155
+ data1: NDArray[np.float64],
156
+ data2: NDArray[np.float64],
157
+ sample_rate: float,
158
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], dict[str, Any]]:
159
+ """Align two signals using cross-correlation.
160
+
161
+ Args:
162
+ data1: Reference signal.
163
+ data2: Signal to align.
164
+ sample_rate: Sample rate in Hz.
165
+
166
+ Returns:
167
+ Tuple of (aligned_data1, aligned_data2, alignment_info).
168
+ """
169
+ # Use cross-correlation to find optimal alignment
170
+ # For efficiency, use FFT-based correlation
171
+ n = len(data1) + len(data2) - 1
172
+ n_fft = 2 ** int(np.ceil(np.log2(n))) # Next power of 2
173
+
174
+ # Compute cross-correlation using FFT
175
+ fft1 = fft.fft(data1, n=n_fft)
176
+ fft2 = fft.fft(data2, n=n_fft)
177
+ cross_corr = fft.ifft(fft1 * np.conj(fft2)).real
178
+
179
+ # Find peak
180
+ peak_idx = np.argmax(np.abs(cross_corr))
181
+ offset = peak_idx - n_fft if peak_idx > n_fft // 2 else peak_idx
182
+
183
+ # Compute correlation coefficient at peak
184
+ corr_peak = cross_corr[peak_idx] / np.sqrt(np.sum(data1**2) * np.sum(data2**2))
185
+
186
+ # Apply offset
187
+ if offset > 0:
188
+ aligned1 = data1[offset:]
189
+ aligned2 = data2[: len(aligned1)]
190
+ elif offset < 0:
191
+ aligned2 = data2[-offset:]
192
+ aligned1 = data1[: len(aligned2)]
193
+ else:
194
+ min_len = min(len(data1), len(data2))
195
+ aligned1 = data1[:min_len]
196
+ aligned2 = data2[:min_len]
197
+
198
+ # Ensure equal length
199
+ min_len = min(len(aligned1), len(aligned2))
200
+ aligned1 = aligned1[:min_len]
201
+ aligned2 = aligned2[:min_len]
202
+
203
+ # Calculate timing offset in ns
204
+ offset_time_ns = offset / sample_rate * 1e9
205
+
206
+ alignment_info = {
207
+ "offset_samples": int(offset),
208
+ "offset_time_ns": f"{offset_time_ns:.2f}",
209
+ "correlation_peak": f"{corr_peak:.6f}",
210
+ "quality": "excellent"
211
+ if abs(corr_peak) > 0.95
212
+ else "good"
213
+ if abs(corr_peak) > 0.8
214
+ else "poor",
215
+ }
216
+
217
+ return aligned1, aligned2, alignment_info
218
+
219
+
220
+ def _compute_timing_drift(
221
+ data1: NDArray[np.float64],
222
+ data2: NDArray[np.float64],
223
+ sample_rate: float,
224
+ ) -> dict[str, Any]:
225
+ """Compute timing drift between two signals using edge detection.
226
+
227
+ Args:
228
+ data1: Reference signal.
229
+ data2: Comparison signal.
230
+ sample_rate: Sample rate in Hz.
231
+
232
+ Returns:
233
+ Dictionary with timing drift metrics.
234
+ """
235
+ # Find edges using threshold crossing
236
+ threshold1 = (np.max(data1) + np.min(data1)) / 2
237
+ threshold2 = (np.max(data2) + np.min(data2)) / 2
238
+
239
+ # Rising edges
240
+ edges1 = np.where(np.diff(data1 > threshold1).astype(int) > 0)[0]
241
+ edges2 = np.where(np.diff(data2 > threshold2).astype(int) > 0)[0]
242
+
243
+ if len(edges1) < 2 or len(edges2) < 2:
244
+ return {
245
+ "value_ns": "N/A",
246
+ "percentage": "N/A",
247
+ "significant": False,
248
+ "note": "Insufficient edges for timing analysis",
249
+ }
250
+
251
+ # Match edges and compute timing differences
252
+ # Use nearest-neighbor matching
253
+ timing_diffs = []
254
+ for e1 in edges1[: min(100, len(edges1))]: # Limit to first 100 edges
255
+ nearest_idx = np.argmin(np.abs(edges2 - e1))
256
+ if abs(edges2[nearest_idx] - e1) < sample_rate * 0.1: # Within 100ms
257
+ timing_diffs.append((edges2[nearest_idx] - e1) / sample_rate)
258
+
259
+ if len(timing_diffs) < 3:
260
+ return {
261
+ "value_ns": "N/A",
262
+ "percentage": "N/A",
263
+ "significant": False,
264
+ "note": "Could not match sufficient edges",
265
+ }
266
+
267
+ timing_diffs_arr = np.array(timing_diffs)
268
+ mean_drift_ns = float(np.mean(timing_diffs_arr)) * 1e9
269
+ std_drift_ns = float(np.std(timing_diffs_arr)) * 1e9
270
+
271
+ # Calculate period for percentage
272
+ periods1 = np.diff(edges1) / sample_rate
273
+ mean_period = float(np.mean(periods1)) if len(periods1) > 0 else 1.0
274
+ mean_diff = float(np.mean(timing_diffs_arr))
275
+ drift_percent = abs(mean_diff / mean_period * 100) if mean_period > 0 else 0.0
276
+
277
+ return {
278
+ "value_ns": f"{mean_drift_ns:.2f}",
279
+ "std_ns": f"{std_drift_ns:.2f}",
280
+ "percentage": f"{drift_percent:.4f}%",
281
+ "edges_analyzed": len(timing_diffs_arr),
282
+ "significant": bool(drift_percent > 0.1), # >0.1% is significant
283
+ }
284
+
285
+
286
+ def _compute_spectral_difference(
287
+ data1: NDArray[np.float64],
288
+ data2: NDArray[np.float64],
289
+ sample_rate: float,
290
+ threshold: float,
291
+ ) -> dict[str, Any]:
292
+ """Compute spectral differences between two signals.
293
+
294
+ Args:
295
+ data1: Reference signal.
296
+ data2: Comparison signal.
297
+ sample_rate: Sample rate in Hz.
298
+ threshold: Percentage threshold for significance.
299
+
300
+ Returns:
301
+ Dictionary with spectral comparison metrics.
302
+ """
303
+ # Compute FFT for both signals
304
+ n = len(data1)
305
+ # Use zero-padding for better frequency resolution
306
+ # Pad to at least 10x the original length for good interpolation
307
+ n_fft = 2 ** int(np.ceil(np.log2(n * 10)))
308
+
309
+ # Apply window to reduce spectral leakage
310
+ window = signal.windows.hann(n)
311
+ windowed1 = data1 * window
312
+ windowed2 = data2 * window
313
+
314
+ # Compute magnitude spectra
315
+ fft1 = np.abs(fft.rfft(windowed1, n=n_fft))
316
+ fft2 = np.abs(fft.rfft(windowed2, n=n_fft))
317
+ freqs = fft.rfftfreq(n_fft, d=1 / sample_rate)
318
+
319
+ # Avoid division by zero
320
+ fft1 = np.maximum(fft1, 1e-12)
321
+ fft2 = np.maximum(fft2, 1e-12)
322
+
323
+ # Find dominant frequencies
324
+ peak1_idx = np.argmax(fft1[1:]) + 1 # Skip DC
325
+ peak2_idx = np.argmax(fft2[1:]) + 1
326
+ dominant_freq1 = freqs[peak1_idx]
327
+ dominant_freq2 = freqs[peak2_idx]
328
+ freq_diff = abs(dominant_freq2 - dominant_freq1)
329
+ freq_diff_percent = freq_diff / dominant_freq1 * 100 if dominant_freq1 > 0 else 0
330
+
331
+ # Compute magnitude differences in dB
332
+ db_diff = 20 * np.log10(fft2 / fft1)
333
+ max_db_diff = np.max(np.abs(db_diff))
334
+ mean_db_diff = np.mean(np.abs(db_diff))
335
+
336
+ # Check for harmonic changes
337
+ # Find first 5 harmonics of dominant frequency
338
+ harmonic_changes = []
339
+ for h in range(1, 6):
340
+ harm_freq = dominant_freq1 * h
341
+ harm_idx = int(harm_freq / (sample_rate / n_fft))
342
+ if harm_idx < len(fft1):
343
+ harm_db_diff = 20 * np.log10(fft2[harm_idx] / fft1[harm_idx])
344
+ harmonic_changes.append(
345
+ {
346
+ "harmonic": h,
347
+ "frequency_hz": f"{harm_freq:.1f}",
348
+ "change_db": f"{harm_db_diff:.2f}",
349
+ }
350
+ )
351
+
352
+ return {
353
+ "dominant_freq1_hz": f"{dominant_freq1:.1f}",
354
+ "dominant_freq2_hz": f"{dominant_freq2:.1f}",
355
+ "freq_diff_hz": f"{freq_diff:.2f}",
356
+ "freq_diff_percent": f"{freq_diff_percent:.4f}%",
357
+ "max_magnitude_diff_db": f"{max_db_diff:.2f}",
358
+ "mean_magnitude_diff_db": f"{mean_db_diff:.2f}",
359
+ "harmonic_changes": harmonic_changes[:3], # First 3 harmonics
360
+ "significant": bool(freq_diff_percent > threshold or max_db_diff > 6.0), # 6dB = 2x power
361
+ }
362
+
363
+
364
+ def _perform_comparison(
365
+ trace1: WaveformTrace,
366
+ trace2: WaveformTrace,
367
+ threshold: float,
368
+ align_signals: bool,
369
+ ) -> dict[str, Any]:
370
+ """Perform comprehensive signal comparison analysis.
371
+
372
+ Args:
373
+ trace1: First trace (reference).
374
+ trace2: Second trace (comparison).
375
+ threshold: Percentage threshold for reporting differences.
376
+ align_signals: Whether to align signals using cross-correlation.
377
+
378
+ Returns:
379
+ Dictionary of comparison results.
380
+ """
381
+ sample_rate1 = trace1.metadata.sample_rate
382
+ sample_rate2 = trace2.metadata.sample_rate
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
389
+
390
+ # Initialize results
391
+ results: dict[str, Any] = {
392
+ "threshold_percent": threshold,
393
+ "aligned": align_signals,
394
+ "sample_rate_mismatch": rate_mismatch,
395
+ }
396
+
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
+ }
408
+
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",
418
+ }
419
+
420
+ # Prepare data for comparison
421
+ data1 = trace1.data.astype(np.float64)
422
+ data2 = trace2.data.astype(np.float64)
423
+
424
+ # Signal alignment using cross-correlation
425
+ if align_signals:
426
+ data1, data2, alignment_info = _align_signals(data1, data2, sample_rate1)
427
+ results["alignment"] = alignment_info
428
+ else:
429
+ # Ensure equal length
430
+ min_len = min(len(data1), len(data2))
431
+ data1 = data1[:min_len]
432
+ data2 = data2[:min_len]
433
+
434
+ # Timing drift analysis
435
+ results["timing_drift"] = _compute_timing_drift(data1, data2, sample_rate1)
436
+
437
+ # Amplitude difference analysis
438
+ diff = data2 - data1
439
+ abs_diff = np.abs(diff)
440
+
441
+ mean1 = data1.mean()
442
+ mean_diff = float(diff.mean())
443
+ mean_diff_percent = abs(mean_diff / mean1 * 100) if mean1 != 0 else 0
444
+ max_diff = float(abs_diff.max())
445
+ rms_diff = float(np.sqrt((diff**2).mean()))
446
+
447
+ # Compare to reference RMS
448
+ rms1 = float(np.sqrt((data1**2).mean()))
449
+ rms_diff_percent = rms_diff / rms1 * 100 if rms1 > 0 else 0
450
+
451
+ results["amplitude_difference"] = {
452
+ "mean_diff_v": f"{mean_diff:.6f}",
453
+ "mean_diff_percent": f"{mean_diff_percent:.2f}%",
454
+ "max_diff_v": f"{max_diff:.6f}",
455
+ "rms_diff_v": f"{rms_diff:.6f}",
456
+ "rms_diff_percent": f"{rms_diff_percent:.2f}%",
457
+ "significant": bool(mean_diff_percent > threshold),
458
+ }
459
+
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
464
+ b, a = signal.butter(4, cutoff / nyquist, btype="high")
465
+
466
+ try:
467
+ noise1 = signal.filtfilt(b, a, data1)
468
+ noise2 = signal.filtfilt(b, a, data2)
469
+ noise_std1 = float(np.std(noise1))
470
+ noise_std2 = float(np.std(noise2))
471
+ except Exception:
472
+ # Fallback to simple std if filter fails
473
+ noise_std1 = float(np.std(data1))
474
+ noise_std2 = float(np.std(data2))
475
+
476
+ noise_change = ((noise_std2 - noise_std1) / noise_std1 * 100) if noise_std1 != 0 else 0
477
+
478
+ results["noise_change"] = {
479
+ "noise1_v": f"{noise_std1:.6f}",
480
+ "noise2_v": f"{noise_std2:.6f}",
481
+ "change_percent": f"{noise_change:.2f}%",
482
+ "significant": bool(abs(noise_change) > threshold),
483
+ }
484
+
485
+ # Correlation coefficient
486
+ if len(data1) > 1 and len(data2) > 1:
487
+ with np.errstate(divide="ignore", invalid="ignore"):
488
+ correlation = float(np.corrcoef(data1, data2)[0, 1])
489
+ results["correlation"] = {
490
+ "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",
498
+ }
499
+
500
+ # Spectral differences
501
+ results["spectral_difference"] = _compute_spectral_difference(
502
+ data1, data2, sample_rate1, threshold
503
+ )
504
+
505
+ # Overall assessment
506
+ significant_count = sum(
507
+ [
508
+ results.get("amplitude_difference", {}).get("significant", False),
509
+ results.get("noise_change", {}).get("significant", False),
510
+ results.get("timing_drift", {}).get("significant", False),
511
+ results.get("spectral_difference", {}).get("significant", False),
512
+ ]
513
+ )
514
+
515
+ if significant_count == 0:
516
+ match_quality = "excellent"
517
+ elif significant_count == 1:
518
+ match_quality = "good"
519
+ elif significant_count == 2:
520
+ match_quality = "fair"
521
+ else:
522
+ match_quality = "poor"
523
+
524
+ results["summary"] = {
525
+ "significant_differences": significant_count,
526
+ "overall_match": match_quality,
527
+ "categories_with_differences": [
528
+ cat
529
+ for cat in [
530
+ "amplitude_difference",
531
+ "noise_change",
532
+ "timing_drift",
533
+ "spectral_difference",
534
+ ]
535
+ if results.get(cat, {}).get("significant", False)
536
+ ],
537
+ }
538
+
539
+ return results
540
+
541
+
542
+ def _generate_html_report(
543
+ results: dict[str, Any],
544
+ file1: str,
545
+ file2: str,
546
+ ) -> str:
547
+ """Generate HTML comparison report.
548
+
549
+ Args:
550
+ results: Comparison results dictionary.
551
+ file1: First file path.
552
+ file2: Second file path.
553
+
554
+ Returns:
555
+ HTML content as string.
556
+ """
557
+ # Get summary info
558
+ summary = results.get("summary", {})
559
+ match_quality = summary.get("overall_match", "unknown")
560
+ significant_diffs = summary.get("significant_differences", 0)
561
+
562
+ # Color based on quality
563
+ quality_colors = {
564
+ "excellent": "#28a745",
565
+ "good": "#17a2b8",
566
+ "fair": "#ffc107",
567
+ "poor": "#dc3545",
568
+ }
569
+ quality_color = quality_colors.get(match_quality, "#6c757d")
570
+
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>TraceKit Signal Comparison Report</title>
577
+ <style>
578
+ body {{
579
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
580
+ max-width: 1200px;
581
+ margin: 0 auto;
582
+ padding: 20px;
583
+ background: #f5f5f5;
584
+ }}
585
+ .header {{
586
+ background: #2c3e50;
587
+ color: white;
588
+ padding: 20px;
589
+ border-radius: 8px 8px 0 0;
590
+ }}
591
+ .content {{
592
+ background: white;
593
+ padding: 20px;
594
+ border-radius: 0 0 8px 8px;
595
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
596
+ }}
597
+ .summary {{
598
+ background: {quality_color};
599
+ color: white;
600
+ padding: 15px;
601
+ border-radius: 8px;
602
+ margin: 20px 0;
603
+ }}
604
+ .section {{
605
+ margin: 20px 0;
606
+ padding: 15px;
607
+ border: 1px solid #e0e0e0;
608
+ border-radius: 8px;
609
+ }}
610
+ .section h3 {{
611
+ margin-top: 0;
612
+ color: #2c3e50;
613
+ }}
614
+ table {{
615
+ width: 100%;
616
+ border-collapse: collapse;
617
+ }}
618
+ th, td {{
619
+ padding: 10px;
620
+ text-align: left;
621
+ border-bottom: 1px solid #e0e0e0;
622
+ }}
623
+ th {{
624
+ background: #f8f9fa;
625
+ }}
626
+ .significant {{
627
+ color: #dc3545;
628
+ font-weight: bold;
629
+ }}
630
+ .ok {{
631
+ color: #28a745;
632
+ }}
633
+ </style>
634
+ </head>
635
+ <body>
636
+ <div class="header">
637
+ <h1>TraceKit Signal Comparison Report</h1>
638
+ <p>File 1: {Path(file1).name}</p>
639
+ <p>File 2: {Path(file2).name}</p>
640
+ </div>
641
+
642
+ <div class="content">
643
+ <div class="summary">
644
+ <h2>Overall Match: {match_quality.upper()}</h2>
645
+ <p>{significant_diffs} significant difference(s) detected</p>
646
+ </div>
647
+
648
+ <div class="section">
649
+ <h3>Trace Statistics</h3>
650
+ <table>
651
+ <tr>
652
+ <th>Metric</th>
653
+ <th>Trace 1</th>
654
+ <th>Trace 2</th>
655
+ </tr>
656
+ <tr>
657
+ <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>
660
+ </tr>
661
+ <tr>
662
+ <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>
665
+ </tr>
666
+ <tr>
667
+ <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>
670
+ </tr>
671
+ <tr>
672
+ <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>
675
+ </tr>
676
+ <tr>
677
+ <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>
680
+ </tr>
681
+ </table>
682
+ </div>
683
+
684
+ <div class="section">
685
+ <h3>Amplitude Difference</h3>
686
+ <table>
687
+ <tr>
688
+ <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")}
692
+ </td>
693
+ </tr>
694
+ <tr>
695
+ <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>
698
+ </tr>
699
+ <tr>
700
+ <td>Max Difference</td>
701
+ <td colspan="2">{results.get("amplitude_difference", {}).get("max_diff_v", "N/A")}</td>
702
+ </tr>
703
+ </table>
704
+ </div>
705
+
706
+ <div class="section">
707
+ <h3>Timing Drift</h3>
708
+ <table>
709
+ <tr>
710
+ <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")}
714
+ </td>
715
+ </tr>
716
+ </table>
717
+ </div>
718
+
719
+ <div class="section">
720
+ <h3>Noise Change</h3>
721
+ <table>
722
+ <tr>
723
+ <td>Trace 1 Noise</td>
724
+ <td>{results.get("noise_change", {}).get("noise1_v", "N/A")}</td>
725
+ </tr>
726
+ <tr>
727
+ <td>Trace 2 Noise</td>
728
+ <td>{results.get("noise_change", {}).get("noise2_v", "N/A")}</td>
729
+ </tr>
730
+ <tr>
731
+ <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")}
734
+ </td>
735
+ </tr>
736
+ </table>
737
+ </div>
738
+
739
+ <div class="section">
740
+ <h3>Spectral Difference</h3>
741
+ <table>
742
+ <tr>
743
+ <td>Dominant Frequency 1</td>
744
+ <td>{results.get("spectral_difference", {}).get("dominant_freq1_hz", "N/A")} Hz</td>
745
+ </tr>
746
+ <tr>
747
+ <td>Dominant Frequency 2</td>
748
+ <td>{results.get("spectral_difference", {}).get("dominant_freq2_hz", "N/A")} Hz</td>
749
+ </tr>
750
+ <tr>
751
+ <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")}
754
+ </td>
755
+ </tr>
756
+ <tr>
757
+ <td>Max Magnitude Difference</td>
758
+ <td>{results.get("spectral_difference", {}).get("max_magnitude_diff_db", "N/A")} dB</td>
759
+ </tr>
760
+ </table>
761
+ </div>
762
+
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
+
769
+ <footer style="margin-top: 30px; text-align: center; color: #6c757d;">
770
+ <p>Generated by TraceKit - Signal Analysis Toolkit</p>
771
+ </footer>
772
+ </div>
773
+ </body>
774
+ </html>"""
775
+ return html