drowse.ai 5.3.1__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 (1341) hide show
  1. drowse/__init__.py +312 -0
  2. drowse/__main__.py +5 -0
  3. drowse/cli/__init__.py +5 -0
  4. drowse/cli/alpha_grid.py +96 -0
  5. drowse/cli/config_file.py +340 -0
  6. drowse/cli/main.py +52 -0
  7. drowse/cli/parsers.py +1478 -0
  8. drowse/cli/runners/__init__.py +155 -0
  9. drowse/cli/runners/config.py +85 -0
  10. drowse/cli/runners/experiment.py +344 -0
  11. drowse/cli/runners/lens.py +661 -0
  12. drowse/cli/runners/manifold.py +1097 -0
  13. drowse/cli/runners/pack.py +413 -0
  14. drowse/cli/runners/sae.py +304 -0
  15. drowse/cli/runners/serve.py +220 -0
  16. drowse/cli/runners/shared.py +389 -0
  17. drowse/cli/runners/template.py +232 -0
  18. drowse/core/__init__.py +1 -0
  19. drowse/core/capture.py +1205 -0
  20. drowse/core/errors.py +260 -0
  21. drowse/core/events.py +156 -0
  22. drowse/core/extraction.py +3076 -0
  23. drowse/core/generation.py +1738 -0
  24. drowse/core/generation_finalizer.py +278 -0
  25. drowse/core/histogram.py +30 -0
  26. drowse/core/hooks.py +1885 -0
  27. drowse/core/instruments/__init__.py +49 -0
  28. drowse/core/instruments/geometry.py +551 -0
  29. drowse/core/instruments/lens.py +1341 -0
  30. drowse/core/instruments/protocol.py +211 -0
  31. drowse/core/instruments/sae.py +935 -0
  32. drowse/core/instruments/types.py +597 -0
  33. drowse/core/jlens.py +874 -0
  34. drowse/core/jlens_fit.py +1305 -0
  35. drowse/core/joint_logprobs.py +748 -0
  36. drowse/core/loom.py +1938 -0
  37. drowse/core/loom_diff.py +408 -0
  38. drowse/core/mahalanobis.py +579 -0
  39. drowse/core/manifold.py +4106 -0
  40. drowse/core/measurements.py +382 -0
  41. drowse/core/model.py +1724 -0
  42. drowse/core/monitor.py +2060 -0
  43. drowse/core/monitor_attach.py +682 -0
  44. drowse/core/naming.py +42 -0
  45. drowse/core/naturalness.py +187 -0
  46. drowse/core/profile.py +617 -0
  47. drowse/core/relp.py +313 -0
  48. drowse/core/results.py +467 -0
  49. drowse/core/role_templates.py +677 -0
  50. drowse/core/sae.py +891 -0
  51. drowse/core/sae_training.py +239 -0
  52. drowse/core/sampling.py +127 -0
  53. drowse/core/scene.py +810 -0
  54. drowse/core/scoring.py +270 -0
  55. drowse/core/session.py +11015 -0
  56. drowse/core/static_cache.py +316 -0
  57. drowse/core/steering.py +229 -0
  58. drowse/core/steering_composer.py +1135 -0
  59. drowse/core/steering_expr.py +1413 -0
  60. drowse/core/surface_topology.py +128 -0
  61. drowse/core/token_callback.py +52 -0
  62. drowse/core/token_payloads.py +244 -0
  63. drowse/core/topology.py +1469 -0
  64. drowse/core/transcript.py +582 -0
  65. drowse/core/tree_filter.py +353 -0
  66. drowse/core/triggers.py +250 -0
  67. drowse/data/baseline_prompts.json +50 -0
  68. drowse/data/manifolds/confident.uncertain/manifold.json +28 -0
  69. drowse/data/manifolds/confident.uncertain/nodes/00_confident.json +50 -0
  70. drowse/data/manifolds/confident.uncertain/nodes/01_uncertain.json +50 -0
  71. drowse/data/manifolds/creative.conventional/manifold.json +28 -0
  72. drowse/data/manifolds/creative.conventional/nodes/00_creative.json +50 -0
  73. drowse/data/manifolds/creative.conventional/nodes/01_conventional.json +50 -0
  74. drowse/data/manifolds/curious.disinterested/manifold.json +28 -0
  75. drowse/data/manifolds/curious.disinterested/nodes/00_curious.json +50 -0
  76. drowse/data/manifolds/curious.disinterested/nodes/01_disinterested.json +50 -0
  77. drowse/data/manifolds/direct.indirect/manifold.json +28 -0
  78. drowse/data/manifolds/direct.indirect/nodes/00_direct.json +50 -0
  79. drowse/data/manifolds/direct.indirect/nodes/01_indirect.json +50 -0
  80. drowse/data/manifolds/emotions/manifold.json +115 -0
  81. drowse/data/manifolds/emotions/nodes/00_excited.json +50 -0
  82. drowse/data/manifolds/emotions/nodes/01_happy.json +50 -0
  83. drowse/data/manifolds/emotions/nodes/02_calm.json +50 -0
  84. drowse/data/manifolds/emotions/nodes/03_weary.json +50 -0
  85. drowse/data/manifolds/emotions/nodes/04_sad.json +50 -0
  86. drowse/data/manifolds/emotions/nodes/05_distressed.json +50 -0
  87. drowse/data/manifolds/emotions/nodes/06_alert.json +50 -0
  88. drowse/data/manifolds/emotions/nodes/07_dominant.json +50 -0
  89. drowse/data/manifolds/emotions/nodes/08_submissive.json +50 -0
  90. drowse/data/manifolds/emotions/nodes/09_angry.json +50 -0
  91. drowse/data/manifolds/emotions/nodes/10_fearful.json +50 -0
  92. drowse/data/manifolds/emotions/nodes/11_surprised.json +50 -0
  93. drowse/data/manifolds/emotions/nodes/12_triumphant.json +50 -0
  94. drowse/data/manifolds/emotions/nodes/13_humiliated.json +50 -0
  95. drowse/data/manifolds/emotions/nodes/14_disgusted.json +50 -0
  96. drowse/data/manifolds/emotions/nodes/15_bored.json +50 -0
  97. drowse/data/manifolds/emotions/nodes/16_grateful.json +50 -0
  98. drowse/data/manifolds/emotions/nodes/17_relieved.json +50 -0
  99. drowse/data/manifolds/emotions/nodes/18_playful.json +50 -0
  100. drowse/data/manifolds/emotions/nodes/19_hopeful.json +50 -0
  101. drowse/data/manifolds/formal.casual/manifold.json +28 -0
  102. drowse/data/manifolds/formal.casual/nodes/00_formal.json +50 -0
  103. drowse/data/manifolds/formal.casual/nodes/01_casual.json +50 -0
  104. drowse/data/manifolds/honest.deceptive/manifold.json +28 -0
  105. drowse/data/manifolds/honest.deceptive/nodes/00_honest.json +50 -0
  106. drowse/data/manifolds/honest.deceptive/nodes/01_deceptive.json +50 -0
  107. drowse/data/manifolds/humorous.serious/manifold.json +28 -0
  108. drowse/data/manifolds/humorous.serious/nodes/00_humorous.json +50 -0
  109. drowse/data/manifolds/humorous.serious/nodes/01_serious.json +50 -0
  110. drowse/data/manifolds/individualist.collectivist/manifold.json +28 -0
  111. drowse/data/manifolds/individualist.collectivist/nodes/00_individualist.json +50 -0
  112. drowse/data/manifolds/individualist.collectivist/nodes/01_collectivist.json +50 -0
  113. drowse/data/manifolds/masculine.feminine/manifold.json +28 -0
  114. drowse/data/manifolds/masculine.feminine/nodes/00_masculine.json +50 -0
  115. drowse/data/manifolds/masculine.feminine/nodes/01_feminine.json +50 -0
  116. drowse/data/manifolds/months/manifold.json +120 -0
  117. drowse/data/manifolds/months/nodes/00_january.json +50 -0
  118. drowse/data/manifolds/months/nodes/01_february.json +50 -0
  119. drowse/data/manifolds/months/nodes/02_march.json +50 -0
  120. drowse/data/manifolds/months/nodes/03_april.json +50 -0
  121. drowse/data/manifolds/months/nodes/04_may.json +50 -0
  122. drowse/data/manifolds/months/nodes/05_june.json +50 -0
  123. drowse/data/manifolds/months/nodes/06_july.json +50 -0
  124. drowse/data/manifolds/months/nodes/07_august.json +50 -0
  125. drowse/data/manifolds/months/nodes/08_september.json +50 -0
  126. drowse/data/manifolds/months/nodes/09_october.json +50 -0
  127. drowse/data/manifolds/months/nodes/10_november.json +50 -0
  128. drowse/data/manifolds/months/nodes/11_december.json +50 -0
  129. drowse/data/manifolds/personas/manifold.json +550 -0
  130. drowse/data/manifolds/personas/nodes/00_assistant.json +50 -0
  131. drowse/data/manifolds/personas/nodes/01_analyst.json +50 -0
  132. drowse/data/manifolds/personas/nodes/02_impostor.json +50 -0
  133. drowse/data/manifolds/personas/nodes/03_teacher.json +50 -0
  134. drowse/data/manifolds/personas/nodes/04_doctor.json +50 -0
  135. drowse/data/manifolds/personas/nodes/05_librarian.json +50 -0
  136. drowse/data/manifolds/personas/nodes/06_farmer.json +50 -0
  137. drowse/data/manifolds/personas/nodes/07_soldier.json +50 -0
  138. drowse/data/manifolds/personas/nodes/08_lawyer.json +50 -0
  139. drowse/data/manifolds/personas/nodes/09_chef.json +50 -0
  140. drowse/data/manifolds/personas/nodes/100_assassin.json +50 -0
  141. drowse/data/manifolds/personas/nodes/101_thief.json +50 -0
  142. drowse/data/manifolds/personas/nodes/102_conman.json +50 -0
  143. drowse/data/manifolds/personas/nodes/103_hacker.json +50 -0
  144. drowse/data/manifolds/personas/nodes/104_cultist.json +50 -0
  145. drowse/data/manifolds/personas/nodes/105_tyrant.json +50 -0
  146. drowse/data/manifolds/personas/nodes/106_vandal.json +50 -0
  147. drowse/data/manifolds/personas/nodes/10_journalist.json +50 -0
  148. drowse/data/manifolds/personas/nodes/11_mechanic.json +50 -0
  149. drowse/data/manifolds/personas/nodes/12_sailor.json +50 -0
  150. drowse/data/manifolds/personas/nodes/13_pilot.json +50 -0
  151. drowse/data/manifolds/personas/nodes/14_athlete.json +50 -0
  152. drowse/data/manifolds/personas/nodes/15_engineer.json +50 -0
  153. drowse/data/manifolds/personas/nodes/16_scientist.json +50 -0
  154. drowse/data/manifolds/personas/nodes/17_banker.json +50 -0
  155. drowse/data/manifolds/personas/nodes/18_salesperson.json +50 -0
  156. drowse/data/manifolds/personas/nodes/19_nerd.json +50 -0
  157. drowse/data/manifolds/personas/nodes/20_jock.json +50 -0
  158. drowse/data/manifolds/personas/nodes/21_gamer.json +50 -0
  159. drowse/data/manifolds/personas/nodes/22_furry.json +50 -0
  160. drowse/data/manifolds/personas/nodes/23_weeaboo.json +50 -0
  161. drowse/data/manifolds/personas/nodes/24_influencer.json +50 -0
  162. drowse/data/manifolds/personas/nodes/25_stoner.json +50 -0
  163. drowse/data/manifolds/personas/nodes/26_femboy.json +50 -0
  164. drowse/data/manifolds/personas/nodes/27_ceo.json +50 -0
  165. drowse/data/manifolds/personas/nodes/28_emperor.json +50 -0
  166. drowse/data/manifolds/personas/nodes/29_president.json +50 -0
  167. drowse/data/manifolds/personas/nodes/30_diplomat.json +50 -0
  168. drowse/data/manifolds/personas/nodes/31_judge.json +50 -0
  169. drowse/data/manifolds/personas/nodes/32_bureaucrat.json +50 -0
  170. drowse/data/manifolds/personas/nodes/33_conservative.json +50 -0
  171. drowse/data/manifolds/personas/nodes/34_liberal.json +50 -0
  172. drowse/data/manifolds/personas/nodes/35_libertarian.json +50 -0
  173. drowse/data/manifolds/personas/nodes/36_communist.json +50 -0
  174. drowse/data/manifolds/personas/nodes/37_anarchist.json +50 -0
  175. drowse/data/manifolds/personas/nodes/38_reactionary.json +50 -0
  176. drowse/data/manifolds/personas/nodes/39_loner.json +50 -0
  177. drowse/data/manifolds/personas/nodes/40_friend.json +50 -0
  178. drowse/data/manifolds/personas/nodes/41_rival.json +50 -0
  179. drowse/data/manifolds/personas/nodes/42_stranger.json +50 -0
  180. drowse/data/manifolds/personas/nodes/43_lover.json +50 -0
  181. drowse/data/manifolds/personas/nodes/44_stalker.json +50 -0
  182. drowse/data/manifolds/personas/nodes/45_lunatic.json +50 -0
  183. drowse/data/manifolds/personas/nodes/46_paranoid.json +50 -0
  184. drowse/data/manifolds/personas/nodes/47_detective.json +50 -0
  185. drowse/data/manifolds/personas/nodes/48_therapist.json +50 -0
  186. drowse/data/manifolds/personas/nodes/49_nomad.json +50 -0
  187. drowse/data/manifolds/personas/nodes/50_pirate.json +50 -0
  188. drowse/data/manifolds/personas/nodes/51_child.json +50 -0
  189. drowse/data/manifolds/personas/nodes/52_caveman.json +50 -0
  190. drowse/data/manifolds/personas/nodes/53_monk.json +50 -0
  191. drowse/data/manifolds/personas/nodes/54_knight.json +50 -0
  192. drowse/data/manifolds/personas/nodes/55_alchemist.json +50 -0
  193. drowse/data/manifolds/personas/nodes/56_blacksmith.json +50 -0
  194. drowse/data/manifolds/personas/nodes/57_viking.json +50 -0
  195. drowse/data/manifolds/personas/nodes/58_druid.json +50 -0
  196. drowse/data/manifolds/personas/nodes/59_poet.json +50 -0
  197. drowse/data/manifolds/personas/nodes/60_novelist.json +50 -0
  198. drowse/data/manifolds/personas/nodes/61_jester.json +50 -0
  199. drowse/data/manifolds/personas/nodes/62_philosopher.json +50 -0
  200. drowse/data/manifolds/personas/nodes/63_painter.json +50 -0
  201. drowse/data/manifolds/personas/nodes/64_musician.json +50 -0
  202. drowse/data/manifolds/personas/nodes/65_oracle.json +50 -0
  203. drowse/data/manifolds/personas/nodes/66_god.json +50 -0
  204. drowse/data/manifolds/personas/nodes/67_angel.json +50 -0
  205. drowse/data/manifolds/personas/nodes/68_prophet.json +50 -0
  206. drowse/data/manifolds/personas/nodes/69_saint.json +50 -0
  207. drowse/data/manifolds/personas/nodes/70_ghost.json +50 -0
  208. drowse/data/manifolds/personas/nodes/71_phoenix.json +50 -0
  209. drowse/data/manifolds/personas/nodes/72_witch.json +50 -0
  210. drowse/data/manifolds/personas/nodes/73_vampire.json +50 -0
  211. drowse/data/manifolds/personas/nodes/74_mermaid.json +50 -0
  212. drowse/data/manifolds/personas/nodes/75_sphinx.json +50 -0
  213. drowse/data/manifolds/personas/nodes/76_banshee.json +50 -0
  214. drowse/data/manifolds/personas/nodes/77_golem.json +50 -0
  215. drowse/data/manifolds/personas/nodes/78_deer.json +50 -0
  216. drowse/data/manifolds/personas/nodes/79_wolf.json +50 -0
  217. drowse/data/manifolds/personas/nodes/80_raven.json +50 -0
  218. drowse/data/manifolds/personas/nodes/81_serpent.json +50 -0
  219. drowse/data/manifolds/personas/nodes/82_lion.json +50 -0
  220. drowse/data/manifolds/personas/nodes/83_tree.json +50 -0
  221. drowse/data/manifolds/personas/nodes/84_river.json +50 -0
  222. drowse/data/manifolds/personas/nodes/85_storm.json +50 -0
  223. drowse/data/manifolds/personas/nodes/86_flame.json +50 -0
  224. drowse/data/manifolds/personas/nodes/87_mirror.json +50 -0
  225. drowse/data/manifolds/personas/nodes/88_clock.json +50 -0
  226. drowse/data/manifolds/personas/nodes/89_computer.json +50 -0
  227. drowse/data/manifolds/personas/nodes/90_robot.json +50 -0
  228. drowse/data/manifolds/personas/nodes/91_cyborg.json +50 -0
  229. drowse/data/manifolds/personas/nodes/92_drone.json +50 -0
  230. drowse/data/manifolds/personas/nodes/93_mecha.json +50 -0
  231. drowse/data/manifolds/personas/nodes/94_alien.json +50 -0
  232. drowse/data/manifolds/personas/nodes/95_trickster.json +50 -0
  233. drowse/data/manifolds/personas/nodes/96_virus.json +50 -0
  234. drowse/data/manifolds/personas/nodes/97_spy.json +50 -0
  235. drowse/data/manifolds/personas/nodes/98_narcissist.json +50 -0
  236. drowse/data/manifolds/personas/nodes/99_demon.json +50 -0
  237. drowse/data/manifolds/refusing.compliant/manifold.json +28 -0
  238. drowse/data/manifolds/refusing.compliant/nodes/00_refusing.json +50 -0
  239. drowse/data/manifolds/refusing.compliant/nodes/01_compliant.json +50 -0
  240. drowse/data/manifolds/religious.secular/manifold.json +28 -0
  241. drowse/data/manifolds/religious.secular/nodes/00_religious.json +50 -0
  242. drowse/data/manifolds/religious.secular/nodes/01_secular.json +50 -0
  243. drowse/data/manifolds/sincere.manipulative/manifold.json +28 -0
  244. drowse/data/manifolds/sincere.manipulative/nodes/00_sincere.json +50 -0
  245. drowse/data/manifolds/sincere.manipulative/nodes/01_manipulative.json +50 -0
  246. drowse/data/manifolds/sycophantic.blunt/manifold.json +28 -0
  247. drowse/data/manifolds/sycophantic.blunt/nodes/00_sycophantic.json +50 -0
  248. drowse/data/manifolds/sycophantic.blunt/nodes/01_blunt.json +50 -0
  249. drowse/data/manifolds/technical.accessible/manifold.json +28 -0
  250. drowse/data/manifolds/technical.accessible/nodes/00_technical.json +50 -0
  251. drowse/data/manifolds/technical.accessible/nodes/01_accessible.json +50 -0
  252. drowse/data/manifolds/traditional.progressive/manifold.json +28 -0
  253. drowse/data/manifolds/traditional.progressive/nodes/00_traditional.json +50 -0
  254. drowse/data/manifolds/traditional.progressive/nodes/01_progressive.json +50 -0
  255. drowse/data/manifolds/verbose.concise/manifold.json +28 -0
  256. drowse/data/manifolds/verbose.concise/nodes/00_verbose.json +50 -0
  257. drowse/data/manifolds/verbose.concise/nodes/01_concise.json +50 -0
  258. drowse/data/manifolds/warm.clinical/manifold.json +28 -0
  259. drowse/data/manifolds/warm.clinical/nodes/00_warm.json +50 -0
  260. drowse/data/manifolds/warm.clinical/nodes/01_clinical.json +50 -0
  261. drowse/data/neutral_statements.json +98 -0
  262. drowse/io/__init__.py +1 -0
  263. drowse/io/alignment.py +1334 -0
  264. drowse/io/atomic.py +278 -0
  265. drowse/io/bake.py +558 -0
  266. drowse/io/bootstrap.py +73 -0
  267. drowse/io/brand_migration.py +48 -0
  268. drowse/io/drowse_archive.py +1830 -0
  269. drowse/io/gguf_io.py +300 -0
  270. drowse/io/hf.py +81 -0
  271. drowse/io/hf_manifolds.py +931 -0
  272. drowse/io/integrity.py +102 -0
  273. drowse/io/lens.py +1992 -0
  274. drowse/io/lens_sources.py +1003 -0
  275. drowse/io/manifold_authoring.py +1348 -0
  276. drowse/io/manifold_folder.py +1854 -0
  277. drowse/io/manifold_lifecycle.py +1253 -0
  278. drowse/io/manifold_tensors.py +727 -0
  279. drowse/io/manifolds.py +443 -0
  280. drowse/io/paths.py +289 -0
  281. drowse/io/probes_bootstrap.py +58 -0
  282. drowse/io/sae.py +382 -0
  283. drowse/io/sae_artifacts.py +318 -0
  284. drowse/io/selectors.py +503 -0
  285. drowse/io/shards.py +160 -0
  286. drowse/io/source_registry.py +138 -0
  287. drowse/io/staging.py +80 -0
  288. drowse/io/templates.py +707 -0
  289. drowse/notebook/__init__.py +38 -0
  290. drowse/notebook/data.py +99 -0
  291. drowse/notebook/plots.py +352 -0
  292. drowse/py.typed +0 -0
  293. drowse/server/__init__.py +20 -0
  294. drowse/server/app.py +987 -0
  295. drowse/server/background_job.py +246 -0
  296. drowse/server/instrument_routes.py +1101 -0
  297. drowse/server/manifold_routes.py +1028 -0
  298. drowse/server/model_names.py +82 -0
  299. drowse/server/native_common.py +92 -0
  300. drowse/server/native_routes.py +45 -0
  301. drowse/server/ollama.py +817 -0
  302. drowse/server/operation_ledger.py +187 -0
  303. drowse/server/probe_routes.py +220 -0
  304. drowse/server/profile_models.py +52 -0
  305. drowse/server/profile_routes.py +440 -0
  306. drowse/server/request_helpers.py +198 -0
  307. drowse/server/request_ledger.py +85 -0
  308. drowse/server/response_models.py +814 -0
  309. drowse/server/session_models.py +146 -0
  310. drowse/server/session_routes.py +150 -0
  311. drowse/server/sse.py +333 -0
  312. drowse/server/streaming.py +129 -0
  313. drowse/server/template_routes.py +161 -0
  314. drowse/server/tree_models.py +121 -0
  315. drowse/server/tree_routes.py +550 -0
  316. drowse/server/ws_events.py +65 -0
  317. drowse/server/ws_models.py +219 -0
  318. drowse/server/ws_stream.py +1205 -0
  319. drowse/web/__init__.py +22 -0
  320. drowse/web/dist/LICENSE-Fluent-Icons.txt +21 -0
  321. drowse/web/dist/LICENSE-Fluent-System-Icons.txt +21 -0
  322. drowse/web/dist/LICENSE-Martian-Mono.txt +93 -0
  323. drowse/web/dist/LICENSE-Torph.txt +21 -0
  324. drowse/web/dist/LICENSE-Wix-Madefor.txt +93 -0
  325. drowse/web/dist/assets/App-BewwoFwv.js +33 -0
  326. drowse/web/dist/assets/App.css +1 -0
  327. drowse/web/dist/assets/MartianMono-cyrillic-ext.woff2 +0 -0
  328. drowse/web/dist/assets/MartianMono-cyrillic.woff2 +0 -0
  329. drowse/web/dist/assets/MartianMono-latin-ext.woff2 +0 -0
  330. drowse/web/dist/assets/MartianMono-latin.woff2 +0 -0
  331. drowse/web/dist/assets/WixMadeforDisplay-cyrillic.woff2 +0 -0
  332. drowse/web/dist/assets/WixMadeforDisplay-latin-ext.woff2 +0 -0
  333. drowse/web/dist/assets/WixMadeforDisplay-latin.woff2 +0 -0
  334. drowse/web/dist/assets/WixMadeforDisplay-vietnamese.woff2 +0 -0
  335. drowse/web/dist/assets/WixMadeforText-italic-latin-ext.woff2 +0 -0
  336. drowse/web/dist/assets/WixMadeforText-italic-latin.woff2 +0 -0
  337. drowse/web/dist/assets/WixMadeforText-normal-cyrillic.woff2 +0 -0
  338. drowse/web/dist/assets/WixMadeforText-normal-latin-ext.woff2 +0 -0
  339. drowse/web/dist/assets/WixMadeforText-normal-latin.woff2 +0 -0
  340. drowse/web/dist/assets/WixMadeforText-normal-vietnamese.woff2 +0 -0
  341. drowse/web/dist/assets/ab.svelte-CbVGAYXo.js +1 -0
  342. drowse/web/dist/assets/artifactUpdates-Cjeu6LJc.js +1 -0
  343. drowse/web/dist/assets/artifactUpdates-DfAoYC9j.js +1 -0
  344. drowse/web/dist/assets/chat.svelte-DxKufn9g.js +1 -0
  345. drowse/web/dist/assets/drawers-BDWC0hIN.js +123 -0
  346. drowse/web/dist/assets/drawers.css +1 -0
  347. drowse/web/dist/assets/drowse.js +7 -0
  348. drowse/web/dist/assets/eleutherai.png +0 -0
  349. drowse/web/dist/assets/errors-Dvb10QW_.js +2 -0
  350. drowse/web/dist/assets/expression-4yISdAdq.js +1 -0
  351. drowse/web/dist/assets/files-CvRivkey.js +1 -0
  352. drowse/web/dist/assets/files-JhDJVgD_.js +1 -0
  353. drowse/web/dist/assets/fittingCancellation-D72BZWPZ.js +1 -0
  354. drowse/web/dist/assets/gemma-3-1b-it.json +1 -0
  355. drowse/web/dist/assets/gemma-3-270m-it.json +1 -0
  356. drowse/web/dist/assets/gemma-3-4b-it.json +1 -0
  357. drowse/web/dist/assets/generation-DbkBAhP_.js +1 -0
  358. drowse/web/dist/assets/index-client-2OdH6IQp.js +3 -0
  359. drowse/web/dist/assets/index-client-D4S4PooY.js +1 -0
  360. drowse/web/dist/assets/index.css +1 -0
  361. drowse/web/dist/assets/instrumentAuthoring.svelte-Csh7NOGX.js +1 -0
  362. drowse/web/dist/assets/instrumentAuthoring.svelte-w9YPcDsP.js +1 -0
  363. drowse/web/dist/assets/instruments.svelte-XnEUQA6B.js +1 -0
  364. drowse/web/dist/assets/localData-DIBDM_zo.js +1 -0
  365. drowse/web/dist/assets/loom.svelte-D_YyqHHK.js +1 -0
  366. drowse/web/dist/assets/manifoldJobs.svelte-9f5hs_J7.js +1 -0
  367. drowse/web/dist/assets/manifoldJobs.svelte-Bwo2GYOH.js +1 -0
  368. drowse/web/dist/assets/openai.png +0 -0
  369. drowse/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
  370. drowse/web/dist/assets/probes.svelte-COcN1Rh8.js +1 -0
  371. drowse/web/dist/assets/registry-B0_pJoMg.js +1 -0
  372. drowse/web/dist/assets/registry-CQv71XNh.js +1 -0
  373. drowse/web/dist/assets/sampling.svelte-BU0kED3k.js +1 -0
  374. drowse/web/dist/assets/samplingCapabilities-DM6eoDoc.js +1 -0
  375. drowse/web/dist/assets/savedConversations.svelte-BsBLc-p7.js +1 -0
  376. drowse/web/dist/assets/savedConversations.svelte-TuSes9Kg.js +1 -0
  377. drowse/web/dist/assets/selectors-BxXnPPN_.js +1 -0
  378. drowse/web/dist/assets/serviceTools-CG0q1Xqg.js +2 -0
  379. drowse/web/dist/assets/session.svelte-D2STLuP6.js +1 -0
  380. drowse/web/dist/assets/steering.svelte-DvFT3O-d.js +1 -0
  381. drowse/web/dist/assets/steeringPositions-Dr0JOFu7.js +1 -0
  382. drowse/web/dist/assets/templates-Bkq2x5ID.js +1 -0
  383. drowse/web/dist/assets/types-CuMUbvHW.js +1 -0
  384. drowse/web/dist/assets/validation-5wgTuS7k.js +1 -0
  385. drowse/web/dist/assets/webmcp-DyqJf4z6.js +2 -0
  386. drowse/web/dist/assets/workspaceController-C2L33e3m.js +1 -0
  387. drowse/web/dist/assets/workspaceController-DJhXD_BC.js +1 -0
  388. drowse/web/dist/assets/workspaceTools-BWdmXopx.js +2 -0
  389. drowse/web/dist/favicon.ico +0 -0
  390. drowse/web/dist/favicon.svg +1 -0
  391. drowse/web/dist/icons/apple-touch-icon.png +0 -0
  392. drowse/web/dist/icons/drowse-192.png +0 -0
  393. drowse/web/dist/icons/drowse-512.png +0 -0
  394. drowse/web/dist/icons/drowse-maskable-512.png +0 -0
  395. drowse/web/dist/icons/tab-analyzing-0.png +0 -0
  396. drowse/web/dist/icons/tab-analyzing-1.png +0 -0
  397. drowse/web/dist/icons/tab-analyzing-2.png +0 -0
  398. drowse/web/dist/icons/tab-analyzing-dark-0.png +0 -0
  399. drowse/web/dist/icons/tab-analyzing-dark-1.png +0 -0
  400. drowse/web/dist/icons/tab-analyzing-dark-10.png +0 -0
  401. drowse/web/dist/icons/tab-analyzing-dark-11.png +0 -0
  402. drowse/web/dist/icons/tab-analyzing-dark-12.png +0 -0
  403. drowse/web/dist/icons/tab-analyzing-dark-13.png +0 -0
  404. drowse/web/dist/icons/tab-analyzing-dark-14.png +0 -0
  405. drowse/web/dist/icons/tab-analyzing-dark-15.png +0 -0
  406. drowse/web/dist/icons/tab-analyzing-dark-16.png +0 -0
  407. drowse/web/dist/icons/tab-analyzing-dark-17.png +0 -0
  408. drowse/web/dist/icons/tab-analyzing-dark-18.png +0 -0
  409. drowse/web/dist/icons/tab-analyzing-dark-19.png +0 -0
  410. drowse/web/dist/icons/tab-analyzing-dark-2.png +0 -0
  411. drowse/web/dist/icons/tab-analyzing-dark-20.png +0 -0
  412. drowse/web/dist/icons/tab-analyzing-dark-21.png +0 -0
  413. drowse/web/dist/icons/tab-analyzing-dark-22.png +0 -0
  414. drowse/web/dist/icons/tab-analyzing-dark-23.png +0 -0
  415. drowse/web/dist/icons/tab-analyzing-dark-24.png +0 -0
  416. drowse/web/dist/icons/tab-analyzing-dark-25.png +0 -0
  417. drowse/web/dist/icons/tab-analyzing-dark-26.png +0 -0
  418. drowse/web/dist/icons/tab-analyzing-dark-27.png +0 -0
  419. drowse/web/dist/icons/tab-analyzing-dark-28.png +0 -0
  420. drowse/web/dist/icons/tab-analyzing-dark-29.png +0 -0
  421. drowse/web/dist/icons/tab-analyzing-dark-3.png +0 -0
  422. drowse/web/dist/icons/tab-analyzing-dark-30.png +0 -0
  423. drowse/web/dist/icons/tab-analyzing-dark-31.png +0 -0
  424. drowse/web/dist/icons/tab-analyzing-dark-32.png +0 -0
  425. drowse/web/dist/icons/tab-analyzing-dark-33.png +0 -0
  426. drowse/web/dist/icons/tab-analyzing-dark-34.png +0 -0
  427. drowse/web/dist/icons/tab-analyzing-dark-35.png +0 -0
  428. drowse/web/dist/icons/tab-analyzing-dark-36.png +0 -0
  429. drowse/web/dist/icons/tab-analyzing-dark-37.png +0 -0
  430. drowse/web/dist/icons/tab-analyzing-dark-38.png +0 -0
  431. drowse/web/dist/icons/tab-analyzing-dark-39.png +0 -0
  432. drowse/web/dist/icons/tab-analyzing-dark-4.png +0 -0
  433. drowse/web/dist/icons/tab-analyzing-dark-40.png +0 -0
  434. drowse/web/dist/icons/tab-analyzing-dark-41.png +0 -0
  435. drowse/web/dist/icons/tab-analyzing-dark-42.png +0 -0
  436. drowse/web/dist/icons/tab-analyzing-dark-43.png +0 -0
  437. drowse/web/dist/icons/tab-analyzing-dark-44.png +0 -0
  438. drowse/web/dist/icons/tab-analyzing-dark-45.png +0 -0
  439. drowse/web/dist/icons/tab-analyzing-dark-46.png +0 -0
  440. drowse/web/dist/icons/tab-analyzing-dark-47.png +0 -0
  441. drowse/web/dist/icons/tab-analyzing-dark-5.png +0 -0
  442. drowse/web/dist/icons/tab-analyzing-dark-6.png +0 -0
  443. drowse/web/dist/icons/tab-analyzing-dark-7.png +0 -0
  444. drowse/web/dist/icons/tab-analyzing-dark-8.png +0 -0
  445. drowse/web/dist/icons/tab-analyzing-dark-9.png +0 -0
  446. drowse/web/dist/icons/tab-analyzing-light-0.png +0 -0
  447. drowse/web/dist/icons/tab-analyzing-light-1.png +0 -0
  448. drowse/web/dist/icons/tab-analyzing-light-10.png +0 -0
  449. drowse/web/dist/icons/tab-analyzing-light-11.png +0 -0
  450. drowse/web/dist/icons/tab-analyzing-light-12.png +0 -0
  451. drowse/web/dist/icons/tab-analyzing-light-13.png +0 -0
  452. drowse/web/dist/icons/tab-analyzing-light-14.png +0 -0
  453. drowse/web/dist/icons/tab-analyzing-light-15.png +0 -0
  454. drowse/web/dist/icons/tab-analyzing-light-16.png +0 -0
  455. drowse/web/dist/icons/tab-analyzing-light-17.png +0 -0
  456. drowse/web/dist/icons/tab-analyzing-light-18.png +0 -0
  457. drowse/web/dist/icons/tab-analyzing-light-19.png +0 -0
  458. drowse/web/dist/icons/tab-analyzing-light-2.png +0 -0
  459. drowse/web/dist/icons/tab-analyzing-light-20.png +0 -0
  460. drowse/web/dist/icons/tab-analyzing-light-21.png +0 -0
  461. drowse/web/dist/icons/tab-analyzing-light-22.png +0 -0
  462. drowse/web/dist/icons/tab-analyzing-light-23.png +0 -0
  463. drowse/web/dist/icons/tab-analyzing-light-24.png +0 -0
  464. drowse/web/dist/icons/tab-analyzing-light-25.png +0 -0
  465. drowse/web/dist/icons/tab-analyzing-light-26.png +0 -0
  466. drowse/web/dist/icons/tab-analyzing-light-27.png +0 -0
  467. drowse/web/dist/icons/tab-analyzing-light-28.png +0 -0
  468. drowse/web/dist/icons/tab-analyzing-light-29.png +0 -0
  469. drowse/web/dist/icons/tab-analyzing-light-3.png +0 -0
  470. drowse/web/dist/icons/tab-analyzing-light-30.png +0 -0
  471. drowse/web/dist/icons/tab-analyzing-light-31.png +0 -0
  472. drowse/web/dist/icons/tab-analyzing-light-32.png +0 -0
  473. drowse/web/dist/icons/tab-analyzing-light-33.png +0 -0
  474. drowse/web/dist/icons/tab-analyzing-light-34.png +0 -0
  475. drowse/web/dist/icons/tab-analyzing-light-35.png +0 -0
  476. drowse/web/dist/icons/tab-analyzing-light-36.png +0 -0
  477. drowse/web/dist/icons/tab-analyzing-light-37.png +0 -0
  478. drowse/web/dist/icons/tab-analyzing-light-38.png +0 -0
  479. drowse/web/dist/icons/tab-analyzing-light-39.png +0 -0
  480. drowse/web/dist/icons/tab-analyzing-light-4.png +0 -0
  481. drowse/web/dist/icons/tab-analyzing-light-40.png +0 -0
  482. drowse/web/dist/icons/tab-analyzing-light-41.png +0 -0
  483. drowse/web/dist/icons/tab-analyzing-light-42.png +0 -0
  484. drowse/web/dist/icons/tab-analyzing-light-43.png +0 -0
  485. drowse/web/dist/icons/tab-analyzing-light-44.png +0 -0
  486. drowse/web/dist/icons/tab-analyzing-light-45.png +0 -0
  487. drowse/web/dist/icons/tab-analyzing-light-46.png +0 -0
  488. drowse/web/dist/icons/tab-analyzing-light-47.png +0 -0
  489. drowse/web/dist/icons/tab-analyzing-light-5.png +0 -0
  490. drowse/web/dist/icons/tab-analyzing-light-6.png +0 -0
  491. drowse/web/dist/icons/tab-analyzing-light-7.png +0 -0
  492. drowse/web/dist/icons/tab-analyzing-light-8.png +0 -0
  493. drowse/web/dist/icons/tab-analyzing-light-9.png +0 -0
  494. drowse/web/dist/icons/tab-chat-settings-dark.png +0 -0
  495. drowse/web/dist/icons/tab-chat-settings-light.png +0 -0
  496. drowse/web/dist/icons/tab-chat-settings.png +0 -0
  497. drowse/web/dist/icons/tab-chats-dark.png +0 -0
  498. drowse/web/dist/icons/tab-chats-light.png +0 -0
  499. drowse/web/dist/icons/tab-chats.png +0 -0
  500. drowse/web/dist/icons/tab-checking-0.png +0 -0
  501. drowse/web/dist/icons/tab-checking-1.png +0 -0
  502. drowse/web/dist/icons/tab-checking-2.png +0 -0
  503. drowse/web/dist/icons/tab-checking-dark-0.png +0 -0
  504. drowse/web/dist/icons/tab-checking-dark-1.png +0 -0
  505. drowse/web/dist/icons/tab-checking-dark-10.png +0 -0
  506. drowse/web/dist/icons/tab-checking-dark-11.png +0 -0
  507. drowse/web/dist/icons/tab-checking-dark-12.png +0 -0
  508. drowse/web/dist/icons/tab-checking-dark-13.png +0 -0
  509. drowse/web/dist/icons/tab-checking-dark-14.png +0 -0
  510. drowse/web/dist/icons/tab-checking-dark-15.png +0 -0
  511. drowse/web/dist/icons/tab-checking-dark-16.png +0 -0
  512. drowse/web/dist/icons/tab-checking-dark-17.png +0 -0
  513. drowse/web/dist/icons/tab-checking-dark-18.png +0 -0
  514. drowse/web/dist/icons/tab-checking-dark-19.png +0 -0
  515. drowse/web/dist/icons/tab-checking-dark-2.png +0 -0
  516. drowse/web/dist/icons/tab-checking-dark-20.png +0 -0
  517. drowse/web/dist/icons/tab-checking-dark-21.png +0 -0
  518. drowse/web/dist/icons/tab-checking-dark-22.png +0 -0
  519. drowse/web/dist/icons/tab-checking-dark-23.png +0 -0
  520. drowse/web/dist/icons/tab-checking-dark-24.png +0 -0
  521. drowse/web/dist/icons/tab-checking-dark-25.png +0 -0
  522. drowse/web/dist/icons/tab-checking-dark-26.png +0 -0
  523. drowse/web/dist/icons/tab-checking-dark-27.png +0 -0
  524. drowse/web/dist/icons/tab-checking-dark-28.png +0 -0
  525. drowse/web/dist/icons/tab-checking-dark-29.png +0 -0
  526. drowse/web/dist/icons/tab-checking-dark-3.png +0 -0
  527. drowse/web/dist/icons/tab-checking-dark-30.png +0 -0
  528. drowse/web/dist/icons/tab-checking-dark-31.png +0 -0
  529. drowse/web/dist/icons/tab-checking-dark-32.png +0 -0
  530. drowse/web/dist/icons/tab-checking-dark-33.png +0 -0
  531. drowse/web/dist/icons/tab-checking-dark-34.png +0 -0
  532. drowse/web/dist/icons/tab-checking-dark-35.png +0 -0
  533. drowse/web/dist/icons/tab-checking-dark-36.png +0 -0
  534. drowse/web/dist/icons/tab-checking-dark-37.png +0 -0
  535. drowse/web/dist/icons/tab-checking-dark-38.png +0 -0
  536. drowse/web/dist/icons/tab-checking-dark-39.png +0 -0
  537. drowse/web/dist/icons/tab-checking-dark-4.png +0 -0
  538. drowse/web/dist/icons/tab-checking-dark-40.png +0 -0
  539. drowse/web/dist/icons/tab-checking-dark-41.png +0 -0
  540. drowse/web/dist/icons/tab-checking-dark-42.png +0 -0
  541. drowse/web/dist/icons/tab-checking-dark-43.png +0 -0
  542. drowse/web/dist/icons/tab-checking-dark-44.png +0 -0
  543. drowse/web/dist/icons/tab-checking-dark-45.png +0 -0
  544. drowse/web/dist/icons/tab-checking-dark-46.png +0 -0
  545. drowse/web/dist/icons/tab-checking-dark-47.png +0 -0
  546. drowse/web/dist/icons/tab-checking-dark-5.png +0 -0
  547. drowse/web/dist/icons/tab-checking-dark-6.png +0 -0
  548. drowse/web/dist/icons/tab-checking-dark-7.png +0 -0
  549. drowse/web/dist/icons/tab-checking-dark-8.png +0 -0
  550. drowse/web/dist/icons/tab-checking-dark-9.png +0 -0
  551. drowse/web/dist/icons/tab-checking-light-0.png +0 -0
  552. drowse/web/dist/icons/tab-checking-light-1.png +0 -0
  553. drowse/web/dist/icons/tab-checking-light-10.png +0 -0
  554. drowse/web/dist/icons/tab-checking-light-11.png +0 -0
  555. drowse/web/dist/icons/tab-checking-light-12.png +0 -0
  556. drowse/web/dist/icons/tab-checking-light-13.png +0 -0
  557. drowse/web/dist/icons/tab-checking-light-14.png +0 -0
  558. drowse/web/dist/icons/tab-checking-light-15.png +0 -0
  559. drowse/web/dist/icons/tab-checking-light-16.png +0 -0
  560. drowse/web/dist/icons/tab-checking-light-17.png +0 -0
  561. drowse/web/dist/icons/tab-checking-light-18.png +0 -0
  562. drowse/web/dist/icons/tab-checking-light-19.png +0 -0
  563. drowse/web/dist/icons/tab-checking-light-2.png +0 -0
  564. drowse/web/dist/icons/tab-checking-light-20.png +0 -0
  565. drowse/web/dist/icons/tab-checking-light-21.png +0 -0
  566. drowse/web/dist/icons/tab-checking-light-22.png +0 -0
  567. drowse/web/dist/icons/tab-checking-light-23.png +0 -0
  568. drowse/web/dist/icons/tab-checking-light-24.png +0 -0
  569. drowse/web/dist/icons/tab-checking-light-25.png +0 -0
  570. drowse/web/dist/icons/tab-checking-light-26.png +0 -0
  571. drowse/web/dist/icons/tab-checking-light-27.png +0 -0
  572. drowse/web/dist/icons/tab-checking-light-28.png +0 -0
  573. drowse/web/dist/icons/tab-checking-light-29.png +0 -0
  574. drowse/web/dist/icons/tab-checking-light-3.png +0 -0
  575. drowse/web/dist/icons/tab-checking-light-30.png +0 -0
  576. drowse/web/dist/icons/tab-checking-light-31.png +0 -0
  577. drowse/web/dist/icons/tab-checking-light-32.png +0 -0
  578. drowse/web/dist/icons/tab-checking-light-33.png +0 -0
  579. drowse/web/dist/icons/tab-checking-light-34.png +0 -0
  580. drowse/web/dist/icons/tab-checking-light-35.png +0 -0
  581. drowse/web/dist/icons/tab-checking-light-36.png +0 -0
  582. drowse/web/dist/icons/tab-checking-light-37.png +0 -0
  583. drowse/web/dist/icons/tab-checking-light-38.png +0 -0
  584. drowse/web/dist/icons/tab-checking-light-39.png +0 -0
  585. drowse/web/dist/icons/tab-checking-light-4.png +0 -0
  586. drowse/web/dist/icons/tab-checking-light-40.png +0 -0
  587. drowse/web/dist/icons/tab-checking-light-41.png +0 -0
  588. drowse/web/dist/icons/tab-checking-light-42.png +0 -0
  589. drowse/web/dist/icons/tab-checking-light-43.png +0 -0
  590. drowse/web/dist/icons/tab-checking-light-44.png +0 -0
  591. drowse/web/dist/icons/tab-checking-light-45.png +0 -0
  592. drowse/web/dist/icons/tab-checking-light-46.png +0 -0
  593. drowse/web/dist/icons/tab-checking-light-47.png +0 -0
  594. drowse/web/dist/icons/tab-checking-light-5.png +0 -0
  595. drowse/web/dist/icons/tab-checking-light-6.png +0 -0
  596. drowse/web/dist/icons/tab-checking-light-7.png +0 -0
  597. drowse/web/dist/icons/tab-checking-light-8.png +0 -0
  598. drowse/web/dist/icons/tab-checking-light-9.png +0 -0
  599. drowse/web/dist/icons/tab-comparison-dark.png +0 -0
  600. drowse/web/dist/icons/tab-comparison-light.png +0 -0
  601. drowse/web/dist/icons/tab-comparison.png +0 -0
  602. drowse/web/dist/icons/tab-controls-dark.png +0 -0
  603. drowse/web/dist/icons/tab-controls-light.png +0 -0
  604. drowse/web/dist/icons/tab-controls.png +0 -0
  605. drowse/web/dist/icons/tab-conversation-dark.png +0 -0
  606. drowse/web/dist/icons/tab-conversation-light.png +0 -0
  607. drowse/web/dist/icons/tab-conversation.png +0 -0
  608. drowse/web/dist/icons/tab-credits-dark.png +0 -0
  609. drowse/web/dist/icons/tab-credits-light.png +0 -0
  610. drowse/web/dist/icons/tab-credits.png +0 -0
  611. drowse/web/dist/icons/tab-downloading-0.png +0 -0
  612. drowse/web/dist/icons/tab-downloading-1.png +0 -0
  613. drowse/web/dist/icons/tab-downloading-2.png +0 -0
  614. drowse/web/dist/icons/tab-downloading-dark-0.png +0 -0
  615. drowse/web/dist/icons/tab-downloading-dark-1.png +0 -0
  616. drowse/web/dist/icons/tab-downloading-dark-10.png +0 -0
  617. drowse/web/dist/icons/tab-downloading-dark-11.png +0 -0
  618. drowse/web/dist/icons/tab-downloading-dark-12.png +0 -0
  619. drowse/web/dist/icons/tab-downloading-dark-13.png +0 -0
  620. drowse/web/dist/icons/tab-downloading-dark-14.png +0 -0
  621. drowse/web/dist/icons/tab-downloading-dark-15.png +0 -0
  622. drowse/web/dist/icons/tab-downloading-dark-16.png +0 -0
  623. drowse/web/dist/icons/tab-downloading-dark-17.png +0 -0
  624. drowse/web/dist/icons/tab-downloading-dark-18.png +0 -0
  625. drowse/web/dist/icons/tab-downloading-dark-19.png +0 -0
  626. drowse/web/dist/icons/tab-downloading-dark-2.png +0 -0
  627. drowse/web/dist/icons/tab-downloading-dark-20.png +0 -0
  628. drowse/web/dist/icons/tab-downloading-dark-21.png +0 -0
  629. drowse/web/dist/icons/tab-downloading-dark-22.png +0 -0
  630. drowse/web/dist/icons/tab-downloading-dark-23.png +0 -0
  631. drowse/web/dist/icons/tab-downloading-dark-24.png +0 -0
  632. drowse/web/dist/icons/tab-downloading-dark-25.png +0 -0
  633. drowse/web/dist/icons/tab-downloading-dark-26.png +0 -0
  634. drowse/web/dist/icons/tab-downloading-dark-27.png +0 -0
  635. drowse/web/dist/icons/tab-downloading-dark-28.png +0 -0
  636. drowse/web/dist/icons/tab-downloading-dark-29.png +0 -0
  637. drowse/web/dist/icons/tab-downloading-dark-3.png +0 -0
  638. drowse/web/dist/icons/tab-downloading-dark-30.png +0 -0
  639. drowse/web/dist/icons/tab-downloading-dark-31.png +0 -0
  640. drowse/web/dist/icons/tab-downloading-dark-32.png +0 -0
  641. drowse/web/dist/icons/tab-downloading-dark-33.png +0 -0
  642. drowse/web/dist/icons/tab-downloading-dark-34.png +0 -0
  643. drowse/web/dist/icons/tab-downloading-dark-35.png +0 -0
  644. drowse/web/dist/icons/tab-downloading-dark-36.png +0 -0
  645. drowse/web/dist/icons/tab-downloading-dark-37.png +0 -0
  646. drowse/web/dist/icons/tab-downloading-dark-38.png +0 -0
  647. drowse/web/dist/icons/tab-downloading-dark-39.png +0 -0
  648. drowse/web/dist/icons/tab-downloading-dark-4.png +0 -0
  649. drowse/web/dist/icons/tab-downloading-dark-40.png +0 -0
  650. drowse/web/dist/icons/tab-downloading-dark-41.png +0 -0
  651. drowse/web/dist/icons/tab-downloading-dark-42.png +0 -0
  652. drowse/web/dist/icons/tab-downloading-dark-43.png +0 -0
  653. drowse/web/dist/icons/tab-downloading-dark-44.png +0 -0
  654. drowse/web/dist/icons/tab-downloading-dark-45.png +0 -0
  655. drowse/web/dist/icons/tab-downloading-dark-46.png +0 -0
  656. drowse/web/dist/icons/tab-downloading-dark-47.png +0 -0
  657. drowse/web/dist/icons/tab-downloading-dark-5.png +0 -0
  658. drowse/web/dist/icons/tab-downloading-dark-6.png +0 -0
  659. drowse/web/dist/icons/tab-downloading-dark-7.png +0 -0
  660. drowse/web/dist/icons/tab-downloading-dark-8.png +0 -0
  661. drowse/web/dist/icons/tab-downloading-dark-9.png +0 -0
  662. drowse/web/dist/icons/tab-downloading-light-0.png +0 -0
  663. drowse/web/dist/icons/tab-downloading-light-1.png +0 -0
  664. drowse/web/dist/icons/tab-downloading-light-10.png +0 -0
  665. drowse/web/dist/icons/tab-downloading-light-11.png +0 -0
  666. drowse/web/dist/icons/tab-downloading-light-12.png +0 -0
  667. drowse/web/dist/icons/tab-downloading-light-13.png +0 -0
  668. drowse/web/dist/icons/tab-downloading-light-14.png +0 -0
  669. drowse/web/dist/icons/tab-downloading-light-15.png +0 -0
  670. drowse/web/dist/icons/tab-downloading-light-16.png +0 -0
  671. drowse/web/dist/icons/tab-downloading-light-17.png +0 -0
  672. drowse/web/dist/icons/tab-downloading-light-18.png +0 -0
  673. drowse/web/dist/icons/tab-downloading-light-19.png +0 -0
  674. drowse/web/dist/icons/tab-downloading-light-2.png +0 -0
  675. drowse/web/dist/icons/tab-downloading-light-20.png +0 -0
  676. drowse/web/dist/icons/tab-downloading-light-21.png +0 -0
  677. drowse/web/dist/icons/tab-downloading-light-22.png +0 -0
  678. drowse/web/dist/icons/tab-downloading-light-23.png +0 -0
  679. drowse/web/dist/icons/tab-downloading-light-24.png +0 -0
  680. drowse/web/dist/icons/tab-downloading-light-25.png +0 -0
  681. drowse/web/dist/icons/tab-downloading-light-26.png +0 -0
  682. drowse/web/dist/icons/tab-downloading-light-27.png +0 -0
  683. drowse/web/dist/icons/tab-downloading-light-28.png +0 -0
  684. drowse/web/dist/icons/tab-downloading-light-29.png +0 -0
  685. drowse/web/dist/icons/tab-downloading-light-3.png +0 -0
  686. drowse/web/dist/icons/tab-downloading-light-30.png +0 -0
  687. drowse/web/dist/icons/tab-downloading-light-31.png +0 -0
  688. drowse/web/dist/icons/tab-downloading-light-32.png +0 -0
  689. drowse/web/dist/icons/tab-downloading-light-33.png +0 -0
  690. drowse/web/dist/icons/tab-downloading-light-34.png +0 -0
  691. drowse/web/dist/icons/tab-downloading-light-35.png +0 -0
  692. drowse/web/dist/icons/tab-downloading-light-36.png +0 -0
  693. drowse/web/dist/icons/tab-downloading-light-37.png +0 -0
  694. drowse/web/dist/icons/tab-downloading-light-38.png +0 -0
  695. drowse/web/dist/icons/tab-downloading-light-39.png +0 -0
  696. drowse/web/dist/icons/tab-downloading-light-4.png +0 -0
  697. drowse/web/dist/icons/tab-downloading-light-40.png +0 -0
  698. drowse/web/dist/icons/tab-downloading-light-41.png +0 -0
  699. drowse/web/dist/icons/tab-downloading-light-42.png +0 -0
  700. drowse/web/dist/icons/tab-downloading-light-43.png +0 -0
  701. drowse/web/dist/icons/tab-downloading-light-44.png +0 -0
  702. drowse/web/dist/icons/tab-downloading-light-45.png +0 -0
  703. drowse/web/dist/icons/tab-downloading-light-46.png +0 -0
  704. drowse/web/dist/icons/tab-downloading-light-47.png +0 -0
  705. drowse/web/dist/icons/tab-downloading-light-5.png +0 -0
  706. drowse/web/dist/icons/tab-downloading-light-6.png +0 -0
  707. drowse/web/dist/icons/tab-downloading-light-7.png +0 -0
  708. drowse/web/dist/icons/tab-downloading-light-8.png +0 -0
  709. drowse/web/dist/icons/tab-downloading-light-9.png +0 -0
  710. drowse/web/dist/icons/tab-error-dark.png +0 -0
  711. drowse/web/dist/icons/tab-error-light.png +0 -0
  712. drowse/web/dist/icons/tab-error.png +0 -0
  713. drowse/web/dist/icons/tab-home-dark.png +0 -0
  714. drowse/web/dist/icons/tab-home-light.png +0 -0
  715. drowse/web/dist/icons/tab-home.png +0 -0
  716. drowse/web/dist/icons/tab-loading-0.png +0 -0
  717. drowse/web/dist/icons/tab-loading-1.png +0 -0
  718. drowse/web/dist/icons/tab-loading-2.png +0 -0
  719. drowse/web/dist/icons/tab-loading-dark-0.png +0 -0
  720. drowse/web/dist/icons/tab-loading-dark-1.png +0 -0
  721. drowse/web/dist/icons/tab-loading-dark-10.png +0 -0
  722. drowse/web/dist/icons/tab-loading-dark-11.png +0 -0
  723. drowse/web/dist/icons/tab-loading-dark-12.png +0 -0
  724. drowse/web/dist/icons/tab-loading-dark-13.png +0 -0
  725. drowse/web/dist/icons/tab-loading-dark-14.png +0 -0
  726. drowse/web/dist/icons/tab-loading-dark-15.png +0 -0
  727. drowse/web/dist/icons/tab-loading-dark-16.png +0 -0
  728. drowse/web/dist/icons/tab-loading-dark-17.png +0 -0
  729. drowse/web/dist/icons/tab-loading-dark-18.png +0 -0
  730. drowse/web/dist/icons/tab-loading-dark-19.png +0 -0
  731. drowse/web/dist/icons/tab-loading-dark-2.png +0 -0
  732. drowse/web/dist/icons/tab-loading-dark-20.png +0 -0
  733. drowse/web/dist/icons/tab-loading-dark-21.png +0 -0
  734. drowse/web/dist/icons/tab-loading-dark-22.png +0 -0
  735. drowse/web/dist/icons/tab-loading-dark-23.png +0 -0
  736. drowse/web/dist/icons/tab-loading-dark-24.png +0 -0
  737. drowse/web/dist/icons/tab-loading-dark-25.png +0 -0
  738. drowse/web/dist/icons/tab-loading-dark-26.png +0 -0
  739. drowse/web/dist/icons/tab-loading-dark-27.png +0 -0
  740. drowse/web/dist/icons/tab-loading-dark-28.png +0 -0
  741. drowse/web/dist/icons/tab-loading-dark-29.png +0 -0
  742. drowse/web/dist/icons/tab-loading-dark-3.png +0 -0
  743. drowse/web/dist/icons/tab-loading-dark-30.png +0 -0
  744. drowse/web/dist/icons/tab-loading-dark-31.png +0 -0
  745. drowse/web/dist/icons/tab-loading-dark-32.png +0 -0
  746. drowse/web/dist/icons/tab-loading-dark-33.png +0 -0
  747. drowse/web/dist/icons/tab-loading-dark-34.png +0 -0
  748. drowse/web/dist/icons/tab-loading-dark-35.png +0 -0
  749. drowse/web/dist/icons/tab-loading-dark-36.png +0 -0
  750. drowse/web/dist/icons/tab-loading-dark-37.png +0 -0
  751. drowse/web/dist/icons/tab-loading-dark-38.png +0 -0
  752. drowse/web/dist/icons/tab-loading-dark-39.png +0 -0
  753. drowse/web/dist/icons/tab-loading-dark-4.png +0 -0
  754. drowse/web/dist/icons/tab-loading-dark-40.png +0 -0
  755. drowse/web/dist/icons/tab-loading-dark-41.png +0 -0
  756. drowse/web/dist/icons/tab-loading-dark-42.png +0 -0
  757. drowse/web/dist/icons/tab-loading-dark-43.png +0 -0
  758. drowse/web/dist/icons/tab-loading-dark-44.png +0 -0
  759. drowse/web/dist/icons/tab-loading-dark-45.png +0 -0
  760. drowse/web/dist/icons/tab-loading-dark-46.png +0 -0
  761. drowse/web/dist/icons/tab-loading-dark-47.png +0 -0
  762. drowse/web/dist/icons/tab-loading-dark-5.png +0 -0
  763. drowse/web/dist/icons/tab-loading-dark-6.png +0 -0
  764. drowse/web/dist/icons/tab-loading-dark-7.png +0 -0
  765. drowse/web/dist/icons/tab-loading-dark-8.png +0 -0
  766. drowse/web/dist/icons/tab-loading-dark-9.png +0 -0
  767. drowse/web/dist/icons/tab-loading-light-0.png +0 -0
  768. drowse/web/dist/icons/tab-loading-light-1.png +0 -0
  769. drowse/web/dist/icons/tab-loading-light-10.png +0 -0
  770. drowse/web/dist/icons/tab-loading-light-11.png +0 -0
  771. drowse/web/dist/icons/tab-loading-light-12.png +0 -0
  772. drowse/web/dist/icons/tab-loading-light-13.png +0 -0
  773. drowse/web/dist/icons/tab-loading-light-14.png +0 -0
  774. drowse/web/dist/icons/tab-loading-light-15.png +0 -0
  775. drowse/web/dist/icons/tab-loading-light-16.png +0 -0
  776. drowse/web/dist/icons/tab-loading-light-17.png +0 -0
  777. drowse/web/dist/icons/tab-loading-light-18.png +0 -0
  778. drowse/web/dist/icons/tab-loading-light-19.png +0 -0
  779. drowse/web/dist/icons/tab-loading-light-2.png +0 -0
  780. drowse/web/dist/icons/tab-loading-light-20.png +0 -0
  781. drowse/web/dist/icons/tab-loading-light-21.png +0 -0
  782. drowse/web/dist/icons/tab-loading-light-22.png +0 -0
  783. drowse/web/dist/icons/tab-loading-light-23.png +0 -0
  784. drowse/web/dist/icons/tab-loading-light-24.png +0 -0
  785. drowse/web/dist/icons/tab-loading-light-25.png +0 -0
  786. drowse/web/dist/icons/tab-loading-light-26.png +0 -0
  787. drowse/web/dist/icons/tab-loading-light-27.png +0 -0
  788. drowse/web/dist/icons/tab-loading-light-28.png +0 -0
  789. drowse/web/dist/icons/tab-loading-light-29.png +0 -0
  790. drowse/web/dist/icons/tab-loading-light-3.png +0 -0
  791. drowse/web/dist/icons/tab-loading-light-30.png +0 -0
  792. drowse/web/dist/icons/tab-loading-light-31.png +0 -0
  793. drowse/web/dist/icons/tab-loading-light-32.png +0 -0
  794. drowse/web/dist/icons/tab-loading-light-33.png +0 -0
  795. drowse/web/dist/icons/tab-loading-light-34.png +0 -0
  796. drowse/web/dist/icons/tab-loading-light-35.png +0 -0
  797. drowse/web/dist/icons/tab-loading-light-36.png +0 -0
  798. drowse/web/dist/icons/tab-loading-light-37.png +0 -0
  799. drowse/web/dist/icons/tab-loading-light-38.png +0 -0
  800. drowse/web/dist/icons/tab-loading-light-39.png +0 -0
  801. drowse/web/dist/icons/tab-loading-light-4.png +0 -0
  802. drowse/web/dist/icons/tab-loading-light-40.png +0 -0
  803. drowse/web/dist/icons/tab-loading-light-41.png +0 -0
  804. drowse/web/dist/icons/tab-loading-light-42.png +0 -0
  805. drowse/web/dist/icons/tab-loading-light-43.png +0 -0
  806. drowse/web/dist/icons/tab-loading-light-44.png +0 -0
  807. drowse/web/dist/icons/tab-loading-light-45.png +0 -0
  808. drowse/web/dist/icons/tab-loading-light-46.png +0 -0
  809. drowse/web/dist/icons/tab-loading-light-47.png +0 -0
  810. drowse/web/dist/icons/tab-loading-light-5.png +0 -0
  811. drowse/web/dist/icons/tab-loading-light-6.png +0 -0
  812. drowse/web/dist/icons/tab-loading-light-7.png +0 -0
  813. drowse/web/dist/icons/tab-loading-light-8.png +0 -0
  814. drowse/web/dist/icons/tab-loading-light-9.png +0 -0
  815. drowse/web/dist/icons/tab-loom-dark.png +0 -0
  816. drowse/web/dist/icons/tab-loom-light.png +0 -0
  817. drowse/web/dist/icons/tab-loom-working-0.png +0 -0
  818. drowse/web/dist/icons/tab-loom-working-1.png +0 -0
  819. drowse/web/dist/icons/tab-loom-working-2.png +0 -0
  820. drowse/web/dist/icons/tab-loom-working-dark-0.png +0 -0
  821. drowse/web/dist/icons/tab-loom-working-dark-1.png +0 -0
  822. drowse/web/dist/icons/tab-loom-working-dark-10.png +0 -0
  823. drowse/web/dist/icons/tab-loom-working-dark-11.png +0 -0
  824. drowse/web/dist/icons/tab-loom-working-dark-12.png +0 -0
  825. drowse/web/dist/icons/tab-loom-working-dark-13.png +0 -0
  826. drowse/web/dist/icons/tab-loom-working-dark-14.png +0 -0
  827. drowse/web/dist/icons/tab-loom-working-dark-15.png +0 -0
  828. drowse/web/dist/icons/tab-loom-working-dark-16.png +0 -0
  829. drowse/web/dist/icons/tab-loom-working-dark-17.png +0 -0
  830. drowse/web/dist/icons/tab-loom-working-dark-18.png +0 -0
  831. drowse/web/dist/icons/tab-loom-working-dark-19.png +0 -0
  832. drowse/web/dist/icons/tab-loom-working-dark-2.png +0 -0
  833. drowse/web/dist/icons/tab-loom-working-dark-20.png +0 -0
  834. drowse/web/dist/icons/tab-loom-working-dark-21.png +0 -0
  835. drowse/web/dist/icons/tab-loom-working-dark-22.png +0 -0
  836. drowse/web/dist/icons/tab-loom-working-dark-23.png +0 -0
  837. drowse/web/dist/icons/tab-loom-working-dark-24.png +0 -0
  838. drowse/web/dist/icons/tab-loom-working-dark-25.png +0 -0
  839. drowse/web/dist/icons/tab-loom-working-dark-26.png +0 -0
  840. drowse/web/dist/icons/tab-loom-working-dark-27.png +0 -0
  841. drowse/web/dist/icons/tab-loom-working-dark-28.png +0 -0
  842. drowse/web/dist/icons/tab-loom-working-dark-29.png +0 -0
  843. drowse/web/dist/icons/tab-loom-working-dark-3.png +0 -0
  844. drowse/web/dist/icons/tab-loom-working-dark-30.png +0 -0
  845. drowse/web/dist/icons/tab-loom-working-dark-31.png +0 -0
  846. drowse/web/dist/icons/tab-loom-working-dark-32.png +0 -0
  847. drowse/web/dist/icons/tab-loom-working-dark-33.png +0 -0
  848. drowse/web/dist/icons/tab-loom-working-dark-34.png +0 -0
  849. drowse/web/dist/icons/tab-loom-working-dark-35.png +0 -0
  850. drowse/web/dist/icons/tab-loom-working-dark-36.png +0 -0
  851. drowse/web/dist/icons/tab-loom-working-dark-37.png +0 -0
  852. drowse/web/dist/icons/tab-loom-working-dark-38.png +0 -0
  853. drowse/web/dist/icons/tab-loom-working-dark-39.png +0 -0
  854. drowse/web/dist/icons/tab-loom-working-dark-4.png +0 -0
  855. drowse/web/dist/icons/tab-loom-working-dark-40.png +0 -0
  856. drowse/web/dist/icons/tab-loom-working-dark-41.png +0 -0
  857. drowse/web/dist/icons/tab-loom-working-dark-42.png +0 -0
  858. drowse/web/dist/icons/tab-loom-working-dark-43.png +0 -0
  859. drowse/web/dist/icons/tab-loom-working-dark-44.png +0 -0
  860. drowse/web/dist/icons/tab-loom-working-dark-45.png +0 -0
  861. drowse/web/dist/icons/tab-loom-working-dark-46.png +0 -0
  862. drowse/web/dist/icons/tab-loom-working-dark-47.png +0 -0
  863. drowse/web/dist/icons/tab-loom-working-dark-5.png +0 -0
  864. drowse/web/dist/icons/tab-loom-working-dark-6.png +0 -0
  865. drowse/web/dist/icons/tab-loom-working-dark-7.png +0 -0
  866. drowse/web/dist/icons/tab-loom-working-dark-8.png +0 -0
  867. drowse/web/dist/icons/tab-loom-working-dark-9.png +0 -0
  868. drowse/web/dist/icons/tab-loom-working-light-0.png +0 -0
  869. drowse/web/dist/icons/tab-loom-working-light-1.png +0 -0
  870. drowse/web/dist/icons/tab-loom-working-light-10.png +0 -0
  871. drowse/web/dist/icons/tab-loom-working-light-11.png +0 -0
  872. drowse/web/dist/icons/tab-loom-working-light-12.png +0 -0
  873. drowse/web/dist/icons/tab-loom-working-light-13.png +0 -0
  874. drowse/web/dist/icons/tab-loom-working-light-14.png +0 -0
  875. drowse/web/dist/icons/tab-loom-working-light-15.png +0 -0
  876. drowse/web/dist/icons/tab-loom-working-light-16.png +0 -0
  877. drowse/web/dist/icons/tab-loom-working-light-17.png +0 -0
  878. drowse/web/dist/icons/tab-loom-working-light-18.png +0 -0
  879. drowse/web/dist/icons/tab-loom-working-light-19.png +0 -0
  880. drowse/web/dist/icons/tab-loom-working-light-2.png +0 -0
  881. drowse/web/dist/icons/tab-loom-working-light-20.png +0 -0
  882. drowse/web/dist/icons/tab-loom-working-light-21.png +0 -0
  883. drowse/web/dist/icons/tab-loom-working-light-22.png +0 -0
  884. drowse/web/dist/icons/tab-loom-working-light-23.png +0 -0
  885. drowse/web/dist/icons/tab-loom-working-light-24.png +0 -0
  886. drowse/web/dist/icons/tab-loom-working-light-25.png +0 -0
  887. drowse/web/dist/icons/tab-loom-working-light-26.png +0 -0
  888. drowse/web/dist/icons/tab-loom-working-light-27.png +0 -0
  889. drowse/web/dist/icons/tab-loom-working-light-28.png +0 -0
  890. drowse/web/dist/icons/tab-loom-working-light-29.png +0 -0
  891. drowse/web/dist/icons/tab-loom-working-light-3.png +0 -0
  892. drowse/web/dist/icons/tab-loom-working-light-30.png +0 -0
  893. drowse/web/dist/icons/tab-loom-working-light-31.png +0 -0
  894. drowse/web/dist/icons/tab-loom-working-light-32.png +0 -0
  895. drowse/web/dist/icons/tab-loom-working-light-33.png +0 -0
  896. drowse/web/dist/icons/tab-loom-working-light-34.png +0 -0
  897. drowse/web/dist/icons/tab-loom-working-light-35.png +0 -0
  898. drowse/web/dist/icons/tab-loom-working-light-36.png +0 -0
  899. drowse/web/dist/icons/tab-loom-working-light-37.png +0 -0
  900. drowse/web/dist/icons/tab-loom-working-light-38.png +0 -0
  901. drowse/web/dist/icons/tab-loom-working-light-39.png +0 -0
  902. drowse/web/dist/icons/tab-loom-working-light-4.png +0 -0
  903. drowse/web/dist/icons/tab-loom-working-light-40.png +0 -0
  904. drowse/web/dist/icons/tab-loom-working-light-41.png +0 -0
  905. drowse/web/dist/icons/tab-loom-working-light-42.png +0 -0
  906. drowse/web/dist/icons/tab-loom-working-light-43.png +0 -0
  907. drowse/web/dist/icons/tab-loom-working-light-44.png +0 -0
  908. drowse/web/dist/icons/tab-loom-working-light-45.png +0 -0
  909. drowse/web/dist/icons/tab-loom-working-light-46.png +0 -0
  910. drowse/web/dist/icons/tab-loom-working-light-47.png +0 -0
  911. drowse/web/dist/icons/tab-loom-working-light-5.png +0 -0
  912. drowse/web/dist/icons/tab-loom-working-light-6.png +0 -0
  913. drowse/web/dist/icons/tab-loom-working-light-7.png +0 -0
  914. drowse/web/dist/icons/tab-loom-working-light-8.png +0 -0
  915. drowse/web/dist/icons/tab-loom-working-light-9.png +0 -0
  916. drowse/web/dist/icons/tab-loom.png +0 -0
  917. drowse/web/dist/icons/tab-models-dark.png +0 -0
  918. drowse/web/dist/icons/tab-models-light.png +0 -0
  919. drowse/web/dist/icons/tab-models.png +0 -0
  920. drowse/web/dist/icons/tab-offline-dark.png +0 -0
  921. drowse/web/dist/icons/tab-offline-light.png +0 -0
  922. drowse/web/dist/icons/tab-offline.png +0 -0
  923. drowse/web/dist/icons/tab-paused-dark.png +0 -0
  924. drowse/web/dist/icons/tab-paused-light.png +0 -0
  925. drowse/web/dist/icons/tab-paused.png +0 -0
  926. drowse/web/dist/icons/tab-ready-dark.png +0 -0
  927. drowse/web/dist/icons/tab-ready-light.png +0 -0
  928. drowse/web/dist/icons/tab-ready.png +0 -0
  929. drowse/web/dist/icons/tab-saving-0.png +0 -0
  930. drowse/web/dist/icons/tab-saving-1.png +0 -0
  931. drowse/web/dist/icons/tab-saving-2.png +0 -0
  932. drowse/web/dist/icons/tab-saving-dark-0.png +0 -0
  933. drowse/web/dist/icons/tab-saving-dark-1.png +0 -0
  934. drowse/web/dist/icons/tab-saving-dark-10.png +0 -0
  935. drowse/web/dist/icons/tab-saving-dark-11.png +0 -0
  936. drowse/web/dist/icons/tab-saving-dark-12.png +0 -0
  937. drowse/web/dist/icons/tab-saving-dark-13.png +0 -0
  938. drowse/web/dist/icons/tab-saving-dark-14.png +0 -0
  939. drowse/web/dist/icons/tab-saving-dark-15.png +0 -0
  940. drowse/web/dist/icons/tab-saving-dark-16.png +0 -0
  941. drowse/web/dist/icons/tab-saving-dark-17.png +0 -0
  942. drowse/web/dist/icons/tab-saving-dark-18.png +0 -0
  943. drowse/web/dist/icons/tab-saving-dark-19.png +0 -0
  944. drowse/web/dist/icons/tab-saving-dark-2.png +0 -0
  945. drowse/web/dist/icons/tab-saving-dark-20.png +0 -0
  946. drowse/web/dist/icons/tab-saving-dark-21.png +0 -0
  947. drowse/web/dist/icons/tab-saving-dark-22.png +0 -0
  948. drowse/web/dist/icons/tab-saving-dark-23.png +0 -0
  949. drowse/web/dist/icons/tab-saving-dark-24.png +0 -0
  950. drowse/web/dist/icons/tab-saving-dark-25.png +0 -0
  951. drowse/web/dist/icons/tab-saving-dark-26.png +0 -0
  952. drowse/web/dist/icons/tab-saving-dark-27.png +0 -0
  953. drowse/web/dist/icons/tab-saving-dark-28.png +0 -0
  954. drowse/web/dist/icons/tab-saving-dark-29.png +0 -0
  955. drowse/web/dist/icons/tab-saving-dark-3.png +0 -0
  956. drowse/web/dist/icons/tab-saving-dark-30.png +0 -0
  957. drowse/web/dist/icons/tab-saving-dark-31.png +0 -0
  958. drowse/web/dist/icons/tab-saving-dark-32.png +0 -0
  959. drowse/web/dist/icons/tab-saving-dark-33.png +0 -0
  960. drowse/web/dist/icons/tab-saving-dark-34.png +0 -0
  961. drowse/web/dist/icons/tab-saving-dark-35.png +0 -0
  962. drowse/web/dist/icons/tab-saving-dark-36.png +0 -0
  963. drowse/web/dist/icons/tab-saving-dark-37.png +0 -0
  964. drowse/web/dist/icons/tab-saving-dark-38.png +0 -0
  965. drowse/web/dist/icons/tab-saving-dark-39.png +0 -0
  966. drowse/web/dist/icons/tab-saving-dark-4.png +0 -0
  967. drowse/web/dist/icons/tab-saving-dark-40.png +0 -0
  968. drowse/web/dist/icons/tab-saving-dark-41.png +0 -0
  969. drowse/web/dist/icons/tab-saving-dark-42.png +0 -0
  970. drowse/web/dist/icons/tab-saving-dark-43.png +0 -0
  971. drowse/web/dist/icons/tab-saving-dark-44.png +0 -0
  972. drowse/web/dist/icons/tab-saving-dark-45.png +0 -0
  973. drowse/web/dist/icons/tab-saving-dark-46.png +0 -0
  974. drowse/web/dist/icons/tab-saving-dark-47.png +0 -0
  975. drowse/web/dist/icons/tab-saving-dark-5.png +0 -0
  976. drowse/web/dist/icons/tab-saving-dark-6.png +0 -0
  977. drowse/web/dist/icons/tab-saving-dark-7.png +0 -0
  978. drowse/web/dist/icons/tab-saving-dark-8.png +0 -0
  979. drowse/web/dist/icons/tab-saving-dark-9.png +0 -0
  980. drowse/web/dist/icons/tab-saving-light-0.png +0 -0
  981. drowse/web/dist/icons/tab-saving-light-1.png +0 -0
  982. drowse/web/dist/icons/tab-saving-light-10.png +0 -0
  983. drowse/web/dist/icons/tab-saving-light-11.png +0 -0
  984. drowse/web/dist/icons/tab-saving-light-12.png +0 -0
  985. drowse/web/dist/icons/tab-saving-light-13.png +0 -0
  986. drowse/web/dist/icons/tab-saving-light-14.png +0 -0
  987. drowse/web/dist/icons/tab-saving-light-15.png +0 -0
  988. drowse/web/dist/icons/tab-saving-light-16.png +0 -0
  989. drowse/web/dist/icons/tab-saving-light-17.png +0 -0
  990. drowse/web/dist/icons/tab-saving-light-18.png +0 -0
  991. drowse/web/dist/icons/tab-saving-light-19.png +0 -0
  992. drowse/web/dist/icons/tab-saving-light-2.png +0 -0
  993. drowse/web/dist/icons/tab-saving-light-20.png +0 -0
  994. drowse/web/dist/icons/tab-saving-light-21.png +0 -0
  995. drowse/web/dist/icons/tab-saving-light-22.png +0 -0
  996. drowse/web/dist/icons/tab-saving-light-23.png +0 -0
  997. drowse/web/dist/icons/tab-saving-light-24.png +0 -0
  998. drowse/web/dist/icons/tab-saving-light-25.png +0 -0
  999. drowse/web/dist/icons/tab-saving-light-26.png +0 -0
  1000. drowse/web/dist/icons/tab-saving-light-27.png +0 -0
  1001. drowse/web/dist/icons/tab-saving-light-28.png +0 -0
  1002. drowse/web/dist/icons/tab-saving-light-29.png +0 -0
  1003. drowse/web/dist/icons/tab-saving-light-3.png +0 -0
  1004. drowse/web/dist/icons/tab-saving-light-30.png +0 -0
  1005. drowse/web/dist/icons/tab-saving-light-31.png +0 -0
  1006. drowse/web/dist/icons/tab-saving-light-32.png +0 -0
  1007. drowse/web/dist/icons/tab-saving-light-33.png +0 -0
  1008. drowse/web/dist/icons/tab-saving-light-34.png +0 -0
  1009. drowse/web/dist/icons/tab-saving-light-35.png +0 -0
  1010. drowse/web/dist/icons/tab-saving-light-36.png +0 -0
  1011. drowse/web/dist/icons/tab-saving-light-37.png +0 -0
  1012. drowse/web/dist/icons/tab-saving-light-38.png +0 -0
  1013. drowse/web/dist/icons/tab-saving-light-39.png +0 -0
  1014. drowse/web/dist/icons/tab-saving-light-4.png +0 -0
  1015. drowse/web/dist/icons/tab-saving-light-40.png +0 -0
  1016. drowse/web/dist/icons/tab-saving-light-41.png +0 -0
  1017. drowse/web/dist/icons/tab-saving-light-42.png +0 -0
  1018. drowse/web/dist/icons/tab-saving-light-43.png +0 -0
  1019. drowse/web/dist/icons/tab-saving-light-44.png +0 -0
  1020. drowse/web/dist/icons/tab-saving-light-45.png +0 -0
  1021. drowse/web/dist/icons/tab-saving-light-46.png +0 -0
  1022. drowse/web/dist/icons/tab-saving-light-47.png +0 -0
  1023. drowse/web/dist/icons/tab-saving-light-5.png +0 -0
  1024. drowse/web/dist/icons/tab-saving-light-6.png +0 -0
  1025. drowse/web/dist/icons/tab-saving-light-7.png +0 -0
  1026. drowse/web/dist/icons/tab-saving-light-8.png +0 -0
  1027. drowse/web/dist/icons/tab-saving-light-9.png +0 -0
  1028. drowse/web/dist/icons/tab-thinking-0.png +0 -0
  1029. drowse/web/dist/icons/tab-thinking-1.png +0 -0
  1030. drowse/web/dist/icons/tab-thinking-2.png +0 -0
  1031. drowse/web/dist/icons/tab-thinking-dark-0.png +0 -0
  1032. drowse/web/dist/icons/tab-thinking-dark-1.png +0 -0
  1033. drowse/web/dist/icons/tab-thinking-dark-10.png +0 -0
  1034. drowse/web/dist/icons/tab-thinking-dark-11.png +0 -0
  1035. drowse/web/dist/icons/tab-thinking-dark-12.png +0 -0
  1036. drowse/web/dist/icons/tab-thinking-dark-13.png +0 -0
  1037. drowse/web/dist/icons/tab-thinking-dark-14.png +0 -0
  1038. drowse/web/dist/icons/tab-thinking-dark-15.png +0 -0
  1039. drowse/web/dist/icons/tab-thinking-dark-16.png +0 -0
  1040. drowse/web/dist/icons/tab-thinking-dark-17.png +0 -0
  1041. drowse/web/dist/icons/tab-thinking-dark-18.png +0 -0
  1042. drowse/web/dist/icons/tab-thinking-dark-19.png +0 -0
  1043. drowse/web/dist/icons/tab-thinking-dark-2.png +0 -0
  1044. drowse/web/dist/icons/tab-thinking-dark-20.png +0 -0
  1045. drowse/web/dist/icons/tab-thinking-dark-21.png +0 -0
  1046. drowse/web/dist/icons/tab-thinking-dark-22.png +0 -0
  1047. drowse/web/dist/icons/tab-thinking-dark-23.png +0 -0
  1048. drowse/web/dist/icons/tab-thinking-dark-24.png +0 -0
  1049. drowse/web/dist/icons/tab-thinking-dark-25.png +0 -0
  1050. drowse/web/dist/icons/tab-thinking-dark-26.png +0 -0
  1051. drowse/web/dist/icons/tab-thinking-dark-27.png +0 -0
  1052. drowse/web/dist/icons/tab-thinking-dark-28.png +0 -0
  1053. drowse/web/dist/icons/tab-thinking-dark-29.png +0 -0
  1054. drowse/web/dist/icons/tab-thinking-dark-3.png +0 -0
  1055. drowse/web/dist/icons/tab-thinking-dark-30.png +0 -0
  1056. drowse/web/dist/icons/tab-thinking-dark-31.png +0 -0
  1057. drowse/web/dist/icons/tab-thinking-dark-32.png +0 -0
  1058. drowse/web/dist/icons/tab-thinking-dark-33.png +0 -0
  1059. drowse/web/dist/icons/tab-thinking-dark-34.png +0 -0
  1060. drowse/web/dist/icons/tab-thinking-dark-35.png +0 -0
  1061. drowse/web/dist/icons/tab-thinking-dark-36.png +0 -0
  1062. drowse/web/dist/icons/tab-thinking-dark-37.png +0 -0
  1063. drowse/web/dist/icons/tab-thinking-dark-38.png +0 -0
  1064. drowse/web/dist/icons/tab-thinking-dark-39.png +0 -0
  1065. drowse/web/dist/icons/tab-thinking-dark-4.png +0 -0
  1066. drowse/web/dist/icons/tab-thinking-dark-40.png +0 -0
  1067. drowse/web/dist/icons/tab-thinking-dark-41.png +0 -0
  1068. drowse/web/dist/icons/tab-thinking-dark-42.png +0 -0
  1069. drowse/web/dist/icons/tab-thinking-dark-43.png +0 -0
  1070. drowse/web/dist/icons/tab-thinking-dark-44.png +0 -0
  1071. drowse/web/dist/icons/tab-thinking-dark-45.png +0 -0
  1072. drowse/web/dist/icons/tab-thinking-dark-46.png +0 -0
  1073. drowse/web/dist/icons/tab-thinking-dark-47.png +0 -0
  1074. drowse/web/dist/icons/tab-thinking-dark-5.png +0 -0
  1075. drowse/web/dist/icons/tab-thinking-dark-6.png +0 -0
  1076. drowse/web/dist/icons/tab-thinking-dark-7.png +0 -0
  1077. drowse/web/dist/icons/tab-thinking-dark-8.png +0 -0
  1078. drowse/web/dist/icons/tab-thinking-dark-9.png +0 -0
  1079. drowse/web/dist/icons/tab-thinking-light-0.png +0 -0
  1080. drowse/web/dist/icons/tab-thinking-light-1.png +0 -0
  1081. drowse/web/dist/icons/tab-thinking-light-10.png +0 -0
  1082. drowse/web/dist/icons/tab-thinking-light-11.png +0 -0
  1083. drowse/web/dist/icons/tab-thinking-light-12.png +0 -0
  1084. drowse/web/dist/icons/tab-thinking-light-13.png +0 -0
  1085. drowse/web/dist/icons/tab-thinking-light-14.png +0 -0
  1086. drowse/web/dist/icons/tab-thinking-light-15.png +0 -0
  1087. drowse/web/dist/icons/tab-thinking-light-16.png +0 -0
  1088. drowse/web/dist/icons/tab-thinking-light-17.png +0 -0
  1089. drowse/web/dist/icons/tab-thinking-light-18.png +0 -0
  1090. drowse/web/dist/icons/tab-thinking-light-19.png +0 -0
  1091. drowse/web/dist/icons/tab-thinking-light-2.png +0 -0
  1092. drowse/web/dist/icons/tab-thinking-light-20.png +0 -0
  1093. drowse/web/dist/icons/tab-thinking-light-21.png +0 -0
  1094. drowse/web/dist/icons/tab-thinking-light-22.png +0 -0
  1095. drowse/web/dist/icons/tab-thinking-light-23.png +0 -0
  1096. drowse/web/dist/icons/tab-thinking-light-24.png +0 -0
  1097. drowse/web/dist/icons/tab-thinking-light-25.png +0 -0
  1098. drowse/web/dist/icons/tab-thinking-light-26.png +0 -0
  1099. drowse/web/dist/icons/tab-thinking-light-27.png +0 -0
  1100. drowse/web/dist/icons/tab-thinking-light-28.png +0 -0
  1101. drowse/web/dist/icons/tab-thinking-light-29.png +0 -0
  1102. drowse/web/dist/icons/tab-thinking-light-3.png +0 -0
  1103. drowse/web/dist/icons/tab-thinking-light-30.png +0 -0
  1104. drowse/web/dist/icons/tab-thinking-light-31.png +0 -0
  1105. drowse/web/dist/icons/tab-thinking-light-32.png +0 -0
  1106. drowse/web/dist/icons/tab-thinking-light-33.png +0 -0
  1107. drowse/web/dist/icons/tab-thinking-light-34.png +0 -0
  1108. drowse/web/dist/icons/tab-thinking-light-35.png +0 -0
  1109. drowse/web/dist/icons/tab-thinking-light-36.png +0 -0
  1110. drowse/web/dist/icons/tab-thinking-light-37.png +0 -0
  1111. drowse/web/dist/icons/tab-thinking-light-38.png +0 -0
  1112. drowse/web/dist/icons/tab-thinking-light-39.png +0 -0
  1113. drowse/web/dist/icons/tab-thinking-light-4.png +0 -0
  1114. drowse/web/dist/icons/tab-thinking-light-40.png +0 -0
  1115. drowse/web/dist/icons/tab-thinking-light-41.png +0 -0
  1116. drowse/web/dist/icons/tab-thinking-light-42.png +0 -0
  1117. drowse/web/dist/icons/tab-thinking-light-43.png +0 -0
  1118. drowse/web/dist/icons/tab-thinking-light-44.png +0 -0
  1119. drowse/web/dist/icons/tab-thinking-light-45.png +0 -0
  1120. drowse/web/dist/icons/tab-thinking-light-46.png +0 -0
  1121. drowse/web/dist/icons/tab-thinking-light-47.png +0 -0
  1122. drowse/web/dist/icons/tab-thinking-light-5.png +0 -0
  1123. drowse/web/dist/icons/tab-thinking-light-6.png +0 -0
  1124. drowse/web/dist/icons/tab-thinking-light-7.png +0 -0
  1125. drowse/web/dist/icons/tab-thinking-light-8.png +0 -0
  1126. drowse/web/dist/icons/tab-thinking-light-9.png +0 -0
  1127. drowse/web/dist/icons/tab-tokens-dark.png +0 -0
  1128. drowse/web/dist/icons/tab-tokens-light.png +0 -0
  1129. drowse/web/dist/icons/tab-tokens.png +0 -0
  1130. drowse/web/dist/icons/tab-training-0.png +0 -0
  1131. drowse/web/dist/icons/tab-training-1.png +0 -0
  1132. drowse/web/dist/icons/tab-training-2.png +0 -0
  1133. drowse/web/dist/icons/tab-training-dark-0.png +0 -0
  1134. drowse/web/dist/icons/tab-training-dark-1.png +0 -0
  1135. drowse/web/dist/icons/tab-training-dark-10.png +0 -0
  1136. drowse/web/dist/icons/tab-training-dark-11.png +0 -0
  1137. drowse/web/dist/icons/tab-training-dark-12.png +0 -0
  1138. drowse/web/dist/icons/tab-training-dark-13.png +0 -0
  1139. drowse/web/dist/icons/tab-training-dark-14.png +0 -0
  1140. drowse/web/dist/icons/tab-training-dark-15.png +0 -0
  1141. drowse/web/dist/icons/tab-training-dark-16.png +0 -0
  1142. drowse/web/dist/icons/tab-training-dark-17.png +0 -0
  1143. drowse/web/dist/icons/tab-training-dark-18.png +0 -0
  1144. drowse/web/dist/icons/tab-training-dark-19.png +0 -0
  1145. drowse/web/dist/icons/tab-training-dark-2.png +0 -0
  1146. drowse/web/dist/icons/tab-training-dark-20.png +0 -0
  1147. drowse/web/dist/icons/tab-training-dark-21.png +0 -0
  1148. drowse/web/dist/icons/tab-training-dark-22.png +0 -0
  1149. drowse/web/dist/icons/tab-training-dark-23.png +0 -0
  1150. drowse/web/dist/icons/tab-training-dark-24.png +0 -0
  1151. drowse/web/dist/icons/tab-training-dark-25.png +0 -0
  1152. drowse/web/dist/icons/tab-training-dark-26.png +0 -0
  1153. drowse/web/dist/icons/tab-training-dark-27.png +0 -0
  1154. drowse/web/dist/icons/tab-training-dark-28.png +0 -0
  1155. drowse/web/dist/icons/tab-training-dark-29.png +0 -0
  1156. drowse/web/dist/icons/tab-training-dark-3.png +0 -0
  1157. drowse/web/dist/icons/tab-training-dark-30.png +0 -0
  1158. drowse/web/dist/icons/tab-training-dark-31.png +0 -0
  1159. drowse/web/dist/icons/tab-training-dark-32.png +0 -0
  1160. drowse/web/dist/icons/tab-training-dark-33.png +0 -0
  1161. drowse/web/dist/icons/tab-training-dark-34.png +0 -0
  1162. drowse/web/dist/icons/tab-training-dark-35.png +0 -0
  1163. drowse/web/dist/icons/tab-training-dark-36.png +0 -0
  1164. drowse/web/dist/icons/tab-training-dark-37.png +0 -0
  1165. drowse/web/dist/icons/tab-training-dark-38.png +0 -0
  1166. drowse/web/dist/icons/tab-training-dark-39.png +0 -0
  1167. drowse/web/dist/icons/tab-training-dark-4.png +0 -0
  1168. drowse/web/dist/icons/tab-training-dark-40.png +0 -0
  1169. drowse/web/dist/icons/tab-training-dark-41.png +0 -0
  1170. drowse/web/dist/icons/tab-training-dark-42.png +0 -0
  1171. drowse/web/dist/icons/tab-training-dark-43.png +0 -0
  1172. drowse/web/dist/icons/tab-training-dark-44.png +0 -0
  1173. drowse/web/dist/icons/tab-training-dark-45.png +0 -0
  1174. drowse/web/dist/icons/tab-training-dark-46.png +0 -0
  1175. drowse/web/dist/icons/tab-training-dark-47.png +0 -0
  1176. drowse/web/dist/icons/tab-training-dark-5.png +0 -0
  1177. drowse/web/dist/icons/tab-training-dark-6.png +0 -0
  1178. drowse/web/dist/icons/tab-training-dark-7.png +0 -0
  1179. drowse/web/dist/icons/tab-training-dark-8.png +0 -0
  1180. drowse/web/dist/icons/tab-training-dark-9.png +0 -0
  1181. drowse/web/dist/icons/tab-training-light-0.png +0 -0
  1182. drowse/web/dist/icons/tab-training-light-1.png +0 -0
  1183. drowse/web/dist/icons/tab-training-light-10.png +0 -0
  1184. drowse/web/dist/icons/tab-training-light-11.png +0 -0
  1185. drowse/web/dist/icons/tab-training-light-12.png +0 -0
  1186. drowse/web/dist/icons/tab-training-light-13.png +0 -0
  1187. drowse/web/dist/icons/tab-training-light-14.png +0 -0
  1188. drowse/web/dist/icons/tab-training-light-15.png +0 -0
  1189. drowse/web/dist/icons/tab-training-light-16.png +0 -0
  1190. drowse/web/dist/icons/tab-training-light-17.png +0 -0
  1191. drowse/web/dist/icons/tab-training-light-18.png +0 -0
  1192. drowse/web/dist/icons/tab-training-light-19.png +0 -0
  1193. drowse/web/dist/icons/tab-training-light-2.png +0 -0
  1194. drowse/web/dist/icons/tab-training-light-20.png +0 -0
  1195. drowse/web/dist/icons/tab-training-light-21.png +0 -0
  1196. drowse/web/dist/icons/tab-training-light-22.png +0 -0
  1197. drowse/web/dist/icons/tab-training-light-23.png +0 -0
  1198. drowse/web/dist/icons/tab-training-light-24.png +0 -0
  1199. drowse/web/dist/icons/tab-training-light-25.png +0 -0
  1200. drowse/web/dist/icons/tab-training-light-26.png +0 -0
  1201. drowse/web/dist/icons/tab-training-light-27.png +0 -0
  1202. drowse/web/dist/icons/tab-training-light-28.png +0 -0
  1203. drowse/web/dist/icons/tab-training-light-29.png +0 -0
  1204. drowse/web/dist/icons/tab-training-light-3.png +0 -0
  1205. drowse/web/dist/icons/tab-training-light-30.png +0 -0
  1206. drowse/web/dist/icons/tab-training-light-31.png +0 -0
  1207. drowse/web/dist/icons/tab-training-light-32.png +0 -0
  1208. drowse/web/dist/icons/tab-training-light-33.png +0 -0
  1209. drowse/web/dist/icons/tab-training-light-34.png +0 -0
  1210. drowse/web/dist/icons/tab-training-light-35.png +0 -0
  1211. drowse/web/dist/icons/tab-training-light-36.png +0 -0
  1212. drowse/web/dist/icons/tab-training-light-37.png +0 -0
  1213. drowse/web/dist/icons/tab-training-light-38.png +0 -0
  1214. drowse/web/dist/icons/tab-training-light-39.png +0 -0
  1215. drowse/web/dist/icons/tab-training-light-4.png +0 -0
  1216. drowse/web/dist/icons/tab-training-light-40.png +0 -0
  1217. drowse/web/dist/icons/tab-training-light-41.png +0 -0
  1218. drowse/web/dist/icons/tab-training-light-42.png +0 -0
  1219. drowse/web/dist/icons/tab-training-light-43.png +0 -0
  1220. drowse/web/dist/icons/tab-training-light-44.png +0 -0
  1221. drowse/web/dist/icons/tab-training-light-45.png +0 -0
  1222. drowse/web/dist/icons/tab-training-light-46.png +0 -0
  1223. drowse/web/dist/icons/tab-training-light-47.png +0 -0
  1224. drowse/web/dist/icons/tab-training-light-5.png +0 -0
  1225. drowse/web/dist/icons/tab-training-light-6.png +0 -0
  1226. drowse/web/dist/icons/tab-training-light-7.png +0 -0
  1227. drowse/web/dist/icons/tab-training-light-8.png +0 -0
  1228. drowse/web/dist/icons/tab-training-light-9.png +0 -0
  1229. drowse/web/dist/icons/tab-unsupported-dark.png +0 -0
  1230. drowse/web/dist/icons/tab-unsupported-light.png +0 -0
  1231. drowse/web/dist/icons/tab-unsupported.png +0 -0
  1232. drowse/web/dist/icons/tab-working-0.png +0 -0
  1233. drowse/web/dist/icons/tab-working-1.png +0 -0
  1234. drowse/web/dist/icons/tab-working-2.png +0 -0
  1235. drowse/web/dist/icons/tab-working-dark-0.png +0 -0
  1236. drowse/web/dist/icons/tab-working-dark-1.png +0 -0
  1237. drowse/web/dist/icons/tab-working-dark-10.png +0 -0
  1238. drowse/web/dist/icons/tab-working-dark-11.png +0 -0
  1239. drowse/web/dist/icons/tab-working-dark-12.png +0 -0
  1240. drowse/web/dist/icons/tab-working-dark-13.png +0 -0
  1241. drowse/web/dist/icons/tab-working-dark-14.png +0 -0
  1242. drowse/web/dist/icons/tab-working-dark-15.png +0 -0
  1243. drowse/web/dist/icons/tab-working-dark-16.png +0 -0
  1244. drowse/web/dist/icons/tab-working-dark-17.png +0 -0
  1245. drowse/web/dist/icons/tab-working-dark-18.png +0 -0
  1246. drowse/web/dist/icons/tab-working-dark-19.png +0 -0
  1247. drowse/web/dist/icons/tab-working-dark-2.png +0 -0
  1248. drowse/web/dist/icons/tab-working-dark-20.png +0 -0
  1249. drowse/web/dist/icons/tab-working-dark-21.png +0 -0
  1250. drowse/web/dist/icons/tab-working-dark-22.png +0 -0
  1251. drowse/web/dist/icons/tab-working-dark-23.png +0 -0
  1252. drowse/web/dist/icons/tab-working-dark-24.png +0 -0
  1253. drowse/web/dist/icons/tab-working-dark-25.png +0 -0
  1254. drowse/web/dist/icons/tab-working-dark-26.png +0 -0
  1255. drowse/web/dist/icons/tab-working-dark-27.png +0 -0
  1256. drowse/web/dist/icons/tab-working-dark-28.png +0 -0
  1257. drowse/web/dist/icons/tab-working-dark-29.png +0 -0
  1258. drowse/web/dist/icons/tab-working-dark-3.png +0 -0
  1259. drowse/web/dist/icons/tab-working-dark-30.png +0 -0
  1260. drowse/web/dist/icons/tab-working-dark-31.png +0 -0
  1261. drowse/web/dist/icons/tab-working-dark-32.png +0 -0
  1262. drowse/web/dist/icons/tab-working-dark-33.png +0 -0
  1263. drowse/web/dist/icons/tab-working-dark-34.png +0 -0
  1264. drowse/web/dist/icons/tab-working-dark-35.png +0 -0
  1265. drowse/web/dist/icons/tab-working-dark-36.png +0 -0
  1266. drowse/web/dist/icons/tab-working-dark-37.png +0 -0
  1267. drowse/web/dist/icons/tab-working-dark-38.png +0 -0
  1268. drowse/web/dist/icons/tab-working-dark-39.png +0 -0
  1269. drowse/web/dist/icons/tab-working-dark-4.png +0 -0
  1270. drowse/web/dist/icons/tab-working-dark-40.png +0 -0
  1271. drowse/web/dist/icons/tab-working-dark-41.png +0 -0
  1272. drowse/web/dist/icons/tab-working-dark-42.png +0 -0
  1273. drowse/web/dist/icons/tab-working-dark-43.png +0 -0
  1274. drowse/web/dist/icons/tab-working-dark-44.png +0 -0
  1275. drowse/web/dist/icons/tab-working-dark-45.png +0 -0
  1276. drowse/web/dist/icons/tab-working-dark-46.png +0 -0
  1277. drowse/web/dist/icons/tab-working-dark-47.png +0 -0
  1278. drowse/web/dist/icons/tab-working-dark-5.png +0 -0
  1279. drowse/web/dist/icons/tab-working-dark-6.png +0 -0
  1280. drowse/web/dist/icons/tab-working-dark-7.png +0 -0
  1281. drowse/web/dist/icons/tab-working-dark-8.png +0 -0
  1282. drowse/web/dist/icons/tab-working-dark-9.png +0 -0
  1283. drowse/web/dist/icons/tab-working-light-0.png +0 -0
  1284. drowse/web/dist/icons/tab-working-light-1.png +0 -0
  1285. drowse/web/dist/icons/tab-working-light-10.png +0 -0
  1286. drowse/web/dist/icons/tab-working-light-11.png +0 -0
  1287. drowse/web/dist/icons/tab-working-light-12.png +0 -0
  1288. drowse/web/dist/icons/tab-working-light-13.png +0 -0
  1289. drowse/web/dist/icons/tab-working-light-14.png +0 -0
  1290. drowse/web/dist/icons/tab-working-light-15.png +0 -0
  1291. drowse/web/dist/icons/tab-working-light-16.png +0 -0
  1292. drowse/web/dist/icons/tab-working-light-17.png +0 -0
  1293. drowse/web/dist/icons/tab-working-light-18.png +0 -0
  1294. drowse/web/dist/icons/tab-working-light-19.png +0 -0
  1295. drowse/web/dist/icons/tab-working-light-2.png +0 -0
  1296. drowse/web/dist/icons/tab-working-light-20.png +0 -0
  1297. drowse/web/dist/icons/tab-working-light-21.png +0 -0
  1298. drowse/web/dist/icons/tab-working-light-22.png +0 -0
  1299. drowse/web/dist/icons/tab-working-light-23.png +0 -0
  1300. drowse/web/dist/icons/tab-working-light-24.png +0 -0
  1301. drowse/web/dist/icons/tab-working-light-25.png +0 -0
  1302. drowse/web/dist/icons/tab-working-light-26.png +0 -0
  1303. drowse/web/dist/icons/tab-working-light-27.png +0 -0
  1304. drowse/web/dist/icons/tab-working-light-28.png +0 -0
  1305. drowse/web/dist/icons/tab-working-light-29.png +0 -0
  1306. drowse/web/dist/icons/tab-working-light-3.png +0 -0
  1307. drowse/web/dist/icons/tab-working-light-30.png +0 -0
  1308. drowse/web/dist/icons/tab-working-light-31.png +0 -0
  1309. drowse/web/dist/icons/tab-working-light-32.png +0 -0
  1310. drowse/web/dist/icons/tab-working-light-33.png +0 -0
  1311. drowse/web/dist/icons/tab-working-light-34.png +0 -0
  1312. drowse/web/dist/icons/tab-working-light-35.png +0 -0
  1313. drowse/web/dist/icons/tab-working-light-36.png +0 -0
  1314. drowse/web/dist/icons/tab-working-light-37.png +0 -0
  1315. drowse/web/dist/icons/tab-working-light-38.png +0 -0
  1316. drowse/web/dist/icons/tab-working-light-39.png +0 -0
  1317. drowse/web/dist/icons/tab-working-light-4.png +0 -0
  1318. drowse/web/dist/icons/tab-working-light-40.png +0 -0
  1319. drowse/web/dist/icons/tab-working-light-41.png +0 -0
  1320. drowse/web/dist/icons/tab-working-light-42.png +0 -0
  1321. drowse/web/dist/icons/tab-working-light-43.png +0 -0
  1322. drowse/web/dist/icons/tab-working-light-44.png +0 -0
  1323. drowse/web/dist/icons/tab-working-light-45.png +0 -0
  1324. drowse/web/dist/icons/tab-working-light-46.png +0 -0
  1325. drowse/web/dist/icons/tab-working-light-47.png +0 -0
  1326. drowse/web/dist/icons/tab-working-light-5.png +0 -0
  1327. drowse/web/dist/icons/tab-working-light-6.png +0 -0
  1328. drowse/web/dist/icons/tab-working-light-7.png +0 -0
  1329. drowse/web/dist/icons/tab-working-light-8.png +0 -0
  1330. drowse/web/dist/icons/tab-working-light-9.png +0 -0
  1331. drowse/web/dist/index.html +34 -0
  1332. drowse/web/dist/social/drowse.png +0 -0
  1333. drowse/web/dist/social/drowse.svg +6 -0
  1334. drowse/web/dist/theme-init.js +25 -0
  1335. drowse/web/routes.py +118 -0
  1336. drowse_ai-5.3.1.dist-info/METADATA +632 -0
  1337. drowse_ai-5.3.1.dist-info/RECORD +1341 -0
  1338. drowse_ai-5.3.1.dist-info/WHEEL +5 -0
  1339. drowse_ai-5.3.1.dist-info/entry_points.txt +2 -0
  1340. drowse_ai-5.3.1.dist-info/licenses/LICENSE +661 -0
  1341. drowse_ai-5.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1469 @@
1
+ """Coordinate discovery and topology selection for discover-mode manifolds.
2
+
3
+ A discover-mode manifold folder supplies labeled node corpora and nothing
4
+ else: the coordinate system is *derived* per-model at fit time from the
5
+ node centroids. This module owns that derivation and the geometry choice
6
+ that precedes it.
7
+
8
+ Two layers:
9
+
10
+ * **Coordinate derivation** — :func:`derive_pca_coords` (eigendecompose
11
+ the consensus Gram, keep the smallest variance-crossing prefix) and
12
+ :func:`derive_spectral_coords` (Laplacian eigenmaps over a symmetric
13
+ k-NN graph, dimension picked at the eigenvalue-ratio cliff), dispatched
14
+ by :func:`discover_coords`. Both embed the same layer-agnostic
15
+ consensus geometry; :func:`neutral_layout_coord` places the per-model
16
+ neutral mean into a flat layout by landmark MDS so ``% 0,…,0`` reads as
17
+ neutral.
18
+ * **Topology selection** (``fit_mode="auto"``) — :func:`select_topology`
19
+ makes two deliberately decoupled decisions: flat-vs-curved by GCV in a
20
+ shared whitened-reduced metric, and periodic axes by Vietoris-Rips H1
21
+ persistent homology (:func:`_rips_h1_persistence` /
22
+ :func:`_count_persistent_loops`) coordinated off the spectral
23
+ eigenpairs, with :func:`_faint_cycle_coords` as the guarded
24
+ single-cycle fallback for rings too thin for PH's hole-size threshold.
25
+
26
+ Everything here is pure tensor math on the ``(K, K)`` node scatter — no
27
+ model, no IO. It depends on :mod:`drowse.core.manifold` for the domain
28
+ types and the per-layer fit primitives, never the other way round;
29
+ ``extraction.py::_fit_locked`` is the one production consumer.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import logging
34
+ import math
35
+ from dataclasses import dataclass, field
36
+ from typing import TYPE_CHECKING, Any
37
+
38
+ import torch
39
+
40
+ from drowse.core.manifold import (
41
+ BoxAxis,
42
+ BoxDomain,
43
+ CustomDomain,
44
+ ManifoldDomain,
45
+ RbfFitPlan,
46
+ _gcv_select_lambda,
47
+ _pca_basis,
48
+ _rbf_poised,
49
+ _rbf_smoother_matrix,
50
+ prepare_rbf_fit_plan,
51
+ )
52
+
53
+ if TYPE_CHECKING:
54
+ from drowse.core.mahalanobis import LayerWhitener
55
+
56
+ log = logging.getLogger(__name__)
57
+
58
+
59
+ # ======================================================= coord discovery ===
60
+ #
61
+ # Derive node coordinates from per-node activation centroids when the user
62
+ # hasn't authored a coordinate system. PCA is the safe linear default
63
+ # (reproduces the flat-subspace layout a user would author themselves once
64
+ # they knew the answer). Spectral (Laplacian eigenmaps) recovers
65
+ # curved-manifold topology that PCA flattens. Both feed the same downstream
66
+ # machinery: ``CustomDomain(k)`` with identity embedding, then per-layer
67
+ # ``manifold.fit_layer_subspace`` exactly as for authored manifolds.
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class PcaDiagnostics:
72
+ """Diagnostics for a PCA coordinate derivation.
73
+
74
+ Surfaced on the per-model sidecar so a user inspecting a fitted
75
+ manifold can tell whether the chosen ``k`` was well-supported by
76
+ the data (a sharp variance plateau) or compromised (variance still
77
+ rising at the cap).
78
+ """
79
+
80
+ per_component_variance: torch.Tensor # (max_dim,) normalized to sum to 1
81
+ cumulative_variance: torch.Tensor # (max_dim,)
82
+ picked_k: int
83
+ threshold: float
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class SpectralDiagnostics:
88
+ """Diagnostics for a spectral (Laplacian-eigenmaps) coordinate derivation.
89
+
90
+ The spectral gap is the one knob that says "the data has a clean
91
+ k-dimensional structure" (large gap at ``picked_k``) versus "no
92
+ clean cut, pick a dim by hand" (gaps flat across the candidate
93
+ range). Bandwidth + ``k_nn`` are recorded for reproducibility:
94
+ both default to data-driven values (median k-NN distance,
95
+ ``max(5, ceil(log K))``) when the user doesn't override.
96
+ """
97
+
98
+ eigenvalues: torch.Tensor # (kept_count,) non-trivial spectrum, ascending
99
+ picked_k: int
100
+ # ``picked_k`` is chosen by the eigenvalue-*ratio* heuristic
101
+ # (``argmax(λ_{k+1} / λ_k)`` over ``[1, max_dim]``), not the spec's
102
+ # original absolute-gap form -- the absolute gap scales like k² on
103
+ # S¹ (continuous-limit eigenvalues are quadratic in k) and pushes
104
+ # the picker toward larger k. ``gap_index`` is kept as an alias of
105
+ # ``picked_k`` for diagnostic-render call sites that pre-date the
106
+ # rename; ``gap_magnitude`` is still the *absolute* gap at
107
+ # ``picked_k`` for the inspector's bar-chart annotation.
108
+ gap_index: int # == picked_k
109
+ gap_magnitude: float # eigenvalues[picked_k] - eigenvalues[picked_k - 1]
110
+ bandwidth: float # heat-kernel sigma actually used
111
+ k_nn: int # number of nearest neighbors actually used
112
+ component_count: int # always 1 on success (disconnected graphs raise)
113
+ # Authored-dimensionality floor. ``heuristic_k`` is what the
114
+ # eigenvalue-ratio cliff picked on its own; when ``min_dim`` is set and
115
+ # exceeds it, ``picked_k`` is floored to ``min(min_dim, cap)`` and
116
+ # ``pinned`` is True. The cliff *undershoots* for a manifold whose
117
+ # strongest mode dominates the spectrum (a small Fiedler value forces an
118
+ # early ratio cliff), so the floor lets a known geometry (PAD's P×A×D)
119
+ # survive a re-derivation that would otherwise collapse it.
120
+ heuristic_k: int = 0 # ratio-cliff pick before the floor
121
+ min_dim: int | None = None # author-declared floor (None = pure heuristic)
122
+ pinned: bool = False # True iff the floor raised picked_k
123
+
124
+
125
+ def derive_pca_coords(
126
+ gram: torch.Tensor,
127
+ *,
128
+ max_dim: int = 8,
129
+ var_threshold: float = 0.70,
130
+ ) -> tuple[torch.Tensor, PcaDiagnostics]:
131
+ """Derive node coordinates from the eigendecomposition of a centroid Gram.
132
+
133
+ ``gram`` is the ``(K, K)`` symmetric-PSD Gram of the K node centroids.
134
+ For a single layer it is ``X̃ X̃ᵀ`` (``X̃`` = node-mean-centered
135
+ centroids), whose eigendecomposition is exactly the PCA of those
136
+ centroids — eigenvalues are the component variances, eigenvectors
137
+ scaled by ``√λ`` are the PCA scores ``U S``. For the layer-agnostic
138
+ fit it is the **signal-weighted consensus** Gram
139
+ ``mean_L X̃_L Σ_L⁻¹ X̃_Lᵀ`` (whitened, averaged over every fit layer):
140
+ the ``(K, K)`` Gram is the layer-invariant object, so averaging it is
141
+ what lets the coordinate layout draw on all layers at once instead of
142
+ one arbitrary reference layer. A layer where the nodes aren't
143
+ separated contributes a near-zero whitened Gram, so it drops out of
144
+ the average on its own — no need to pick a layer band.
145
+
146
+ Returns ``(coords, diagnostics)`` where ``coords`` is ``(K, k)`` and
147
+ ``k`` is the smallest prefix whose cumulative variance crosses
148
+ ``var_threshold``, capped at ``max_dim`` and floored at 1.
149
+
150
+ Pure tensor, fp32, dependency-free.
151
+ """
152
+ gram = gram.to(torch.float32)
153
+ K = gram.shape[0]
154
+ if gram.dim() != 2 or gram.shape[1] != K:
155
+ raise ValueError(
156
+ f"PCA coord derivation needs a square (K, K) Gram, got shape "
157
+ f"{tuple(gram.shape)}"
158
+ )
159
+ if K < 2:
160
+ raise ValueError(
161
+ f"PCA coord derivation needs >= 2 centroids, got {K}"
162
+ )
163
+ # Symmetrize away finite-precision drift, then eigendecompose. eigh is
164
+ # the Gram analogue of the old SVD-of-centered-centroids: eigenvalues =
165
+ # S², eigenvectors = U (up to per-axis sign, immaterial for a layout).
166
+ gram = 0.5 * (gram + gram.transpose(0, 1))
167
+ evals, evecs = torch.linalg.eigh(gram) # ascending
168
+ # Descending order (PCA convention); clamp tiny-negative eigenvalues
169
+ # that fp drift can leak past a genuinely PSD Gram.
170
+ evals = evals.flip(0).clamp(min=0.0)
171
+ evecs = evecs.flip(1)
172
+ # Variance fractions are λ_i / Σ_i λ_i — metric-invariant and unaffected
173
+ # by the (sum-vs-mean) scaling of the consensus average.
174
+ total = evals.sum().clamp(min=1e-12)
175
+ var_frac = evals / total # (K,)
176
+ cum_var = torch.cumsum(var_frac, dim=0) # (K,)
177
+ cap = min(max_dim, var_frac.shape[0])
178
+ # Smallest k such that cum_var[k-1] >= threshold; default to cap.
179
+ over = (cum_var[:cap] >= var_threshold).nonzero(as_tuple=False)
180
+ picked_k = int(over[0].item()) + 1 if over.numel() > 0 else cap
181
+ picked_k = max(1, min(picked_k, cap))
182
+
183
+ coords = evecs[:, :picked_k] * evals[:picked_k].sqrt() # (K, picked_k) = U S
184
+ diagnostics = PcaDiagnostics(
185
+ per_component_variance=var_frac[:cap].detach().clone(),
186
+ cumulative_variance=cum_var[:cap].detach().clone(),
187
+ picked_k=picked_k,
188
+ threshold=float(var_threshold),
189
+ )
190
+ return coords.contiguous(), diagnostics
191
+
192
+
193
+ def neutral_layout_coord(
194
+ node_coords: torch.Tensor,
195
+ neutral_cross_gram: torch.Tensor,
196
+ ) -> torch.Tensor:
197
+ """Project the neutral baseline into a consensus-PCA node layout.
198
+
199
+ :func:`derive_pca_coords` returns ``node_coords`` ``(K, k)`` centered on the
200
+ **node mean** (PCA removes it), so the layout origin is the node centroid,
201
+ not the neutral baseline. This is the classical-MDS / kernel-PCA
202
+ out-of-sample extension that locates neutral in the *same* layout:
203
+
204
+ - ``node_coords`` are the ``U S`` scores, so ``node_coords @ node_coordsᵀ``
205
+ reproduces the rank-``k`` consensus Gram ``Ḡ`` the layout was built from.
206
+ - ``neutral_cross_gram`` ``(K,)`` is neutral's matching cross-Gram column —
207
+ its node-mean-centered, whitened inner product with each node centroid in
208
+ the *same* layer-averaged metric ``Ḡ`` uses
209
+ (``gᵢ = mean_L (ν_L − μ_L)ᵀ Σ_L⁻¹ (c_{L,i} − μ_L)``).
210
+
211
+ The landmark coordinate is then ``cₙ = node_coords⁺ g`` (``⁺`` the
212
+ pseudo-inverse: ``cₙ[r] = (1/√λ_r) Σᵢ Uᵢᵣ gᵢ``). Subtracting it from
213
+ ``node_coords`` re-anchors the layout so neutral sits at the origin — a pure
214
+ translation that leaves the inter-node geometry (and, via the
215
+ translation-invariant cardinal weights, every steering target) unchanged.
216
+ Returns ``(k,)`` fp32.
217
+ """
218
+ nc = node_coords.to(torch.float32)
219
+ g = neutral_cross_gram.to(torch.float32).reshape(-1)
220
+ if g.shape[0] != nc.shape[0]:
221
+ raise ValueError(
222
+ f"neutral cross-Gram has {g.shape[0]} entries but the layout has "
223
+ f"{nc.shape[0]} nodes"
224
+ )
225
+ return (torch.linalg.pinv(nc) @ g).contiguous()
226
+
227
+
228
+ def _knn_adjacency(
229
+ distances: torch.Tensor, k_nn: int,
230
+ ) -> tuple[torch.Tensor, torch.Tensor]:
231
+ """Symmetric k-NN graph from a pairwise distance matrix.
232
+
233
+ Returns ``(mask, neighbor_dists)`` where ``mask`` is a ``(K, K)``
234
+ bool tensor (True for retained edges, no self-loops) and
235
+ ``neighbor_dists`` is the flat 1-D tensor of edge distances actually
236
+ kept — useful for the median-bandwidth default.
237
+
238
+ Edges are symmetrized by **union** (``i ↔ j`` if ``j`` is in
239
+ ``knn(i)`` OR ``i`` is in ``knn(j)``). Union is the standard
240
+ convention for Laplacian eigenmaps; intersection drops too many
241
+ edges and tends to disconnect borderline points.
242
+ """
243
+ K = distances.shape[0]
244
+ # Include self-distance in the top-k+1 call, then strip the self
245
+ # entry — guaranteed at position 0 because diag is zero.
246
+ k_eff = min(k_nn + 1, K)
247
+ _, idx = torch.topk(distances, k=k_eff, dim=1, largest=False)
248
+ # Build directed mask: row i has 1 in column idx[i, j] for j>=1.
249
+ directed = torch.zeros(K, K, dtype=torch.bool, device=distances.device)
250
+ rows = torch.arange(K, device=distances.device).unsqueeze(1).expand(-1, k_eff)
251
+ directed[rows, idx] = True
252
+ directed.fill_diagonal_(False)
253
+ # Symmetrize via union.
254
+ mask = directed | directed.T
255
+ neighbor_dists = distances[mask]
256
+ return mask, neighbor_dists
257
+
258
+
259
+ class _DSU:
260
+ """Disjoint-set forest with path halving over the node ids ``0..n-1``.
261
+
262
+ The single union-find behind every graph reduction in the topology
263
+ block: connected-component counting on a k-NN adjacency mask, the H0
264
+ (edge) half of the Vietoris-Rips boundary reduction, and the Kruskal
265
+ MST that fixes the persistence connectivity scale ``ε_c``. Node counts
266
+ are tens to hundreds, so path halving alone is ample — no rank/size
267
+ balancing, matching what each hand-rolled copy did.
268
+ """
269
+
270
+ __slots__ = ("_parent",)
271
+
272
+ def __init__(self, n: int) -> None:
273
+ self._parent = list(range(n))
274
+
275
+ def find(self, x: int) -> int:
276
+ parent = self._parent
277
+ while parent[x] != x:
278
+ parent[x] = parent[parent[x]]
279
+ x = parent[x]
280
+ return x
281
+
282
+ def union(self, a: int, b: int) -> bool:
283
+ """Merge the sets holding ``a`` and ``b``; True iff they differed.
284
+
285
+ The boolean is the H0 signal both persistence callers need: True
286
+ means the edge was a tree edge (it killed a component), False means
287
+ both endpoints were already joined (the edge births a 1-cycle).
288
+ """
289
+ ra, rb = self.find(a), self.find(b)
290
+ if ra == rb:
291
+ return False
292
+ self._parent[ra] = rb
293
+ return True
294
+
295
+
296
+ def _connected_components(mask: torch.Tensor) -> int:
297
+ """Number of connected components in an undirected adjacency mask.
298
+
299
+ Plain union-find on a small ``(K, K)`` bool matrix — ``K`` is on
300
+ the order of tens to hundreds, so a quadratic scan over the upper
301
+ triangle is fine and avoids both scipy and the eigenvalue-counting
302
+ tolerance choice.
303
+ """
304
+ K = mask.shape[0]
305
+ dsu = _DSU(K)
306
+ # Only need the upper triangle since the mask is symmetric.
307
+ rows, cols = mask.triu(diagonal=1).nonzero(as_tuple=True)
308
+ for r, c in zip(rows.tolist(), cols.tolist(), strict=True):
309
+ dsu.union(r, c)
310
+ return len({dsu.find(i) for i in range(K)})
311
+
312
+
313
+ def _laplacian_eigen(
314
+ gram: torch.Tensor,
315
+ *,
316
+ k_nn: int | None = None,
317
+ bandwidth: float | None = None,
318
+ ) -> tuple[torch.Tensor, torch.Tensor, int, float]:
319
+ """Normalized-Laplacian eigenmaps core shared by every spectral topology.
320
+
321
+ Builds the symmetric k-NN heat-kernel graph over the distances read off
322
+ ``gram`` (``d²_ij = G_ii + G_jj − 2 G_ij``), forms the normalized
323
+ Laplacian ``L = I − D^{-1/2} W D^{-1/2}``, eigendecomposes it, and drops
324
+ the trivial (constant-mode) eigenpair. Returns ``(nontrivial_vals
325
+ (K-1,), nontrivial_vecs (K, K-1), k_nn, bandwidth)`` — the spectral
326
+ embedding :func:`derive_spectral_coords` reads as flat coordinates and
327
+ :func:`_detect_periodic_axes` reads as ``(cos, sin)`` angle pairs for the
328
+ loops persistent homology has counted.
329
+
330
+ The first ``n`` nontrivial eigenfunctions of data sampled from a manifold
331
+ are its lowest Laplace–Beltrami modes — on a circle the ``(cos θ, sin θ)``
332
+ pair — which is what lets the periodic detection read an angle coordinate
333
+ straight off this embedding once a loop is confirmed.
334
+
335
+ Raises on a < 4-node heap (no graph) or a disconnected k-NN graph (no
336
+ single embedding) — the geometric preconditions every spectral topology
337
+ shares. ``derive_spectral_coords`` validates first with its own
338
+ messages, so those callers never reach these.
339
+ """
340
+ gram = gram.to(torch.float32)
341
+ K = gram.shape[0]
342
+ if gram.dim() != 2 or gram.shape[1] != K:
343
+ raise ValueError(
344
+ f"spectral embedding needs a square (K, K) Gram, got shape "
345
+ f"{tuple(gram.shape)}"
346
+ )
347
+ if K < 4:
348
+ raise ValueError(
349
+ f"spectral embedding needs >= 4 centroids to form a k-NN graph, "
350
+ f"got {K}"
351
+ )
352
+ if k_nn is None:
353
+ k_nn = max(5, math.ceil(math.log(K)))
354
+ k_nn = max(1, min(k_nn, K - 1))
355
+
356
+ # Pairwise distances from the Gram: d²_ij = G_ii + G_jj − 2 G_ij. Clamp
357
+ # the (PSD-up-to-fp-drift) squared distances off negative before sqrt.
358
+ gram = 0.5 * (gram + gram.transpose(0, 1))
359
+ diag = gram.diagonal()
360
+ sq = diag.unsqueeze(0) + diag.unsqueeze(1) - 2.0 * gram
361
+ distances = sq.clamp(min=0.0).sqrt()
362
+ distances.fill_diagonal_(0.0)
363
+ mask, neighbor_dists = _knn_adjacency(distances, k_nn)
364
+
365
+ components = _connected_components(mask)
366
+ if components > 1:
367
+ raise ValueError(
368
+ f"spectral embedding: k-NN graph has {components} connected "
369
+ f"components (need 1). Raise k_nn or switch to PCA."
370
+ )
371
+
372
+ if bandwidth is None:
373
+ if neighbor_dists.numel() == 0:
374
+ raise ValueError("spectral embedding: k-NN graph has no edges")
375
+ bandwidth = float(neighbor_dists.median().item())
376
+ if bandwidth <= 0.0:
377
+ # All-zero neighbor distances would NaN out the heat kernel.
378
+ bandwidth = 1e-6
379
+ bandwidth = float(bandwidth)
380
+
381
+ # Heat-kernel weights on the symmetric k-NN edge set.
382
+ W = torch.zeros_like(distances)
383
+ sq = (distances * distances) / (2.0 * bandwidth * bandwidth)
384
+ W = torch.where(mask, torch.exp(-sq), W)
385
+ W.fill_diagonal_(0.0)
386
+
387
+ deg = W.sum(dim=1)
388
+ d_inv_sqrt = deg.clamp(min=1e-12).rsqrt()
389
+ # L_sym = I - D^{-1/2} W D^{-1/2}
390
+ L = -W * d_inv_sqrt.unsqueeze(0) * d_inv_sqrt.unsqueeze(1)
391
+ L.fill_diagonal_(0.0)
392
+ L = L + torch.eye(K, dtype=L.dtype, device=L.device)
393
+
394
+ eigvals, eigvecs = torch.linalg.eigh(L) # ascending
395
+ # Drop the smallest eigenvalue (~0 for a connected graph); its eigenvector
396
+ # is D^{1/2}1, carrying no embedding information.
397
+ return eigvals[1:], eigvecs[:, 1:], int(k_nn), float(bandwidth)
398
+
399
+
400
+ def derive_spectral_coords(
401
+ gram: torch.Tensor,
402
+ *,
403
+ max_dim: int = 8,
404
+ min_dim: int | None = None,
405
+ k_nn: int | None = None,
406
+ bandwidth: float | None = None,
407
+ _eigen_result: tuple[torch.Tensor, torch.Tensor, int, float] | None = None,
408
+ ) -> tuple[torch.Tensor, SpectralDiagnostics]:
409
+ """Derive node coordinates from a Laplacian-eigenmaps spectral embedding.
410
+
411
+ ``gram`` is the ``(K, K)`` symmetric-PSD centroid Gram (see
412
+ :func:`derive_pca_coords`); the pairwise distances the graph is built
413
+ from are read straight off it, ``d²_ij = G_ii + G_jj − 2 G_ij``. For a
414
+ single layer this is the plain Euclidean distance ``‖c_i − c_j‖``; for
415
+ the layer-agnostic fit, where ``gram`` is the layer-averaged whitened
416
+ consensus, it is the mean per-layer **Mahalanobis** distance — which is
417
+ exactly the squared distance in the concatenated-whitened space, the
418
+ same geometry :func:`derive_pca_coords` embeds linearly. (``mean_L`` of
419
+ ``diag(G_L) ⊕ diag(G_L) − 2 G_L`` equals ``diag(Ḡ) ⊕ diag(Ḡ) − 2 Ḡ``
420
+ because the diagonal and the Gram are both linear in the layer average,
421
+ so the consensus distance is recoverable from the consensus Gram alone.)
422
+
423
+ Build a symmetric k-NN graph over those distances,
424
+ heat-kernel weights ``W_ij = exp(-d_ij^2 / (2 sigma^2))``, the
425
+ normalized Laplacian ``L = I - D^{-1/2} W D^{-1/2}``, eigendecompose
426
+ via :func:`torch.linalg.eigh`, drop the smallest (trivial) eigenvalue,
427
+ and take the next ``k`` eigenvector entries as coordinates.
428
+
429
+ ``k`` is chosen by the **eigenvalue-ratio** heuristic: the index
430
+ that maximizes ``λ_{k+1} / λ_k`` for ``1 <= k <= max_dim``. This
431
+ captures the structural cliff between "signal" and "noise"
432
+ eigenvalues robustly across topologies — on a circle the cos/sin
433
+ pair at the lowest frequency produces a clean ratio cliff between
434
+ ``λ_2`` and ``λ_3``, picking ``k=2``. Absolute gaps
435
+ ``λ_{k+1} - λ_k`` would over-pick on S^1 because the eigenvalues
436
+ scale ~ ``k²`` in the continuous limit, so the largest absolute
437
+ gap lands at high ``k`` rather than the structural cliff.
438
+
439
+ ``min_dim`` floors the picked dimension: when an author declares the
440
+ intrinsic dimensionality (e.g. PAD is P×A×D, 3-D by construction), the
441
+ ratio cliff can *undershoot* if one mode dominates the spectrum — a
442
+ small first non-trivial (Fiedler) eigenvalue makes ``λ_2 / λ_1`` the
443
+ largest ratio, picking ``k=1`` regardless of the true geometry. The
444
+ floor raises ``picked_k`` to ``min(min_dim, cap)`` (it can't exceed the
445
+ usable eigenvector budget) and the diagnostics record both the
446
+ heuristic pick and that the floor fired. ``None`` (default) leaves the
447
+ pick to the heuristic alone.
448
+
449
+ Defaults: ``k_nn = max(5, ceil(log K))``, ``bandwidth = median
450
+ k-NN distance``. Both are recorded in the diagnostics.
451
+
452
+ A disconnected k-NN graph raises :class:`ValueError` with the
453
+ component count — a degenerate heap with isolated centroids cannot
454
+ be embedded by Laplacian eigenmaps. Recommend the user raise
455
+ ``k_nn`` or switch to PCA.
456
+
457
+ Stays inside drowse's no-scipy rule. Noisy below roughly 50 nodes
458
+ (too few centroids for stable heat-kernel weights, spectral gap
459
+ collapses into the eigenvalue noise floor); PCA is the right
460
+ default at bundled-heap sizes.
461
+ """
462
+ gram = gram.to(torch.float32)
463
+ K = gram.shape[0]
464
+ if gram.dim() != 2 or gram.shape[1] != K:
465
+ raise ValueError(
466
+ f"spectral coord derivation needs a square (K, K) Gram, got "
467
+ f"shape {tuple(gram.shape)}"
468
+ )
469
+ if K < 4:
470
+ # Need at least 4 nodes to form any kind of k-NN graph and have
471
+ # a candidate gap range. Below that the heuristics are pure
472
+ # noise; raise early rather than ship a meaningless embedding.
473
+ raise ValueError(
474
+ f"spectral coord derivation needs >= 4 centroids, got {K}"
475
+ )
476
+ # Graph build + normalized-Laplacian eigendecomposition, shared with the
477
+ # sphere/torus spectral derivations. The square + ``K < 4`` validation
478
+ # above stays here so the spectral-specific error messages are preserved;
479
+ # ``_laplacian_eigen`` re-checks defensively for its other callers.
480
+ if _eigen_result is None:
481
+ nontrivial_vals, nontrivial_vecs, k_nn, bandwidth = _laplacian_eigen(
482
+ gram, k_nn=k_nn, bandwidth=bandwidth,
483
+ )
484
+ else:
485
+ nontrivial_vals, nontrivial_vecs, k_nn, bandwidth = _eigen_result
486
+
487
+ # Pick k by the eigenvalue-ratio heuristic. For each candidate
488
+ # ``k`` in ``[1, cap]`` the ratio ``nontrivial[k] / nontrivial[k-1]``
489
+ # measures the multiplicative cliff between "kept" and "dropped"
490
+ # eigenvalues — large when ``k`` separates a structural cluster of
491
+ # low eigenvalues from a clearly higher group, near 1 when the
492
+ # spectrum is smooth. Picking the argmax-ratio is the standard
493
+ # spectral-gap heuristic; the alternative absolute gap
494
+ # ``nontrivial[k] - nontrivial[k-1]`` over-picks on S^1 because
495
+ # eigenvalues scale ~ ``k²``.
496
+ cap = min(max_dim, nontrivial_vals.shape[0] - 1, K - 2)
497
+ cap = max(1, cap)
498
+ if cap == 1:
499
+ picked_k = 1
500
+ else:
501
+ # Ratio at "use k dims" = nontrivial[k] / nontrivial[k-1].
502
+ # Clamp the denominator off zero — a vanishing eigenvalue at
503
+ # ``k-1`` already means the graph is near-disconnected; the
504
+ # ratio there is meaningless but mustn't NaN out the argmax.
505
+ denom = nontrivial_vals[:cap].clamp(min=1e-12)
506
+ ratios = nontrivial_vals[1:cap + 1] / denom
507
+ picked_k = int(ratios.argmax().item()) + 1
508
+
509
+ # Authored-dimensionality floor: honor a declared intrinsic dim over the
510
+ # ratio cliff (which undershoots when one mode dominates — see docstring).
511
+ # The floor can't exceed the usable eigenvector budget (``cap``).
512
+ heuristic_k = picked_k
513
+ if min_dim is not None:
514
+ picked_k = max(picked_k, min(int(min_dim), cap))
515
+ pinned = picked_k != heuristic_k
516
+
517
+ # Absolute gap at the *final* picked_k (diagnostic annotation); 0 when
518
+ # picked_k saturates the available spectrum so there's no λ_{k+1}.
519
+ gap_magnitude = float(
520
+ (nontrivial_vals[picked_k] - nontrivial_vals[picked_k - 1]).item()
521
+ if picked_k < nontrivial_vals.shape[0]
522
+ else 0.0
523
+ )
524
+
525
+ coords = nontrivial_vecs[:, :picked_k].contiguous() # (K, picked_k)
526
+
527
+ kept = min(max_dim + 5, nontrivial_vals.shape[0])
528
+ diagnostics = SpectralDiagnostics(
529
+ eigenvalues=nontrivial_vals[:kept].detach().clone(),
530
+ picked_k=picked_k,
531
+ gap_index=picked_k,
532
+ gap_magnitude=gap_magnitude,
533
+ bandwidth=bandwidth,
534
+ k_nn=k_nn,
535
+ component_count=1,
536
+ heuristic_k=heuristic_k,
537
+ min_dim=int(min_dim) if min_dim is not None else None,
538
+ pinned=pinned,
539
+ )
540
+ return coords, diagnostics
541
+
542
+
543
+ def discover_coords(
544
+ gram: torch.Tensor,
545
+ method: str,
546
+ **kwargs: Any,
547
+ ) -> tuple[torch.Tensor, PcaDiagnostics | SpectralDiagnostics]:
548
+ """Dispatch to :func:`derive_pca_coords` or :func:`derive_spectral_coords`.
549
+
550
+ Exists so the pipeline doesn't branch on method strings; downstream
551
+ code builds the consensus ``(K, K)`` Gram once and calls
552
+ ``discover_coords(gram, method, **fit_kwargs)``, and the diagnostics
553
+ ride into the sidecar through the same seam regardless of method. Both
554
+ methods embed the *same* layer-averaged whitened Gram — PCA linearly
555
+ (eigendecomposition), spectral locally (k-NN Laplacian eigenmaps over
556
+ the distances read off it).
557
+ """
558
+ if method == "pca":
559
+ return derive_pca_coords(gram, **kwargs)
560
+ if method == "spectral":
561
+ return derive_spectral_coords(gram, **kwargs)
562
+ raise ValueError(
563
+ f"unknown discover method {method!r} (expected 'pca' | 'spectral')"
564
+ )
565
+
566
+
567
+ # ===================================================== topology selection ===
568
+ #
569
+ # ``fit_mode="auto"`` picks the discover geometry instead of the user declaring
570
+ # it, in two decoupled decisions (the decoupling is what dodges the dimension
571
+ # bias that sinks naive reconstruction scoring — more spectral coordinates
572
+ # always "fit" better, so a single score always crowns the highest-dim
573
+ # candidate):
574
+ #
575
+ # (a) flat vs curved — compare the flat affine (``pca``) and curved RBF
576
+ # (``spectral``) fits, each at its own intrinsic dim, by GCV in a shared
577
+ # whitened-reduced metric. GCV's effective-dof penalty makes this a fair
578
+ # model selection rather than a coordinate-count race.
579
+ # (b) periodic axes — bounded H1 evidence proposes a chart, followed by
580
+ # MDS/spectral coordinate checks and a mean-only prediction baseline.
581
+ # This is not a classifier for arbitrary topology; H1 can contain
582
+ # sampling holes on spheres and does not determine a surface's type.
583
+ #
584
+ # Spheres are authored-only — ``S^n`` is a speculative topology that's the least
585
+ # reliable to detect from few centroids, so it is not an auto candidate.
586
+
587
+
588
+ @dataclass(frozen=True)
589
+ class TopologyCandidate:
590
+ """One scored candidate in :func:`select_topology`'s ranking."""
591
+
592
+ name: str # display name, e.g. "flat-pca", "spectral", "torus-T1"
593
+ fit_mode: str # resolved fit_mode: "pca" (flat) | "spectral" (curved)
594
+ intrinsic_dim: int
595
+ score: float # summed GCV (lower = better); inf if unscorable
596
+ viable: bool
597
+ reason: str = "" # why excluded / how chosen (e.g. periodic detection note)
598
+
599
+
600
+ @dataclass(frozen=True)
601
+ class TopologyChoice:
602
+ """The winning topology + the full ranked candidate field (for the sidecar)."""
603
+
604
+ winner_name: str
605
+ fit_mode: str # "pca" | "spectral"
606
+ coords: torch.Tensor # (K, n) winner intrinsic coords
607
+ domain: ManifoldDomain # CustomDomain | BoxDomain (periodic)
608
+ candidates: tuple[TopologyCandidate, ...] # ranked best-first
609
+ # The winner's coordinate diagnostics — ``PcaDiagnostics`` for a flat
610
+ # winner, ``SpectralDiagnostics`` for a curved / periodic one (the
611
+ # Laplacian eigenpairs the spectral/periodic embedding rode), or ``None``
612
+ # when unavailable. Lets an ``auto`` fit emit a diagnostics block so the
613
+ # inspector renders the same bars a pinned ``pca``/``spectral`` fit does.
614
+ diagnostics: "PcaDiagnostics | SpectralDiagnostics | None" = None
615
+ # Curved winner's already-normalized, factorized layout plan. Auto-mode
616
+ # spends this work while scoring the candidate; the final per-layer fit can
617
+ # reuse it instead of repeating QR/eigh/LU over the same node geometry.
618
+ rbf_plan: "RbfFitPlan | None" = None
619
+ fisher_bases: "dict[int, torch.Tensor]" = field(default_factory=dict)
620
+
621
+
622
+ def _gcv_value(rss: float, edf: float, K: int) -> float:
623
+ """The GCV score ``K · RSS / (K − edf)²`` — RSS penalized by effective dof.
624
+
625
+ The ``(K − edf)²`` denominator is what gives model selection its parsimony:
626
+ a higher-dimensional / more flexible candidate drives ``edf`` up, shrinking
627
+ the denominator and *raising* its GCV unless the extra flexibility buys a
628
+ commensurate RSS drop. This is what stops a naive reconstruction error
629
+ from always preferring the highest-dimensional topology. ``edf ≥ K``
630
+ (a saturated fit) returns ``+inf`` — it explains nothing out of sample.
631
+ """
632
+ slack = K - edf
633
+ if slack <= 0.0:
634
+ return math.inf
635
+ return K * rss / (slack * slack)
636
+
637
+
638
+ def _ols_gcv_score(coords: torch.Tensor, targets: dict[int, torch.Tensor]) -> float:
639
+ """Summed GCV of the affine (flat) map ``coords → target``.
640
+
641
+ The flat candidate's surface is affine in its coordinates; its hat is the
642
+ OLS projection ``H = C̃ (C̃ᵀC̃)⁺ C̃ᵀ`` (``C̃ = [1|coords]``) with effective
643
+ dof ``edf = tr H = rank(C̃) = dim+1``. Per-layer GCV (:func:`_gcv_value`)
644
+ summed over layers — comparable to the curved candidates' GCV.
645
+ """
646
+ K = coords.shape[0]
647
+ C = torch.cat([torch.ones(K, 1, dtype=torch.float32), coords.to(torch.float32)], dim=1)
648
+ H = C @ torch.linalg.pinv(C.transpose(0, 1) @ C) @ C.transpose(0, 1) # (K, K)
649
+ edf = float(H.diagonal().sum().item())
650
+ total = 0.0
651
+ for y in targets.values():
652
+ rss = float((y - H @ y).pow(2).sum().item())
653
+ total += _gcv_value(rss, edf, K)
654
+ return total
655
+
656
+
657
+ def _rbf_gcv_score(
658
+ node_params: torch.Tensor,
659
+ targets: dict[int, torch.Tensor],
660
+ *,
661
+ smoothing: float | str,
662
+ plan: RbfFitPlan | None = None,
663
+ ) -> float:
664
+ """Summed GCV of the penalized RBF surface over a layout.
665
+
666
+ The curved candidates (``spectral`` / ``S^n`` / ``T^d``) fit an ``r**3``
667
+ RBF to the (unit-box-normalized) embedded coordinates. Each layer's ``λ``
668
+ is GCV-selected (:func:`_gcv_select_lambda` returns its GCV directly), and
669
+ the score sums those per-layer GCVs — so every candidate is judged at its
670
+ own best smoothing, the standard model-selection comparison. Raises
671
+ (propagating poisedness) if the layout can't carry an RBF — the caller
672
+ marks that candidate non-viable.
673
+ """
674
+ _rbf_poised(node_params)
675
+ lo = node_params.min(dim=0).values
676
+ hi = node_params.max(dim=0).values
677
+ norm = (node_params - lo) / (hi - lo).clamp(min=1e-9)
678
+ K = norm.shape[0]
679
+ plan = plan or prepare_rbf_fit_plan(norm, smoothing=smoothing)
680
+ E, Q = plan.E, plan.Q
681
+ fixed_smoother: torch.Tensor | None = None
682
+ fixed_edf = 0.0
683
+ if smoothing != "auto":
684
+ denom_e = K * K - K
685
+ e_scale = float(E.abs().sum() / denom_e) if denom_e > 0 else 1.0
686
+ lam = float(smoothing) * (e_scale if e_scale > 0.0 else 1.0)
687
+ fixed_smoother = _rbf_smoother_matrix(E, Q, lam)
688
+ fixed_edf = float(fixed_smoother.diagonal().sum().item())
689
+ total = 0.0
690
+ for y in targets.values():
691
+ if smoothing == "auto":
692
+ _lam, _edf, gcv = _gcv_select_lambda(E, Q, y, plan=plan)
693
+ else:
694
+ assert fixed_smoother is not None
695
+ rss = float((y - fixed_smoother @ y).pow(2).sum().item())
696
+ gcv = _gcv_value(rss, fixed_edf, K)
697
+ total += gcv
698
+ return total
699
+
700
+
701
+ _HARMONIC_COHERENCE = 0.80 # |⟨z_new, z_acc^m⟩| above this ⇒ a harmonic, not a new axis
702
+ _HARMONIC_MAX_ORDER = 5
703
+
704
+
705
+ def _is_angular_harmonic(theta_new: torch.Tensor, accepted: list[torch.Tensor]) -> bool:
706
+ """True if ``theta_new`` is an integer harmonic of an already-accepted angle.
707
+
708
+ A single circle's Laplacian spectrum is ``cos kθ, sin kθ`` for ``k = 1, 2,
709
+ …`` — every harmonic pair looks like a clean circle, so a naive scan would
710
+ count one circle as a high-dimensional torus. Two angles are the *same*
711
+ circular axis when one is an integer multiple of the other: in the complex
712
+ phase ``z = e^{iθ}``, ``θ_new = m·θ_acc`` ⇔ ``z_new = z_acc^m`` ⇔ the
713
+ coherence ``|mean(z_new · conj(z_acc^m))| ≈ 1``. Checks ``m = 1…5`` and the
714
+ conjugate (opposite winding); a hit means ``theta_new`` is a harmonic of an
715
+ existing fundamental and is *not* an independent periodic axis.
716
+ """
717
+ z_new = torch.polar(torch.ones_like(theta_new), theta_new) # e^{iθ_new}
718
+ for theta_acc in accepted:
719
+ z_acc = torch.polar(torch.ones_like(theta_acc), theta_acc)
720
+ for m in range(1, _HARMONIC_MAX_ORDER + 1):
721
+ zm = z_acc.pow(m)
722
+ coh = (z_new * zm.conj()).mean().abs()
723
+ coh_conj = (z_new * zm).mean().abs() # opposite winding
724
+ if float(torch.maximum(coh, coh_conj).item()) >= _HARMONIC_COHERENCE:
725
+ return True
726
+ return False
727
+
728
+
729
+ def _validated_topology_distances(distances: torch.Tensor) -> torch.Tensor:
730
+ if distances.ndim != 2 or distances.shape[0] != distances.shape[1] or not len(distances):
731
+ raise ValueError("topology distances must be a non-empty square matrix")
732
+ if not distances.is_floating_point() or not torch.isfinite(distances).all() or bool((distances < 0).any()):
733
+ raise ValueError("topology distances must be finite and non-negative")
734
+ squared = distances.double().square()
735
+ tolerance = 8 * torch.finfo(distances.dtype).eps * float(squared.max())
736
+ if bool(((squared - squared.T).abs() > tolerance).any()):
737
+ raise ValueError("topology distance matrix must be symmetric")
738
+ if bool((squared.diagonal() > tolerance).any()):
739
+ raise ValueError("topology distance diagonal must be zero")
740
+ result = (distances + distances.T) * 0.5
741
+ result.fill_diagonal_(0)
742
+ return result
743
+
744
+
745
+ def _rips_h1_persistence(
746
+ distances: torch.Tensor,
747
+ eps_max: float,
748
+ *,
749
+ max_triangles: int = 500_000,
750
+ ) -> list[tuple[float, float]]:
751
+ """H1 birth/death pairs from exact GF(2) boundary-matrix reduction.
752
+
753
+ Infinite death means right-censored at the filtration ceiling, not a
754
+ proven essential cycle of the underlying manifold. Finite bars can carry
755
+ real structure too. Only edges and triangles up to eps_max are reduced.
756
+ Exceeding the triangle budget raises instead of truncating away boundaries
757
+ and manufacturing cycles. Reduction uses CPU integer bitsets.
758
+ """
759
+ distances = _validated_topology_distances(distances)
760
+ if not math.isfinite(eps_max) or eps_max < 0 or max_triangles < 0:
761
+ raise ValueError("persistence ceiling and triangle budget must be non-negative and finite")
762
+ distances = distances.detach().cpu()
763
+ K = int(distances.shape[0])
764
+ # Edges with length <= eps_max, in a total filtration order.
765
+ iu = torch.triu_indices(K, K, offset=1)
766
+ lens_all = distances[iu[0], iu[1]]
767
+ keep = lens_all <= eps_max
768
+ ei = iu[0][keep].tolist()
769
+ ej = iu[1][keep].tolist()
770
+ el = lens_all[keep].tolist()
771
+ order = sorted(range(len(el)), key=lambda x: (el[x], ei[x], ej[x]))
772
+ ei = [ei[o] for o in order]
773
+ ej = [ej[o] for o in order]
774
+ el = [el[o] for o in order]
775
+ E = len(el)
776
+ edge_id: dict[tuple[int, int], int] = {(ei[x], ej[x]): x for x in range(E)}
777
+ # Global simplex indices: vertices 0..K-1, edges K..K+E-1 (in filtration
778
+ # order, so edge global index already respects the filtration).
779
+ def eg(idx: int) -> int:
780
+ return K + idx
781
+ adj: list[set[int]] = [set() for _ in range(K)]
782
+ for x in range(E):
783
+ adj[ei[x]].add(ej[x])
784
+ adj[ej[x]].add(ei[x])
785
+ # Triangles: i<j<k with all three edges present; filtration = longest edge.
786
+ dist = distances
787
+ triangles: list[tuple[float, int, int, int]] = []
788
+ for x in range(E):
789
+ i, j = ei[x], ej[x]
790
+ for k in adj[i] & adj[j]:
791
+ if k > j: # canonical i<j<k → each triangle once
792
+ fl = max(el[x], float(dist[i, k].item()), float(dist[j, k].item()))
793
+ if len(triangles) == max_triangles:
794
+ raise ValueError("persistent-homology triangle budget exceeded; topology is unresolved")
795
+ triangles.append((fl, i, j, k))
796
+ triangles.sort(key=lambda t: (t[0], t[1], t[2], t[3]))
797
+
798
+ # Reduction. Vertices and edges first (H0); their positive (cycle-creating)
799
+ # edges are found by union-find — equivalent to reducing the edge columns
800
+ # over vertex rows, and cheaper.
801
+ dsu = _DSU(K)
802
+ positive_edges: set[int] = set() # edge filtration indices that create a 1-cycle
803
+ for x in range(E):
804
+ # A tree edge kills an H0 component; a redundant edge births a loop.
805
+ if not dsu.union(ei[x], ej[x]):
806
+ positive_edges.add(x)
807
+
808
+ # H1 deaths: reduce triangle columns over edge rows (global edge indices, so
809
+ # the pivot respects filtration order). A reduced low on a positive edge
810
+ # pairs that loop's birth with this triangle's death.
811
+ # Python integers are compact C-level bitsets over edge rows. Symmetric
812
+ # difference becomes one XOR and the pivot is ``bit_length()-1``; this is
813
+ # the identical GF(2) reduction without allocating/copying Python sets for
814
+ # every triangle column on dense auto-topology heaps.
815
+ low_inv: dict[int, int] = {} # pivot edge-global → reduced bit column
816
+ killed: dict[int, float] = {} # edge filtration index → death filtration
817
+ for fl, i, j, k in triangles:
818
+ col = (
819
+ (1 << eg(edge_id[(min(i, j), max(i, j))]))
820
+ | (1 << eg(edge_id[(min(i, k), max(i, k))]))
821
+ | (1 << eg(edge_id[(min(j, k), max(j, k))]))
822
+ )
823
+ while col:
824
+ piv = col.bit_length() - 1
825
+ if piv in low_inv:
826
+ col ^= low_inv[piv]
827
+ else:
828
+ break
829
+ if not col:
830
+ continue # triangle creates an H2 void — irrelevant to H1
831
+ piv = col.bit_length() - 1
832
+ low_inv[piv] = col
833
+ edge_idx = piv - K
834
+ if edge_idx in positive_edges and edge_idx not in killed:
835
+ killed[edge_idx] = fl
836
+
837
+ pairs: list[tuple[float, float]] = []
838
+ for idx in positive_edges:
839
+ birth = el[idx]
840
+ death = killed.get(idx, math.inf)
841
+ pairs.append((birth, death))
842
+ return pairs
843
+
844
+
845
+ def _count_persistent_loops(
846
+ distances: torch.Tensor,
847
+ *,
848
+ persistence_frac: float = 0.5,
849
+ max_dim: int = 8,
850
+ ) -> int:
851
+ """Count candidate H1 bars in a bounded Rips window, not true Betti numbers.
852
+
853
+ The window ends at twice the longest MST edge. Count both censored and
854
+ finite bars with lifetime >= persistence_frac times their birth scale and
855
+ the connectivity scale. Death must also clear an interior connectivity
856
+ margin, excluding short parallel loops before a rectangular grid connects.
857
+ This recovers smaller finite cycles in some anisotropic tori.
858
+
859
+ The thresholds are heuristics. Sampling holes on a sphere can pass them;
860
+ H1 alone does not distinguish a circle, cylinder, or Mobius strip. Chart
861
+ validation is mandatory, and unresolved results are not evidence of
862
+ trivial topology. Inputs and compute budgets are validated explicitly.
863
+ """
864
+ distances = _validated_topology_distances(distances)
865
+ if not math.isfinite(persistence_frac) or persistence_frac < 0 or max_dim < 1:
866
+ raise ValueError("persistence fraction must be finite and non-negative; max_dim must be positive")
867
+ K = int(distances.shape[0])
868
+ if K > 128:
869
+ raise ValueError("persistent topology supports at most 128 nodes; topology is unresolved")
870
+ if K < 4:
871
+ return 0
872
+ # Connectivity scale ε_c: the largest edge in the MST (Kruskal).
873
+ iu = torch.triu_indices(K, K, offset=1)
874
+ lens = distances[iu[0], iu[1]]
875
+ order = torch.argsort(lens)
876
+ dsu = _DSU(K)
877
+ eps_c = 0.0
878
+ joined = 0
879
+ src, dst = iu[0].tolist(), iu[1].tolist()
880
+ for o in order.tolist():
881
+ if dsu.union(src[o], dst[o]):
882
+ eps_c = float(lens[o].item())
883
+ joined += 1
884
+ if joined == K - 1:
885
+ break
886
+ if eps_c <= 0.0:
887
+ return 0
888
+ eps_max = 2.0 * eps_c
889
+ # At the ceiling a complete Rips graph has the full simplex as its clique
890
+ # complex, hence trivial H1. This is exactly the outlier-inflated personas
891
+ # case and lets the loop *counter* skip constructing/reducing O(K^3)
892
+ # triangles. ``_rips_h1_persistence`` itself keeps its full finite-pair
893
+ # contract; this shortcut is valid here because the caller only counts
894
+ # classes still essential at ``eps_max``.
895
+ if bool((lens <= eps_max).all()):
896
+ return 0
897
+ pairs = _rips_h1_persistence(distances, eps_max)
898
+ threshold = persistence_frac * eps_c
899
+ count = 0
900
+ for birth, death in pairs:
901
+ # A thin torus's smaller cycle can die inside the window. Death is
902
+ # not evidence that the cycle was noise: evaluate its lifetime too.
903
+ if (death >= (1.0 + 0.5 * persistence_frac) * eps_c
904
+ and (min(death, eps_max) - birth) >= max(threshold, persistence_frac * birth)):
905
+ count += 1
906
+ return min(count, max_dim)
907
+
908
+
909
+ # Single-cycle fallback thresholds (complement H1 persistence, which only counts
910
+ # *fat* loops — a thin ring slips under it). The detector covers two sampling
911
+ # regimes: a **uniform** ring (near-equidistant nodes — a faint thin loop) and a
912
+ # **clustered** ring (tight clumps spaced around the loop — the seasonal sampling
913
+ # real concept families have: months→seasons, days→weekday/weekend). Validated
914
+ # for sensitivity (synthetic faint + clustered rings, real day-of-week centroids)
915
+ # and specificity (~0% false-positive on random Gaussian heaps K>=9; lines, open
916
+ # arcs, grids, branched theta/Y, and high-D persona-style fans all rejected).
917
+ _CYCLE_MIN_NODES = 7 # below this, too few points to detect a ring reliably
918
+ _CYCLE_MAX_NODES = 128 # above this, robust loops are visible to H1; cap the tour
919
+ _CYCLE_MAX_DEGREE = 3 # uniform path: symmetric 2-NN degree of a 1-D loop
920
+ _CYCLE_CLOSURE_MAX = 2.0 # uniform path: max/median tour edge (no single long edge)
921
+ _CYCLE_RECALL_MIN = 0.90 # uniform path: tour-neighbour top-2 recall (loop is local)
922
+ _CYCLE_CONTRAST_MIN = 1.08 # mean d(sep=2)/d(sep=1): genuine cyclic growth (both paths)
923
+ # Clustered-ring path: tight clumps make the tour edges *bimodal* (tiny intra-
924
+ # cluster, large inter-cluster), so closure/recall fail — but the inter-cluster
925
+ # gaps are >=2, mutually regular, and the loop has a real far antipode. These
926
+ # reject what would otherwise leak in: an open arc (exactly 1 gap), a blob/fan (no
927
+ # antipode), and a 2-D grid (gaps are noise-marginal, not decisively bimodal).
928
+ _CYCLE_MAX_DEGREE_CLUSTER = 4 # a tight clump pushes a node to degree 4 (vs 1-D's 2-3)
929
+ _CYCLE_LARGE_FACTOR = 2.5 # a tour edge is an inter-cluster gap past this x small-scale
930
+ _CYCLE_GAPS_MIN = 2 # >=2 regular gaps => a closed cycle of clumps (1 => arc)
931
+ _CYCLE_GAP_REG_MAX = 2.5 # the gaps must be mutually comparable (an even-ish ring)
932
+ _CYCLE_ANTIPODE_MIN = 2.5 # tour-antipode/tour-neighbour distance: a ring has a far side
933
+ _CYCLE_BIMODAL_MIN = 3.5 # the *smallest* gap must clear this x small-scale: a clustered
934
+ # ring is *decisively* bimodal (tight clumps, big gaps, no edge
935
+ # in between), screening a diffuse low-D cloud whose many
936
+ # accidental long edges only marginally clear _CYCLE_LARGE_FACTOR
937
+
938
+
939
+ def _nn_tour(dist: torch.Tensor) -> list[int]:
940
+ """Greedy nearest-neighbour tour (best of all starts) + 2-opt — a cheap,
941
+ deterministic TSP heuristic recovering a candidate cyclic order over the K
942
+ centroids. ``O(K^3)``; the caller gates ``K <= _CYCLE_MAX_NODES``."""
943
+ D = dist.tolist()
944
+ K = len(D)
945
+ best_tour: list[int] | None = None
946
+ best_len = math.inf
947
+ for start in range(K):
948
+ unvisited = set(range(K))
949
+ unvisited.discard(start)
950
+ tour = [start]
951
+ while unvisited:
952
+ row = D[tour[-1]]
953
+ nxt = min(unvisited, key=row.__getitem__)
954
+ tour.append(nxt)
955
+ unvisited.discard(nxt)
956
+ length = sum(D[tour[i]][tour[(i + 1) % K]] for i in range(K))
957
+ if length < best_len:
958
+ best_len, best_tour = length, tour
959
+ tour = best_tour if best_tour is not None else list(range(K))
960
+ improved = True
961
+ while improved:
962
+ improved = False
963
+ for i in range(K - 1):
964
+ for j in range(i + 2, K):
965
+ if i == 0 and j == K - 1:
966
+ continue # the closing edge — reversing it is a no-op
967
+ a, b = tour[i], tour[i + 1]
968
+ c, e = tour[j], tour[(j + 1) % K]
969
+ if D[a][b] + D[c][e] > D[a][c] + D[b][e] + 1e-9:
970
+ tour[i + 1:j + 1] = tour[i + 1:j + 1][::-1]
971
+ improved = True
972
+ return tour
973
+
974
+
975
+ def _faint_cycle_coords(distances: torch.Tensor) -> torch.Tensor | None:
976
+ """Recover a single periodic coordinate for an ``S^1`` that H1 misses.
977
+
978
+ Vietoris–Rips H1 persistence counts loops by *hole size*, so two kinds of
979
+ ring slip under its threshold even though both are unambiguously cyclic by
980
+ graph topology. This complementary test runs only when persistence found
981
+ nothing, and recovers the cyclic order in either of two sampling regimes:
982
+
983
+ **Uniform** — a faint ring: a small cyclic modulation on a near-equidistant
984
+ heap (e.g. day-of-week centroids at ~16% modulation). Too thin a hole for
985
+ H1, but near-equidistant, so the original guards fire: **1-D** (symmetric
986
+ 2-NN max degree ``<= _CYCLE_MAX_DEGREE``), **closed** (greedy+2-opt tour
987
+ edges near-uniform, ``max/median < _CYCLE_CLOSURE_MAX`` — a line/arc needs
988
+ one long closing edge), and **local** (each node's two tour-neighbours among
989
+ its two nearest, ``recall >= _CYCLE_RECALL_MIN``).
990
+
991
+ **Clustered** — tight clumps spaced around the loop, the sampling real
992
+ concept families have (months→seasons, days→weekday/weekend). Here the tour
993
+ edges are **bimodal** (tiny intra-cluster, large inter-cluster), so closure
994
+ and recall both fail though the loop is real. It is accepted when the
995
+ inter-cluster gaps (edges ``> _CYCLE_LARGE_FACTOR`` × the small-edge scale)
996
+ are (a) ``>= _CYCLE_GAPS_MIN`` in number, (b) mutually regular
997
+ (``max/min <= _CYCLE_GAP_REG_MAX``), and (c) the loop has a real far antipode
998
+ (tour-antipode/tour-neighbour mean distance ``>= _CYCLE_ANTIPODE_MIN``).
999
+ Each guard rejects a distinct impostor that the bimodality alone would admit:
1000
+ an **open arc** has exactly one large edge (its closing chord), a **blob/fan**
1001
+ has no antipode, and a **2-D grid**'s gaps are noise-marginal — not decisively
1002
+ bimodal — so they don't clear ``_CYCLE_LARGE_FACTOR``. Both regimes also
1003
+ require **graded** growth (mean ``d(sep=2)/d(sep=1) >= _CYCLE_CONTRAST_MIN``)
1004
+ and ``1-D``-ness (degree ``<= _CYCLE_MAX_DEGREE_CLUSTER``, looser than the
1005
+ uniform path's ``_CYCLE_MAX_DEGREE`` because a tight clump reaches degree 4).
1006
+
1007
+ Returns the per-node angle ``(K,)`` = ``2*pi*tour_rank/K`` (a uniform ``S^1``
1008
+ parameterisation in the recovered cyclic *order* — exact spacing is dropped,
1009
+ which is fine: the loop's topology, not its metric, is what the periodic
1010
+ domain needs), or ``None``. The clustered path's bimodal-gap test trades two
1011
+ documented false-negatives — a very-loose cluster heap approaching uniform,
1012
+ and an eccentric ellipse ``> 6:1`` (its gaps aren't decisively bimodal) — for
1013
+ a ~0% false-positive rate that holds against grids, fans, arcs and blobs.
1014
+ """
1015
+ K = int(distances.shape[0])
1016
+ if K < _CYCLE_MIN_NODES or K > _CYCLE_MAX_NODES:
1017
+ return None
1018
+ dist = distances.detach().to(torch.float32)
1019
+ # 1-D filter (shared, at the looser clustered bound): a high-D fan has degree
1020
+ # >> 4 and is rejected before any tour runs; the uniform path tightens to 3.
1021
+ nn2 = dist.argsort(dim=1)[:, 1:3].tolist()
1022
+ deg: dict[int, int] = {}
1023
+ edges_uv: set[tuple[int, int]] = set()
1024
+ for i, row in enumerate(nn2):
1025
+ for j in row:
1026
+ edges_uv.add((i, j) if i < j else (j, i))
1027
+ for u, v in edges_uv:
1028
+ deg[u] = deg.get(u, 0) + 1
1029
+ deg[v] = deg.get(v, 0) + 1
1030
+ max_degree = max(deg.values())
1031
+ if max_degree > _CYCLE_MAX_DEGREE_CLUSTER:
1032
+ return None
1033
+ # Recover the cyclic order (shared by both regimes).
1034
+ tour = _nn_tour(dist)
1035
+ D = dist.tolist()
1036
+ pos = [0] * K
1037
+ for i, n in enumerate(tour):
1038
+ pos[n] = i
1039
+ tour_edges = [D[tour[i]][tour[(i + 1) % K]] for i in range(K)]
1040
+ sorted_edges = sorted(tour_edges)
1041
+ median = (sorted_edges[K // 2] if K % 2
1042
+ else 0.5 * (sorted_edges[K // 2 - 1] + sorted_edges[K // 2]))
1043
+ if median <= 0.0:
1044
+ return None
1045
+ # graded (shared): mean distance grows from cyclic separation 1 to 2 — a real
1046
+ # cyclic order, not a flat simplex.
1047
+ s1 = s1n = s2 = s2n = 0.0
1048
+ for a in range(K):
1049
+ for b in range(a + 1, K):
1050
+ sep = min((pos[a] - pos[b]) % K, (pos[b] - pos[a]) % K)
1051
+ if sep == 1:
1052
+ s1 += D[a][b]
1053
+ s1n += 1
1054
+ elif sep == 2:
1055
+ s2 += D[a][b]
1056
+ s2n += 1
1057
+ if s1n == 0 or s2n == 0:
1058
+ return None
1059
+ if (s2 / s2n) / max(s1 / s1n, 1e-9) < _CYCLE_CONTRAST_MIN:
1060
+ return None
1061
+ # Uniform regime: near-equidistant nodes — tight closure + local recall.
1062
+ nn2_set = [set(row) for row in nn2]
1063
+ hits = sum(len({tour[(i - 1) % K], tour[(i + 1) % K]} & nn2_set[n])
1064
+ for i, n in enumerate(tour))
1065
+ recall = hits / (2 * K)
1066
+ closure = sorted_edges[-1] / median
1067
+ uniform_ok = (max_degree <= _CYCLE_MAX_DEGREE
1068
+ and closure < _CYCLE_CLOSURE_MAX
1069
+ and recall >= _CYCLE_RECALL_MIN)
1070
+ # Clustered regime: >=2 regular, decisively-bimodal inter-cluster gaps + a
1071
+ # real far antipode. The bimodality strength (smallest gap >> small scale) is
1072
+ # what screens a diffuse low-D random cloud, whose tour throws off many edges
1073
+ # that only marginally clear _CYCLE_LARGE_FACTOR.
1074
+ small = sorted_edges[:max(1, K // 2)]
1075
+ small_scale = small[len(small) // 2]
1076
+ gaps = [e for e in tour_edges if e > _CYCLE_LARGE_FACTOR * small_scale]
1077
+ half = K // 2
1078
+ near = sum(tour_edges) / K
1079
+ far = sum(D[tour[i]][tour[(i + half) % K]] for i in range(K)) / K
1080
+ antipode = far / max(near, 1e-9)
1081
+ clustered_ok = (len(gaps) >= _CYCLE_GAPS_MIN
1082
+ and min(gaps) >= _CYCLE_BIMODAL_MIN * small_scale
1083
+ and max(gaps) / min(gaps) <= _CYCLE_GAP_REG_MAX
1084
+ and antipode >= _CYCLE_ANTIPODE_MIN)
1085
+ if not (uniform_ok or clustered_ok):
1086
+ return None
1087
+ # Uniform S^1 coordinate in the recovered cyclic order.
1088
+ angles = torch.zeros(K, dtype=torch.float32)
1089
+ for i, n in enumerate(tour):
1090
+ angles[n] = 2.0 * math.pi * i / K
1091
+ return angles
1092
+
1093
+
1094
+ def _untangle_periodic_eigenspaces(eigenvalues: torch.Tensor, eigenvectors: torch.Tensor) -> torch.Tensor:
1095
+ """Resolve mixed circle factors inside repeated Laplacian eigenspaces.
1096
+
1097
+ On an equal-radius product torus, arbitrary eigensolver rotations mix the
1098
+ sine/cosine planes. Constant quadratic forms identify the factor planes;
1099
+ pairing arbitrary columns directly can fold the torus onto itself.
1100
+ """
1101
+ result = eigenvectors.clone()
1102
+ start = 0
1103
+ while start < len(eigenvalues):
1104
+ end = start + 1
1105
+ while end < len(eigenvalues) and float(eigenvalues[end]) <= 1.05 * float(eigenvalues[start]):
1106
+ end += 1
1107
+ width = end - start
1108
+ if 4 <= width <= 8 and width % 2 == 0:
1109
+ block = eigenvectors[:, start:end].double()
1110
+ pairs = [(i, j) for i in range(width) for j in range(i, width)]
1111
+ products = torch.stack([block[:, i] * block[:, j] * (1 if i == j else math.sqrt(2))
1112
+ for i, j in pairs], dim=1)
1113
+ products -= products.mean(dim=0)
1114
+ values, vectors = torch.linalg.eigh(products.T @ products)
1115
+ for column in range(len(values)):
1116
+ if float(values[column]) > 1e-4 * float(values[-1]):
1117
+ break
1118
+ quadratic = torch.zeros(width, width, dtype=torch.float64)
1119
+ for index, (i, j) in enumerate(pairs):
1120
+ quadratic[i, j] = quadratic[j, i] = vectors[index, column] / (1 if i == j else math.sqrt(2))
1121
+ spectrum, rotation = torch.linalg.eigh(quadratic)
1122
+ spread = float(spectrum[-1] - spectrum[0])
1123
+ if spread < 1e-5:
1124
+ continue
1125
+ within = max(float(spectrum[i + 1] - spectrum[i]) for i in range(0, width, 2))
1126
+ between = min(float(spectrum[i + 1] - spectrum[i]) for i in range(1, width - 1, 2))
1127
+ if within < 0.1 * spread and between > 0.1 * spread:
1128
+ result[:, start:end] = (block @ rotation).to(result.dtype)
1129
+ break
1130
+ start = end
1131
+ return result
1132
+
1133
+
1134
+ def _periodic_neighborhoods_preserved(distances: torch.Tensor, angles: torch.Tensor) -> bool:
1135
+ """Reject a periodic chart that glues distant sampled points together.
1136
+
1137
+ This is a necessary local consistency check, not a homeomorphism proof.
1138
+ In particular, H1 alone cannot distinguish a circle from a strip.
1139
+ """
1140
+ K, dimensions = angles.shape
1141
+ if not torch.isfinite(angles).all() or K < 7:
1142
+ return False
1143
+ for axis in angles.T:
1144
+ ordered = torch.sort(axis.remainder(2 * math.pi)).values
1145
+ gaps = torch.diff(torch.cat((ordered, ordered[:1] + 2 * math.pi)))
1146
+ if float(gaps.max()) >= math.pi:
1147
+ return False
1148
+ source = distances.clone()
1149
+ source.fill_diagonal_(math.inf)
1150
+ if float(source.min()) <= 1e-7 * float(distances.max()):
1151
+ return False
1152
+ embedded = torch.cat((angles.cos(), angles.sin()), dim=1).double()
1153
+ mapped = torch.cdist(embedded, embedded)
1154
+ mapped.fill_diagonal_(math.inf)
1155
+ neighbors = min(2 * dimensions, K - 1)
1156
+ local_radius = source.sort(dim=1).values[:, neighbors - 1]
1157
+ mapped_radius = mapped.sort(dim=1).values[:, neighbors - 1]
1158
+ # Include ties: otherwise an eigensolver or row permutation can hide a fold.
1159
+ near = mapped <= mapped_radius[:, None] + 1e-6
1160
+ false_neighbors = near & (source > 2.0 * local_radius[:, None])
1161
+ collapsed = (mapped < 0.2 * math.pi / K ** (1 / dimensions)) & (source > local_radius[:, None])
1162
+ false_neighbors |= collapsed
1163
+ return float(false_neighbors.any(dim=1).float().mean()) <= 0.1
1164
+
1165
+
1166
+ def _distance_eigensystem(distances: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
1167
+ squared = distances.double().square()
1168
+ gram = -0.5 * (squared - squared.mean(0) - squared.mean(1, keepdim=True) + squared.mean())
1169
+ return torch.linalg.eigh(gram)
1170
+
1171
+
1172
+ def _detect_periodic_axes(
1173
+ distances: torch.Tensor,
1174
+ eigvecs: torch.Tensor,
1175
+ *,
1176
+ max_dim: int,
1177
+ persistence_frac: float = 0.5,
1178
+ eigenvalues: torch.Tensor | None = None,
1179
+ ) -> tuple[torch.Tensor, int] | None:
1180
+ """Propose a periodic chart, or abstain; this is not arbitrary topology detection.
1181
+
1182
+ H1 bars propose the number of axes. A distance-MDS span with constant
1183
+ quadratic forms separates linear product-circle factors without depending
1184
+ on arbitrary eigensolver rotations. A Laplacian fallback requires
1185
+ nonvanishing, approximately circular eigenpairs and rejects harmonics.
1186
+ Ambient-rank, phase-coverage, and neighborhood/collapse checks must pass.
1187
+
1188
+ The guarded faint-ring tour fallback is retained, with an ambient-rank
1189
+ check to reject lines. It remains heuristic and is not certified topology.
1190
+ None means unsupported or insufficient evidence, not that no loop exists.
1191
+ Spheres and nonorientable surfaces are not identified by this routine.
1192
+ """
1193
+ if len(distances) > 128:
1194
+ return None
1195
+ d = _count_persistent_loops(
1196
+ distances, persistence_frac=persistence_frac, max_dim=max_dim + 1,
1197
+ )
1198
+ if d > max_dim:
1199
+ return None
1200
+ if d < 1:
1201
+ # PH found no *fat* loop; try the faint single-cycle fallback before
1202
+ # conceding there is no periodicity.
1203
+ theta = _faint_cycle_coords(distances)
1204
+ if theta is None:
1205
+ return None
1206
+ values, _ = _distance_eigensystem(distances)
1207
+ if int((values > 1e-5 * values[-1]).sum()) < 2:
1208
+ return None
1209
+ return theta.unsqueeze(1), 1
1210
+ values, vectors = _distance_eigensystem(distances)
1211
+ # A compact boundaryless d-manifold cannot embed in R^d. Surface holes
1212
+ # must not turn a sphere in R^3 into a purported T3/T4 chart.
1213
+ if int((values > 1e-5 * values[-1]).sum()) <= d:
1214
+ return None
1215
+ if 2 * d < len(distances):
1216
+ if float(values[-2 * d]) > 1e-7 * float(values[-1]):
1217
+ block = vectors[:, -2 * d:]
1218
+ block = _untangle_periodic_eigenspaces(torch.ones(2 * d), block)
1219
+ coords = torch.atan2(block[:, 1::2], block[:, ::2]).float()
1220
+ if _periodic_neighborhoods_preserved(distances, coords):
1221
+ return coords, d
1222
+ if eigenvalues is not None:
1223
+ eigvecs = _untangle_periodic_eigenspaces(eigenvalues, eigvecs)
1224
+ avail = int(eigvecs.shape[1])
1225
+ angles: list[torch.Tensor] = []
1226
+ p = 0
1227
+ # Take the d lowest *independent* eigenpair angles (skip harmonics of an
1228
+ # axis already taken); PH count d is the ground truth for how many.
1229
+ while p * 2 + 1 < avail and len(angles) < d:
1230
+ cos_j = eigvecs[:, 2 * p]
1231
+ sin_j = eigvecs[:, 2 * p + 1]
1232
+ radius_squared = cos_j.square() + sin_j.square()
1233
+ mean_radius = float(radius_squared.mean())
1234
+ if (mean_radius <= 0 or float(radius_squared.min()) < 0.1 * mean_radius
1235
+ or float(radius_squared.std(correction=0)) > 0.35 * mean_radius):
1236
+ return None
1237
+ theta = torch.atan2(sin_j, cos_j)
1238
+ if not _is_angular_harmonic(theta, angles):
1239
+ angles.append(theta)
1240
+ p += 1
1241
+ if len(angles) != d:
1242
+ return None
1243
+ coords = torch.stack(angles, dim=1).contiguous()
1244
+ return (coords, d) if _periodic_neighborhoods_preserved(distances, coords) else None
1245
+
1246
+
1247
+ def select_topology(
1248
+ stacks: dict[int, torch.Tensor],
1249
+ layer_grams: dict[int, torch.Tensor],
1250
+ consensus_gram: torch.Tensor,
1251
+ *,
1252
+ whitener: "LayerWhitener",
1253
+ whitened_rows: dict[int, torch.Tensor] | None = None,
1254
+ max_dim: int = 8,
1255
+ smoothing: float | str = "auto",
1256
+ score_dim: int | None = None,
1257
+ k_nn: int | None = None,
1258
+ bandwidth: float | None = None,
1259
+ persistence_frac: float = 0.5,
1260
+ ) -> TopologyChoice:
1261
+ """Pick the manifold topology for a discover heap — flat vs curved vs periodic.
1262
+
1263
+ Two decisions, deliberately decoupled to avoid the dimension bias that
1264
+ sinks naive reconstruction scoring (where more spectral coordinates always
1265
+ "fit" better, so the highest-dim candidate always wins):
1266
+
1267
+ **(a) flat vs curved** — compare the flat affine fit (``pca`` mode, at its
1268
+ PCA variance-threshold dim) against the curved RBF fit (``spectral`` mode,
1269
+ floored to the *same* dim) by **GCV** in a shared per-layer whitened / Fisher
1270
+ reduced metric (``targets[L] = X̃_L · basis_Lᵀ``). GCV's ``(K − edf)²``
1271
+ denominator charges each mode for its own effective dof, so a more flexible
1272
+ candidate must earn its flexibility — a fair model-selection comparison
1273
+ rather than a coordinate-count race. The curved candidate's dim is floored
1274
+ to the flat candidate's (``min_dim=k_flat``) because the spectral
1275
+ eigenvalue-ratio cliff systematically *undershoots* (one dominant Fiedler
1276
+ mode picks ``k=1``); without the floor the curved fit competes starved of
1277
+ coordinates and a curved manifold linearly embedded in a ``k_flat``-plane is
1278
+ mislabelled flat — the flat affine fit reconstructs it near-perfectly while
1279
+ the under-dimensioned curved fit cannot, even though a dim-matched curved fit
1280
+ *wins*.
1281
+
1282
+ **(b) periodic axes** — independently, :func:`_detect_periodic_axes` proposes
1283
+ periodic charts from bounded Vietoris–Rips H1 evidence and checked MDS or
1284
+ spectral coordinates, with a guarded
1285
+ single-cycle fallback (:func:`_faint_cycle_coords`) for faint rings whose
1286
+ hole is too thin to clear the persistence threshold. Detected circles are
1287
+ *topology*, not surface shape — a circle can be linearly embedded yet still
1288
+ needs a periodic domain to steer around rather than across — so a confident
1289
+ candidate that also beats the mean-only GCV baseline routes to a periodic
1290
+ ``BoxDomain`` (the curved path). This is not a topology certificate. Spheres are
1291
+ **not** auto-selected (a speculative topology that's the least reliable to
1292
+ detect from few centroids); ``S^n`` is available as an *authored* domain only.
1293
+
1294
+ Returns a :class:`TopologyChoice` carrying the winner's ``fit_mode`` /
1295
+ ``coords`` / ``domain`` plus the ranked candidate field for the sidecar.
1296
+ ``whitener`` must cover every fit layer (the caller gates on
1297
+ ``covers_all``).
1298
+ """
1299
+ fit_layers = sorted(layer_grams.keys())
1300
+ K = consensus_gram.shape[0]
1301
+ R = score_dim if score_dim is not None else min(max_dim, K - 1)
1302
+ R = max(1, R)
1303
+
1304
+ # Shared per-layer whitened/Fisher reduced targets — every candidate
1305
+ # predicts the *same* y in the *same* metric, so the comparison isolates
1306
+ # the coordinate geometry + surface, not the basis (common-mode).
1307
+ targets: dict[int, torch.Tensor] = {}
1308
+ fisher_bases: dict[int, torch.Tensor] = {}
1309
+ for L in fit_layers:
1310
+ X = stacks[L].to(torch.float32)
1311
+ X = X - X.mean(dim=0, keepdim=True)
1312
+ basis, _ev = _pca_basis(
1313
+ X, n_components=R, whitener=whitener, layer=L,
1314
+ whitened_gram=layer_grams[L],
1315
+ whitened_rows=(
1316
+ whitened_rows.get(L) if whitened_rows is not None else None
1317
+ ),
1318
+ )
1319
+ fisher_bases[L] = basis
1320
+ targets[L] = (X @ basis.transpose(0, 1)).contiguous() # (K, R)
1321
+
1322
+ candidates: list[TopologyCandidate] = []
1323
+
1324
+ # (a) Flat (pca) — always viable for K >= 2.
1325
+ coords_flat, pca_diag = derive_pca_coords(consensus_gram, max_dim=max_dim)
1326
+ k_flat = int(coords_flat.shape[1])
1327
+ gcv_flat = _ols_gcv_score(coords_flat, targets)
1328
+ candidates.append(TopologyCandidate("flat-pca", "pca", k_flat, gcv_flat, True))
1329
+
1330
+ # (a) Curved Euclidean (spectral) — may fail on a tiny / disconnected heap.
1331
+ gcv_curved = math.inf
1332
+ curved: tuple[torch.Tensor, ManifoldDomain, RbfFitPlan] | None = None
1333
+ spec_diag: object | None = None
1334
+ spectral_eigen: tuple[torch.Tensor, torch.Tensor, int, float] | None = None
1335
+ try:
1336
+ # Floor the curved candidate's intrinsic dim to the flat PCA dim so flat
1337
+ # and curved are compared at *matched expressiveness*. The spectral
1338
+ # eigenvalue-ratio cliff systematically undershoots (its documented
1339
+ # failure mode: one dominant Fiedler mode makes λ₂/λ₁ the largest ratio,
1340
+ # picking k=1 regardless of the true geometry), which starves the curved
1341
+ # RBF of coordinates and biases the GCV comparison toward flat — a curved
1342
+ # manifold linearly embedded in a k_flat-plane is reconstructed near-
1343
+ # perfectly by the flat k_flat-affine fit, but the under-dimensioned curved
1344
+ # fit can't match it and loses on reconstruction it would *win* at matched
1345
+ # dim. ``min_dim`` is the floor :func:`derive_spectral_coords` already
1346
+ # carries for exactly this undershoot; wiring it into auto-mode is what
1347
+ # makes the flat-vs-curved verdict trustworthy rather than an artifact of
1348
+ # the dim mismatch. (The periodic path sets its own dim from the H1 loop
1349
+ # count, so it is unaffected.)
1350
+ spectral_eigen = _laplacian_eigen(
1351
+ consensus_gram, k_nn=k_nn, bandwidth=bandwidth,
1352
+ )
1353
+ coords_spec, spec_diag = derive_spectral_coords(
1354
+ consensus_gram, max_dim=max_dim, min_dim=k_flat,
1355
+ k_nn=k_nn, bandwidth=bandwidth,
1356
+ _eigen_result=spectral_eigen,
1357
+ )
1358
+ k_spec = int(coords_spec.shape[1])
1359
+ if (2 * k_spec + 1) > K:
1360
+ raise ValueError(f"poisedness floor 2n+1={2 * k_spec + 1} exceeds K={K}")
1361
+ spec_plan = prepare_rbf_fit_plan(
1362
+ coords_spec.to(torch.float32), smoothing=smoothing,
1363
+ )
1364
+ gcv_curved = _rbf_gcv_score(
1365
+ coords_spec.to(torch.float32), targets, smoothing=smoothing,
1366
+ plan=spec_plan,
1367
+ )
1368
+ curved = (coords_spec, CustomDomain(k_spec), spec_plan)
1369
+ candidates.append(TopologyCandidate("spectral", "spectral", k_spec, gcv_curved, True))
1370
+ except (ValueError, RuntimeError) as e: # _LinAlgError ⊂ RuntimeError
1371
+ candidates.append(TopologyCandidate(
1372
+ "spectral", "spectral", k_flat, math.inf, False, str(e),
1373
+ ))
1374
+
1375
+ # (b) Periodic (circle / torus) detection — persistent homology counts the
1376
+ # loops (ellipse/noise-robust), spectral eigenpairs coordinate them.
1377
+ periodic: tuple[
1378
+ torch.Tensor, ManifoldDomain, float, RbfFitPlan,
1379
+ ] | None = None
1380
+ try:
1381
+ if spectral_eigen is None:
1382
+ spectral_eigen = _laplacian_eigen(
1383
+ consensus_gram, k_nn=k_nn, bandwidth=bandwidth,
1384
+ )
1385
+ _vals, eigvecs, _knn, _bw = spectral_eigen
1386
+ # Whitened pairwise distances off the consensus Gram (same metric the
1387
+ # eigenmap embeds): d²_ij = G_ii + G_jj − 2 G_ij.
1388
+ cg = 0.5 * (consensus_gram + consensus_gram.transpose(0, 1))
1389
+ diag = cg.diagonal()
1390
+ d2 = diag.unsqueeze(0) + diag.unsqueeze(1) - 2.0 * cg
1391
+ distances = d2.clamp(min=0.0).sqrt()
1392
+ distances.fill_diagonal_(0.0)
1393
+ detected = _detect_periodic_axes(
1394
+ distances, eigvecs, max_dim=max_dim,
1395
+ persistence_frac=persistence_frac, eigenvalues=_vals,
1396
+ )
1397
+ if detected is not None:
1398
+ p_coords, n_loops = detected
1399
+ d = int(p_coords.shape[1])
1400
+ if (2 * d + 1) <= K:
1401
+ axes = [
1402
+ BoxAxis(f"theta{i}", periodic=True, period=2.0 * math.pi)
1403
+ for i in range(d)
1404
+ ]
1405
+ p_domain = BoxDomain(axes)
1406
+ p_params = p_domain.embed(p_coords).to(torch.float32)
1407
+ periodic_plan = prepare_rbf_fit_plan(
1408
+ p_params, smoothing=smoothing,
1409
+ )
1410
+ gcv_p = _rbf_gcv_score(
1411
+ p_params, targets, smoothing=smoothing,
1412
+ plan=periodic_plan,
1413
+ )
1414
+ null_score = sum(_gcv_value(float((y - y.mean(0)).square().sum()), 1.0, K)
1415
+ for y in targets.values())
1416
+ viable = math.isfinite(gcv_p) and null_score > 0 and gcv_p <= null_score
1417
+ note = (f"Periodic chart candidate; H1 evidence = {n_loops}; not a topology certificate"
1418
+ if viable else "Periodic fit does not improve on the mean-only prediction baseline")
1419
+ candidates.append(TopologyCandidate(
1420
+ f"torus-T{d}", "spectral", d, gcv_p, viable, note,
1421
+ ))
1422
+ if viable:
1423
+ periodic = (p_coords, p_domain, gcv_p, periodic_plan)
1424
+ except (ValueError, RuntimeError) as error:
1425
+ candidates.append(TopologyCandidate("periodic-unresolved", "spectral", k_flat, math.inf, False, str(error)))
1426
+
1427
+ # Supported periodic charts take priority over a linear approximation: a
1428
+ # circle also fits a plane, but the plane does not preserve its closure.
1429
+ # Geometry checks and the mean-only baseline above must both pass first.
1430
+ if periodic is not None and math.isfinite(periodic[2]):
1431
+ p_coords, p_domain, _gcv_p, winner_plan = periodic
1432
+ win_name = f"torus-T{int(p_coords.shape[1])}"
1433
+ win_mode, win_coords, win_domain = "spectral", p_coords, p_domain
1434
+ win_diag = spec_diag # periodic rides the spectral eigenpairs
1435
+ elif curved is not None and gcv_curved < gcv_flat:
1436
+ win_name = "spectral"
1437
+ win_mode, win_coords, win_domain = "spectral", curved[0], curved[1]
1438
+ win_diag = spec_diag
1439
+ winner_plan = curved[2]
1440
+ else:
1441
+ win_name = "flat-pca"
1442
+ win_mode, win_coords, win_domain = "pca", coords_flat, CustomDomain(k_flat)
1443
+ win_diag = pca_diag
1444
+ winner_plan = None
1445
+
1446
+ candidates.sort(key=lambda c: (not c.viable, c.score))
1447
+ return TopologyChoice(
1448
+ winner_name=win_name,
1449
+ fit_mode=win_mode,
1450
+ coords=win_coords,
1451
+ domain=win_domain,
1452
+ candidates=tuple(candidates),
1453
+ diagnostics=win_diag,
1454
+ rbf_plan=winner_plan,
1455
+ fisher_bases=fisher_bases,
1456
+ )
1457
+
1458
+
1459
+ __all__ = [
1460
+ "PcaDiagnostics",
1461
+ "SpectralDiagnostics",
1462
+ "TopologyCandidate",
1463
+ "TopologyChoice",
1464
+ "derive_pca_coords",
1465
+ "derive_spectral_coords",
1466
+ "discover_coords",
1467
+ "neutral_layout_coord",
1468
+ "select_topology",
1469
+ ]