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
@@ -0,0 +1,280 @@
1
+ """EMC/EMI compliance testing workflow.
2
+
3
+ This module implements spectral compliance testing against regulatory limits.
4
+
5
+
6
+ Example:
7
+ >>> import oscura as tk
8
+ >>> trace = tk.load('emissions.wfm')
9
+ >>> result = tk.emc_compliance_test(trace, standard='FCC_Part15_ClassB')
10
+ >>> print(f"Status: {result['status']}")
11
+ >>> print(f"Violations: {len(result['violations'])}")
12
+
13
+ References:
14
+ FCC Part 15: Radio Frequency Devices
15
+ CISPR 22/32: Information Technology Equipment
16
+ MIL-STD-461: EMI/EMC Requirements
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, Any, Literal
22
+
23
+ import numpy as np
24
+
25
+ from oscura.core.exceptions import AnalysisError
26
+
27
+ if TYPE_CHECKING:
28
+ from oscura.core.types import WaveformTrace
29
+
30
+
31
+ def emc_compliance_test(
32
+ trace: WaveformTrace,
33
+ *,
34
+ standard: str = "FCC_Part15_ClassB",
35
+ frequency_range: tuple[float, float] | None = None,
36
+ detector: Literal["peak", "quasi-peak", "average"] = "peak",
37
+ report: str | None = None,
38
+ ) -> dict[str, Any]:
39
+ """EMC/EMI compliance testing against regulatory limits.
40
+
41
+ Performs spectral compliance testing:
42
+ - Computes spectrum (FFT or welch)
43
+ - Loads regulatory limit mask
44
+ - Overlays limit lines on spectrum
45
+ - Identifies violations
46
+ - Generates compliance report
47
+
48
+ Args:
49
+ trace: Signal to test for emissions.
50
+ standard: Regulatory standard to test against:
51
+ 'FCC_Part15_ClassA', 'FCC_Part15_ClassB',
52
+ 'CE_CISPR22_ClassA', 'CE_CISPR22_ClassB',
53
+ 'CE_CISPR32_ClassA', 'CE_CISPR32_ClassB',
54
+ 'MIL_STD_461G_CE102', 'MIL_STD_461G_RE102'
55
+ frequency_range: Optional frequency range (f_min, f_max) in Hz.
56
+ detector: Detector type ('peak', 'quasi-peak', 'average').
57
+ report: Optional path to save HTML compliance report.
58
+
59
+ Returns:
60
+ Dictionary containing:
61
+ - status: 'PASS' or 'FAIL'
62
+ - standard: Standard tested against
63
+ - violations: List of frequency violations
64
+ - margin_to_limit: Minimum margin in dB (negative if failing)
65
+ - worst_frequency: Frequency with worst margin
66
+ - worst_margin: Worst margin value in dB
67
+ - spectrum_freq: Frequency array for spectrum
68
+ - spectrum_mag: Magnitude array for spectrum (dBµV or dBm)
69
+ - limit_freq: Frequency array for limit mask
70
+ - limit_mag: Magnitude array for limit mask
71
+
72
+ Returns:
73
+ Dictionary containing:
74
+ - status: 'PASS' or 'FAIL'
75
+ - standard: Standard tested against
76
+ - violations: List of frequency violations
77
+ - margin_to_limit: Minimum margin in dB (negative if failing)
78
+ - worst_frequency: Frequency with worst margin
79
+ - worst_margin: Worst margin value in dB
80
+ - spectrum_freq: Frequency array for spectrum
81
+ - spectrum_mag: Magnitude array for spectrum (dBµV or dBm)
82
+ - limit_freq: Frequency array for limit mask
83
+ - limit_mag: Magnitude array for limit mask
84
+
85
+ Example:
86
+ >>> trace = tk.load('radiated_emissions.wfm')
87
+ >>> result = tk.emc_compliance_test(trace, standard='FCC_Part15_ClassB')
88
+ >>> print(f"Compliance: {result['status']}")
89
+ >>> print(f"Margin: {result['margin_to_limit']:.1f} dB")
90
+ >>> if result['violations']:
91
+ ... print(f"Violations at: {[v['frequency']/1e6 for v in result['violations']]} MHz")
92
+
93
+ References:
94
+ FCC Part 15 Subpart B (Unintentional Radiators)
95
+ CISPR 22/32 (Information Technology Equipment EMC)
96
+ MIL-STD-461G (Military EMC Requirements)
97
+ """
98
+ # Import spectral analysis
99
+ from oscura.analyzers.waveform.spectral import fft
100
+
101
+ # Calculate spectrum
102
+ freq, mag_db = fft(trace) # type: ignore[misc]
103
+ # Note: fft() returns magnitude_db (already in dB relative to 1V)
104
+
105
+ # Convert to dBµV (typical EMC unit)
106
+ # mag_db is in dBV, convert to dBµV: dBµV = dBV + 120
107
+ # (since 1µV = 1e-6 V, and 20*log10(1e6) = 120 dB)
108
+ spectrum_dbuv = mag_db + 120
109
+
110
+ # Load limit mask for standard
111
+ limit_freq, limit_mag = _load_emc_mask(standard)
112
+
113
+ # Apply frequency range if specified
114
+ if frequency_range is not None:
115
+ f_min, f_max = frequency_range
116
+ mask = (freq >= f_min) & (freq <= f_max)
117
+ freq = freq[mask]
118
+ spectrum_dbuv = spectrum_dbuv[mask]
119
+
120
+ # Interpolate limit to spectrum frequencies
121
+ limit_interp = np.interp(freq, limit_freq, limit_mag)
122
+
123
+ # Find violations (spectrum exceeds limit)
124
+ margin = limit_interp - spectrum_dbuv
125
+ violations_mask = margin < 0
126
+
127
+ # Build violations list
128
+ violations = []
129
+ if np.any(violations_mask):
130
+ violation_indices = np.where(violations_mask)[0]
131
+ for idx in violation_indices:
132
+ violations.append(
133
+ {
134
+ "frequency": freq[idx],
135
+ "measured_dbuv": spectrum_dbuv[idx],
136
+ "limit_dbuv": limit_interp[idx],
137
+ "excess_db": -margin[idx], # Positive value for excess
138
+ }
139
+ )
140
+
141
+ # Overall status
142
+ status = "FAIL" if violations else "PASS"
143
+
144
+ # Margin analysis
145
+ margin_to_limit = np.min(margin)
146
+ worst_idx = np.argmin(margin)
147
+ worst_frequency = freq[worst_idx]
148
+ worst_margin = margin[worst_idx]
149
+
150
+ result = {
151
+ "status": status,
152
+ "standard": standard,
153
+ "violations": violations,
154
+ "margin_to_limit": margin_to_limit,
155
+ "worst_frequency": worst_frequency,
156
+ "worst_margin": worst_margin,
157
+ "spectrum_freq": freq,
158
+ "spectrum_mag": spectrum_dbuv,
159
+ "limit_freq": limit_freq,
160
+ "limit_mag": limit_mag,
161
+ "detector": detector,
162
+ }
163
+
164
+ # Generate report if requested
165
+ if report is not None:
166
+ _generate_compliance_report(result, report)
167
+
168
+ return result
169
+
170
+
171
+ def _load_emc_mask(
172
+ standard: str,
173
+ ) -> tuple[np.ndarray[Any, np.dtype[np.float64]], np.ndarray[Any, np.dtype[np.float64]]]:
174
+ """Load EMC limit mask for a standard.
175
+
176
+ Args:
177
+ standard: Standard name.
178
+
179
+ Returns:
180
+ Tuple of (frequency array, limit array in dBµV).
181
+
182
+ Raises:
183
+ AnalysisError: If unknown EMC standard.
184
+ """
185
+ # Simplified mask data - real implementation would load from data files
186
+ masks = {
187
+ "FCC_Part15_ClassB": {
188
+ # Frequencies in MHz, limits in dBµV at 3m
189
+ "freq": np.array([0.15, 0.5, 5.0, 30.0, 88.0, 216.0, 1000.0]) * 1e6,
190
+ "limit": np.array([60, 60, 56, 46, 46, 46, 46]), # dBµV/m
191
+ },
192
+ "FCC_Part15_ClassA": {
193
+ "freq": np.array([0.15, 0.5, 5.0, 30.0, 88.0, 216.0, 1000.0]) * 1e6,
194
+ "limit": np.array([70, 70, 66, 56, 56, 56, 56]),
195
+ },
196
+ "CE_CISPR22_ClassB": {
197
+ "freq": np.array([0.15, 0.5, 5.0, 30.0, 230.0, 1000.0]) * 1e6,
198
+ "limit": np.array([66, 56, 56, 47, 47, 47]),
199
+ },
200
+ "CE_CISPR22_ClassA": {
201
+ "freq": np.array([0.15, 0.5, 5.0, 30.0, 230.0, 1000.0]) * 1e6,
202
+ "limit": np.array([79, 73, 73, 60, 60, 60]),
203
+ },
204
+ "CE_CISPR32_ClassB": {
205
+ "freq": np.array([0.15, 0.5, 5.0, 30.0, 230.0, 1000.0]) * 1e6,
206
+ "limit": np.array([66, 56, 56, 47, 47, 47]),
207
+ },
208
+ "CE_CISPR32_ClassA": {
209
+ "freq": np.array([0.15, 0.5, 5.0, 30.0, 230.0, 1000.0]) * 1e6,
210
+ "limit": np.array([79, 73, 73, 60, 60, 60]),
211
+ },
212
+ "MIL_STD_461G_CE102": {
213
+ "freq": np.array([0.01, 0.15, 10.0, 50.0]) * 1e6,
214
+ "limit": np.array([90, 80, 80, 80]),
215
+ },
216
+ "MIL_STD_461G_RE102": {
217
+ "freq": np.array([2, 30, 200, 1000, 18000]) * 1e6,
218
+ "limit": np.array([54, 54, 34, 34, 34]),
219
+ },
220
+ }
221
+
222
+ if standard not in masks:
223
+ raise AnalysisError(f"Unknown EMC standard: {standard}")
224
+
225
+ mask_data = masks[standard]
226
+ return mask_data["freq"], mask_data["limit"]
227
+
228
+
229
+ def _generate_compliance_report(result: dict[str, Any], output_path: str) -> None:
230
+ """Generate HTML compliance report.
231
+
232
+ Args:
233
+ result: Compliance test result dictionary.
234
+ output_path: Path to save HTML report.
235
+ """
236
+ status_color = "green" if result["status"] == "PASS" else "red"
237
+
238
+ html = f"""
239
+ <html>
240
+ <head><title>EMC Compliance Report</title></head>
241
+ <body>
242
+ <h1>EMC Compliance Test Report</h1>
243
+ <h2>Standard: {result["standard"]}</h2>
244
+ <h2 style="color: {status_color}">Status: {result["status"]}</h2>
245
+
246
+ <h3>Summary</h3>
247
+ <table>
248
+ <tr><th>Parameter</th><th>Value</th></tr>
249
+ <tr><td>Margin to Limit</td><td>{result["margin_to_limit"]:.2f} dB</td></tr>
250
+ <tr><td>Worst Frequency</td><td>{result["worst_frequency"] / 1e6:.2f} MHz</td></tr>
251
+ <tr><td>Worst Margin</td><td>{result["worst_margin"]:.2f} dB</td></tr>
252
+ <tr><td>Violations</td><td>{len(result["violations"])}</td></tr>
253
+ </table>
254
+ """
255
+ if result["violations"]:
256
+ html += """
257
+ <h3>Violations</h3>
258
+ <table>
259
+ <tr><th>Frequency (MHz)</th><th>Measured (dBµV)</th><th>Limit (dBµV)</th><th>Excess (dB)</th></tr>
260
+ """
261
+ for v in result["violations"]:
262
+ html += f"""
263
+ <tr>
264
+ <td>{v["frequency"] / 1e6:.2f}</td>
265
+ <td>{v["measured_dbuv"]:.2f}</td>
266
+ <td>{v["limit_dbuv"]:.2f}</td>
267
+ <td>{v["excess_db"]:.2f}</td>
268
+ </tr>
269
+ """
270
+ html += "</table>"
271
+
272
+ html += """
273
+ </body>
274
+ </html>
275
+ """
276
+ with open(output_path, "w") as f:
277
+ f.write(html)
278
+
279
+
280
+ __all__ = ["emc_compliance_test"]
@@ -0,0 +1,272 @@
1
+ """Digital buffer characterization workflow.
2
+
3
+ This module implements complete TTL/CMOS buffer characterization in a single
4
+ function call, with automatic logic family detection.
5
+
6
+
7
+ Example:
8
+ >>> import oscura as tk
9
+ >>> trace = tk.load('74hc04_output.wfm')
10
+ >>> result = tk.characterize_buffer(trace)
11
+ >>> print(f"Logic Family: {result['logic_family']}")
12
+ >>> print(f"Rise Time: {result['rise_time']:.2f} ns")
13
+ >>> print(f"Status: {result['status']}")
14
+
15
+ References:
16
+ IEEE 181-2011: Standard for Transitional Waveform Definitions
17
+ JEDEC Standard No. 65B: High-Speed Interface Timing
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ import numpy as np
25
+
26
+ from oscura.core.exceptions import AnalysisError
27
+
28
+ if TYPE_CHECKING:
29
+ from oscura.core.types import WaveformTrace
30
+
31
+
32
+ def characterize_buffer(
33
+ trace: WaveformTrace,
34
+ *,
35
+ reference_trace: WaveformTrace | None = None,
36
+ logic_family: str | None = None,
37
+ thresholds: dict[str, float] | None = None,
38
+ report: str | None = None,
39
+ ) -> dict[str, Any]:
40
+ """Characterize digital buffer timing and quality.
41
+
42
+ One-call characterization of digital buffer including:
43
+ - Automatic logic family detection (if not specified)
44
+ - Rise/fall time measurements
45
+ - Propagation delay (if reference provided)
46
+ - Overshoot/undershoot analysis
47
+ - Noise margin calculation
48
+ - Pass/fail against logic family specifications
49
+
50
+ Args:
51
+ trace: Output signal to characterize.
52
+ reference_trace: Optional reference (input) signal for propagation delay.
53
+ logic_family: Logic family override (e.g., 'TTL', 'CMOS_3V3', 'CMOS_5V').
54
+ If None, auto-detected from signal levels.
55
+ thresholds: Optional dict of custom pass/fail thresholds
56
+ (e.g., {'rise_time': 10e-9} for 10 ns max).
57
+ report: Optional path to save HTML report.
58
+
59
+ Returns:
60
+ Dictionary containing:
61
+ - logic_family: Detected or specified logic family
62
+ - rise_time: 10%-90% rise time in seconds
63
+ - fall_time: 10%-90% fall time in seconds
64
+ - propagation_delay: Delay from reference (if provided), in seconds
65
+ - overshoot: Peak overshoot voltage
66
+ - overshoot_percent: Overshoot as percentage of swing
67
+ - undershoot: Peak undershoot voltage
68
+ - undershoot_percent: Undershoot as percentage of swing
69
+ - noise_margin_high: High-level noise margin in volts
70
+ - noise_margin_low: Low-level noise margin in volts
71
+ - status: 'PASS' or 'FAIL' based on logic family specs
72
+ - reference_comparison: Dict with timing drift if reference provided
73
+ - confidence: Confidence score for logic family detection (0-1)
74
+
75
+ Raises:
76
+ AnalysisError: If trace has insufficient transitions for analysis.
77
+
78
+ Example:
79
+ >>> trace = tk.load('74hc04_output.wfm')
80
+ >>> result = tk.characterize_buffer(trace, logic_family='CMOS_3V3')
81
+ >>> print(f"Rise Time: {result['rise_time']*1e9:.2f} ns")
82
+ >>> print(f"Status: {result['status']}")
83
+
84
+ References:
85
+ IEEE 181-2011 Section 5.2 (Edge timing)
86
+ JEDEC Standard No. 65B (Logic family specifications)
87
+ """
88
+ # Import here to avoid circular dependencies
89
+ from oscura.analyzers.waveform.measurements import (
90
+ fall_time,
91
+ overshoot,
92
+ rise_time,
93
+ undershoot,
94
+ )
95
+ from oscura.inference.logic import detect_logic_family
96
+
97
+ # Auto-detect logic family if not specified
98
+ if logic_family is None:
99
+ detection = detect_logic_family(trace)
100
+ logic_family = detection["primary"]["name"]
101
+ confidence = detection["primary"]["confidence"]
102
+ voh = detection["primary"]["voh"]
103
+ vol = detection["primary"]["vol"]
104
+ else:
105
+ confidence = 1.0
106
+ # Measure VOH/VOL from trace
107
+ voh = np.percentile(trace.data, 95)
108
+ vol = np.percentile(trace.data, 5)
109
+
110
+ # Measure timing parameters
111
+ try:
112
+ t_rise = rise_time(trace)
113
+ t_fall = fall_time(trace)
114
+ except Exception as e:
115
+ raise AnalysisError(f"Failed to measure rise/fall time: {e}") from e
116
+
117
+ # Measure overshoot/undershoot
118
+ v_overshoot = overshoot(trace)
119
+ v_undershoot = undershoot(trace)
120
+
121
+ # Calculate percentages
122
+ swing = voh - vol
123
+ if swing > 0:
124
+ overshoot_pct = (v_overshoot / swing) * 100.0
125
+ undershoot_pct = (v_undershoot / swing) * 100.0
126
+ else:
127
+ overshoot_pct = 0.0
128
+ undershoot_pct = 0.0
129
+
130
+ # Calculate noise margins (simplified - uses typical values)
131
+ # In a real implementation, these would come from LOGIC_FAMILIES constants
132
+ logic_specs = _get_logic_specs(logic_family)
133
+ noise_margin_high = voh - logic_specs["vih"]
134
+ noise_margin_low = logic_specs["vil"] - vol
135
+
136
+ # Propagation delay if reference provided
137
+ propagation_delay = None
138
+ timing_drift = None
139
+ if reference_trace is not None:
140
+ try:
141
+ from oscura.analyzers.digital.timing import (
142
+ propagation_delay as prop_delay,
143
+ )
144
+
145
+ propagation_delay = prop_delay(reference_trace, trace)
146
+ except Exception:
147
+ # If propagation delay measurement fails, set to None
148
+ propagation_delay = None
149
+
150
+ # Apply thresholds and determine pass/fail
151
+ status = "PASS"
152
+ if thresholds is not None:
153
+ if "rise_time" in thresholds and t_rise > thresholds["rise_time"]:
154
+ status = "FAIL"
155
+ if "fall_time" in thresholds and t_fall > thresholds["fall_time"]:
156
+ status = "FAIL"
157
+ if "overshoot_percent" in thresholds and overshoot_pct > thresholds["overshoot_percent"]:
158
+ status = "FAIL"
159
+ else:
160
+ # Use logic family defaults
161
+ if t_rise > logic_specs.get("max_rise_time", float("inf")):
162
+ status = "FAIL"
163
+ if t_fall > logic_specs.get("max_fall_time", float("inf")):
164
+ status = "FAIL"
165
+
166
+ # Build result dictionary
167
+ result = {
168
+ "logic_family": logic_family,
169
+ "confidence": confidence,
170
+ "rise_time": t_rise,
171
+ "fall_time": t_fall,
172
+ "propagation_delay": propagation_delay,
173
+ "overshoot": v_overshoot,
174
+ "overshoot_percent": overshoot_pct,
175
+ "undershoot": v_undershoot,
176
+ "undershoot_percent": undershoot_pct,
177
+ "noise_margin_high": noise_margin_high,
178
+ "noise_margin_low": noise_margin_low,
179
+ "voh": voh,
180
+ "vol": vol,
181
+ "status": status,
182
+ "reference_comparison": None,
183
+ }
184
+
185
+ if reference_trace is not None and propagation_delay is not None:
186
+ result["reference_comparison"] = {
187
+ "propagation_delay": propagation_delay,
188
+ "timing_drift": timing_drift,
189
+ }
190
+
191
+ # Generate report if requested
192
+ if report is not None:
193
+ _generate_buffer_report(result, report)
194
+
195
+ return result
196
+
197
+
198
+ def _get_logic_specs(family: str) -> dict[str, float]:
199
+ """Get specifications for a logic family.
200
+
201
+ Args:
202
+ family: Logic family name.
203
+
204
+ Returns:
205
+ Dict with VIH, VIL, and timing specs.
206
+ """
207
+ specs = {
208
+ "TTL": {
209
+ "vih": 2.0,
210
+ "vil": 0.8,
211
+ "max_rise_time": 10e-9,
212
+ "max_fall_time": 10e-9,
213
+ },
214
+ "CMOS_5V": {
215
+ "vih": 3.5,
216
+ "vil": 1.5,
217
+ "max_rise_time": 15e-9,
218
+ "max_fall_time": 15e-9,
219
+ },
220
+ "CMOS_3V3": {
221
+ "vih": 2.0,
222
+ "vil": 0.8,
223
+ "max_rise_time": 5e-9,
224
+ "max_fall_time": 5e-9,
225
+ },
226
+ "LVTTL": {
227
+ "vih": 2.0,
228
+ "vil": 0.8,
229
+ "max_rise_time": 3e-9,
230
+ "max_fall_time": 3e-9,
231
+ },
232
+ "LVCMOS": {
233
+ "vih": 1.7,
234
+ "vil": 0.7,
235
+ "max_rise_time": 2e-9,
236
+ "max_fall_time": 2e-9,
237
+ },
238
+ }
239
+ return specs.get(family, specs["CMOS_3V3"])
240
+
241
+
242
+ def _generate_buffer_report(result: dict[str, Any], output_path: str) -> None:
243
+ """Generate HTML report for buffer characterization.
244
+
245
+ Args:
246
+ result: Characterization result dictionary.
247
+ output_path: Path to save HTML report.
248
+ """
249
+ # Simplified report generation - in real implementation would use
250
+ # oscura.reporting module
251
+ html = f"""
252
+ <html>
253
+ <head><title>Buffer Characterization Report</title></head>
254
+ <body>
255
+ <h1>Buffer Characterization Report</h1>
256
+ <h2>Logic Family: {result["logic_family"]} (confidence: {result["confidence"]:.1%})</h2>
257
+ <table>
258
+ <tr><th>Parameter</th><th>Value</th><th>Units</th></tr>
259
+ <tr><td>Rise Time</td><td>{result["rise_time"] * 1e9:.2f}</td><td>ns</td></tr>
260
+ <tr><td>Fall Time</td><td>{result["fall_time"] * 1e9:.2f}</td><td>ns</td></tr>
261
+ <tr><td>Overshoot</td><td>{result["overshoot_percent"]:.1f}</td><td>%</td></tr>
262
+ <tr><td>Undershoot</td><td>{result["undershoot_percent"]:.1f}</td><td>%</td></tr>
263
+ <tr><td>Status</td><td><b>{result["status"]}</b></td><td></td></tr>
264
+ </table>
265
+ </body>
266
+ </html>
267
+ """
268
+ with open(output_path, "w") as f:
269
+ f.write(html)
270
+
271
+
272
+ __all__ = ["characterize_buffer"]