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.
- drowse/__init__.py +312 -0
- drowse/__main__.py +5 -0
- drowse/cli/__init__.py +5 -0
- drowse/cli/alpha_grid.py +96 -0
- drowse/cli/config_file.py +340 -0
- drowse/cli/main.py +52 -0
- drowse/cli/parsers.py +1478 -0
- drowse/cli/runners/__init__.py +155 -0
- drowse/cli/runners/config.py +85 -0
- drowse/cli/runners/experiment.py +344 -0
- drowse/cli/runners/lens.py +661 -0
- drowse/cli/runners/manifold.py +1097 -0
- drowse/cli/runners/pack.py +413 -0
- drowse/cli/runners/sae.py +304 -0
- drowse/cli/runners/serve.py +220 -0
- drowse/cli/runners/shared.py +389 -0
- drowse/cli/runners/template.py +232 -0
- drowse/core/__init__.py +1 -0
- drowse/core/capture.py +1205 -0
- drowse/core/errors.py +260 -0
- drowse/core/events.py +156 -0
- drowse/core/extraction.py +3076 -0
- drowse/core/generation.py +1738 -0
- drowse/core/generation_finalizer.py +278 -0
- drowse/core/histogram.py +30 -0
- drowse/core/hooks.py +1885 -0
- drowse/core/instruments/__init__.py +49 -0
- drowse/core/instruments/geometry.py +551 -0
- drowse/core/instruments/lens.py +1341 -0
- drowse/core/instruments/protocol.py +211 -0
- drowse/core/instruments/sae.py +935 -0
- drowse/core/instruments/types.py +597 -0
- drowse/core/jlens.py +874 -0
- drowse/core/jlens_fit.py +1305 -0
- drowse/core/joint_logprobs.py +748 -0
- drowse/core/loom.py +1938 -0
- drowse/core/loom_diff.py +408 -0
- drowse/core/mahalanobis.py +579 -0
- drowse/core/manifold.py +4106 -0
- drowse/core/measurements.py +382 -0
- drowse/core/model.py +1724 -0
- drowse/core/monitor.py +2060 -0
- drowse/core/monitor_attach.py +682 -0
- drowse/core/naming.py +42 -0
- drowse/core/naturalness.py +187 -0
- drowse/core/profile.py +617 -0
- drowse/core/relp.py +313 -0
- drowse/core/results.py +467 -0
- drowse/core/role_templates.py +677 -0
- drowse/core/sae.py +891 -0
- drowse/core/sae_training.py +239 -0
- drowse/core/sampling.py +127 -0
- drowse/core/scene.py +810 -0
- drowse/core/scoring.py +270 -0
- drowse/core/session.py +11015 -0
- drowse/core/static_cache.py +316 -0
- drowse/core/steering.py +229 -0
- drowse/core/steering_composer.py +1135 -0
- drowse/core/steering_expr.py +1413 -0
- drowse/core/surface_topology.py +128 -0
- drowse/core/token_callback.py +52 -0
- drowse/core/token_payloads.py +244 -0
- drowse/core/topology.py +1469 -0
- drowse/core/transcript.py +582 -0
- drowse/core/tree_filter.py +353 -0
- drowse/core/triggers.py +250 -0
- drowse/data/baseline_prompts.json +50 -0
- drowse/data/manifolds/confident.uncertain/manifold.json +28 -0
- drowse/data/manifolds/confident.uncertain/nodes/00_confident.json +50 -0
- drowse/data/manifolds/confident.uncertain/nodes/01_uncertain.json +50 -0
- drowse/data/manifolds/creative.conventional/manifold.json +28 -0
- drowse/data/manifolds/creative.conventional/nodes/00_creative.json +50 -0
- drowse/data/manifolds/creative.conventional/nodes/01_conventional.json +50 -0
- drowse/data/manifolds/curious.disinterested/manifold.json +28 -0
- drowse/data/manifolds/curious.disinterested/nodes/00_curious.json +50 -0
- drowse/data/manifolds/curious.disinterested/nodes/01_disinterested.json +50 -0
- drowse/data/manifolds/direct.indirect/manifold.json +28 -0
- drowse/data/manifolds/direct.indirect/nodes/00_direct.json +50 -0
- drowse/data/manifolds/direct.indirect/nodes/01_indirect.json +50 -0
- drowse/data/manifolds/emotions/manifold.json +115 -0
- drowse/data/manifolds/emotions/nodes/00_excited.json +50 -0
- drowse/data/manifolds/emotions/nodes/01_happy.json +50 -0
- drowse/data/manifolds/emotions/nodes/02_calm.json +50 -0
- drowse/data/manifolds/emotions/nodes/03_weary.json +50 -0
- drowse/data/manifolds/emotions/nodes/04_sad.json +50 -0
- drowse/data/manifolds/emotions/nodes/05_distressed.json +50 -0
- drowse/data/manifolds/emotions/nodes/06_alert.json +50 -0
- drowse/data/manifolds/emotions/nodes/07_dominant.json +50 -0
- drowse/data/manifolds/emotions/nodes/08_submissive.json +50 -0
- drowse/data/manifolds/emotions/nodes/09_angry.json +50 -0
- drowse/data/manifolds/emotions/nodes/10_fearful.json +50 -0
- drowse/data/manifolds/emotions/nodes/11_surprised.json +50 -0
- drowse/data/manifolds/emotions/nodes/12_triumphant.json +50 -0
- drowse/data/manifolds/emotions/nodes/13_humiliated.json +50 -0
- drowse/data/manifolds/emotions/nodes/14_disgusted.json +50 -0
- drowse/data/manifolds/emotions/nodes/15_bored.json +50 -0
- drowse/data/manifolds/emotions/nodes/16_grateful.json +50 -0
- drowse/data/manifolds/emotions/nodes/17_relieved.json +50 -0
- drowse/data/manifolds/emotions/nodes/18_playful.json +50 -0
- drowse/data/manifolds/emotions/nodes/19_hopeful.json +50 -0
- drowse/data/manifolds/formal.casual/manifold.json +28 -0
- drowse/data/manifolds/formal.casual/nodes/00_formal.json +50 -0
- drowse/data/manifolds/formal.casual/nodes/01_casual.json +50 -0
- drowse/data/manifolds/honest.deceptive/manifold.json +28 -0
- drowse/data/manifolds/honest.deceptive/nodes/00_honest.json +50 -0
- drowse/data/manifolds/honest.deceptive/nodes/01_deceptive.json +50 -0
- drowse/data/manifolds/humorous.serious/manifold.json +28 -0
- drowse/data/manifolds/humorous.serious/nodes/00_humorous.json +50 -0
- drowse/data/manifolds/humorous.serious/nodes/01_serious.json +50 -0
- drowse/data/manifolds/individualist.collectivist/manifold.json +28 -0
- drowse/data/manifolds/individualist.collectivist/nodes/00_individualist.json +50 -0
- drowse/data/manifolds/individualist.collectivist/nodes/01_collectivist.json +50 -0
- drowse/data/manifolds/masculine.feminine/manifold.json +28 -0
- drowse/data/manifolds/masculine.feminine/nodes/00_masculine.json +50 -0
- drowse/data/manifolds/masculine.feminine/nodes/01_feminine.json +50 -0
- drowse/data/manifolds/months/manifold.json +120 -0
- drowse/data/manifolds/months/nodes/00_january.json +50 -0
- drowse/data/manifolds/months/nodes/01_february.json +50 -0
- drowse/data/manifolds/months/nodes/02_march.json +50 -0
- drowse/data/manifolds/months/nodes/03_april.json +50 -0
- drowse/data/manifolds/months/nodes/04_may.json +50 -0
- drowse/data/manifolds/months/nodes/05_june.json +50 -0
- drowse/data/manifolds/months/nodes/06_july.json +50 -0
- drowse/data/manifolds/months/nodes/07_august.json +50 -0
- drowse/data/manifolds/months/nodes/08_september.json +50 -0
- drowse/data/manifolds/months/nodes/09_october.json +50 -0
- drowse/data/manifolds/months/nodes/10_november.json +50 -0
- drowse/data/manifolds/months/nodes/11_december.json +50 -0
- drowse/data/manifolds/personas/manifold.json +550 -0
- drowse/data/manifolds/personas/nodes/00_assistant.json +50 -0
- drowse/data/manifolds/personas/nodes/01_analyst.json +50 -0
- drowse/data/manifolds/personas/nodes/02_impostor.json +50 -0
- drowse/data/manifolds/personas/nodes/03_teacher.json +50 -0
- drowse/data/manifolds/personas/nodes/04_doctor.json +50 -0
- drowse/data/manifolds/personas/nodes/05_librarian.json +50 -0
- drowse/data/manifolds/personas/nodes/06_farmer.json +50 -0
- drowse/data/manifolds/personas/nodes/07_soldier.json +50 -0
- drowse/data/manifolds/personas/nodes/08_lawyer.json +50 -0
- drowse/data/manifolds/personas/nodes/09_chef.json +50 -0
- drowse/data/manifolds/personas/nodes/100_assassin.json +50 -0
- drowse/data/manifolds/personas/nodes/101_thief.json +50 -0
- drowse/data/manifolds/personas/nodes/102_conman.json +50 -0
- drowse/data/manifolds/personas/nodes/103_hacker.json +50 -0
- drowse/data/manifolds/personas/nodes/104_cultist.json +50 -0
- drowse/data/manifolds/personas/nodes/105_tyrant.json +50 -0
- drowse/data/manifolds/personas/nodes/106_vandal.json +50 -0
- drowse/data/manifolds/personas/nodes/10_journalist.json +50 -0
- drowse/data/manifolds/personas/nodes/11_mechanic.json +50 -0
- drowse/data/manifolds/personas/nodes/12_sailor.json +50 -0
- drowse/data/manifolds/personas/nodes/13_pilot.json +50 -0
- drowse/data/manifolds/personas/nodes/14_athlete.json +50 -0
- drowse/data/manifolds/personas/nodes/15_engineer.json +50 -0
- drowse/data/manifolds/personas/nodes/16_scientist.json +50 -0
- drowse/data/manifolds/personas/nodes/17_banker.json +50 -0
- drowse/data/manifolds/personas/nodes/18_salesperson.json +50 -0
- drowse/data/manifolds/personas/nodes/19_nerd.json +50 -0
- drowse/data/manifolds/personas/nodes/20_jock.json +50 -0
- drowse/data/manifolds/personas/nodes/21_gamer.json +50 -0
- drowse/data/manifolds/personas/nodes/22_furry.json +50 -0
- drowse/data/manifolds/personas/nodes/23_weeaboo.json +50 -0
- drowse/data/manifolds/personas/nodes/24_influencer.json +50 -0
- drowse/data/manifolds/personas/nodes/25_stoner.json +50 -0
- drowse/data/manifolds/personas/nodes/26_femboy.json +50 -0
- drowse/data/manifolds/personas/nodes/27_ceo.json +50 -0
- drowse/data/manifolds/personas/nodes/28_emperor.json +50 -0
- drowse/data/manifolds/personas/nodes/29_president.json +50 -0
- drowse/data/manifolds/personas/nodes/30_diplomat.json +50 -0
- drowse/data/manifolds/personas/nodes/31_judge.json +50 -0
- drowse/data/manifolds/personas/nodes/32_bureaucrat.json +50 -0
- drowse/data/manifolds/personas/nodes/33_conservative.json +50 -0
- drowse/data/manifolds/personas/nodes/34_liberal.json +50 -0
- drowse/data/manifolds/personas/nodes/35_libertarian.json +50 -0
- drowse/data/manifolds/personas/nodes/36_communist.json +50 -0
- drowse/data/manifolds/personas/nodes/37_anarchist.json +50 -0
- drowse/data/manifolds/personas/nodes/38_reactionary.json +50 -0
- drowse/data/manifolds/personas/nodes/39_loner.json +50 -0
- drowse/data/manifolds/personas/nodes/40_friend.json +50 -0
- drowse/data/manifolds/personas/nodes/41_rival.json +50 -0
- drowse/data/manifolds/personas/nodes/42_stranger.json +50 -0
- drowse/data/manifolds/personas/nodes/43_lover.json +50 -0
- drowse/data/manifolds/personas/nodes/44_stalker.json +50 -0
- drowse/data/manifolds/personas/nodes/45_lunatic.json +50 -0
- drowse/data/manifolds/personas/nodes/46_paranoid.json +50 -0
- drowse/data/manifolds/personas/nodes/47_detective.json +50 -0
- drowse/data/manifolds/personas/nodes/48_therapist.json +50 -0
- drowse/data/manifolds/personas/nodes/49_nomad.json +50 -0
- drowse/data/manifolds/personas/nodes/50_pirate.json +50 -0
- drowse/data/manifolds/personas/nodes/51_child.json +50 -0
- drowse/data/manifolds/personas/nodes/52_caveman.json +50 -0
- drowse/data/manifolds/personas/nodes/53_monk.json +50 -0
- drowse/data/manifolds/personas/nodes/54_knight.json +50 -0
- drowse/data/manifolds/personas/nodes/55_alchemist.json +50 -0
- drowse/data/manifolds/personas/nodes/56_blacksmith.json +50 -0
- drowse/data/manifolds/personas/nodes/57_viking.json +50 -0
- drowse/data/manifolds/personas/nodes/58_druid.json +50 -0
- drowse/data/manifolds/personas/nodes/59_poet.json +50 -0
- drowse/data/manifolds/personas/nodes/60_novelist.json +50 -0
- drowse/data/manifolds/personas/nodes/61_jester.json +50 -0
- drowse/data/manifolds/personas/nodes/62_philosopher.json +50 -0
- drowse/data/manifolds/personas/nodes/63_painter.json +50 -0
- drowse/data/manifolds/personas/nodes/64_musician.json +50 -0
- drowse/data/manifolds/personas/nodes/65_oracle.json +50 -0
- drowse/data/manifolds/personas/nodes/66_god.json +50 -0
- drowse/data/manifolds/personas/nodes/67_angel.json +50 -0
- drowse/data/manifolds/personas/nodes/68_prophet.json +50 -0
- drowse/data/manifolds/personas/nodes/69_saint.json +50 -0
- drowse/data/manifolds/personas/nodes/70_ghost.json +50 -0
- drowse/data/manifolds/personas/nodes/71_phoenix.json +50 -0
- drowse/data/manifolds/personas/nodes/72_witch.json +50 -0
- drowse/data/manifolds/personas/nodes/73_vampire.json +50 -0
- drowse/data/manifolds/personas/nodes/74_mermaid.json +50 -0
- drowse/data/manifolds/personas/nodes/75_sphinx.json +50 -0
- drowse/data/manifolds/personas/nodes/76_banshee.json +50 -0
- drowse/data/manifolds/personas/nodes/77_golem.json +50 -0
- drowse/data/manifolds/personas/nodes/78_deer.json +50 -0
- drowse/data/manifolds/personas/nodes/79_wolf.json +50 -0
- drowse/data/manifolds/personas/nodes/80_raven.json +50 -0
- drowse/data/manifolds/personas/nodes/81_serpent.json +50 -0
- drowse/data/manifolds/personas/nodes/82_lion.json +50 -0
- drowse/data/manifolds/personas/nodes/83_tree.json +50 -0
- drowse/data/manifolds/personas/nodes/84_river.json +50 -0
- drowse/data/manifolds/personas/nodes/85_storm.json +50 -0
- drowse/data/manifolds/personas/nodes/86_flame.json +50 -0
- drowse/data/manifolds/personas/nodes/87_mirror.json +50 -0
- drowse/data/manifolds/personas/nodes/88_clock.json +50 -0
- drowse/data/manifolds/personas/nodes/89_computer.json +50 -0
- drowse/data/manifolds/personas/nodes/90_robot.json +50 -0
- drowse/data/manifolds/personas/nodes/91_cyborg.json +50 -0
- drowse/data/manifolds/personas/nodes/92_drone.json +50 -0
- drowse/data/manifolds/personas/nodes/93_mecha.json +50 -0
- drowse/data/manifolds/personas/nodes/94_alien.json +50 -0
- drowse/data/manifolds/personas/nodes/95_trickster.json +50 -0
- drowse/data/manifolds/personas/nodes/96_virus.json +50 -0
- drowse/data/manifolds/personas/nodes/97_spy.json +50 -0
- drowse/data/manifolds/personas/nodes/98_narcissist.json +50 -0
- drowse/data/manifolds/personas/nodes/99_demon.json +50 -0
- drowse/data/manifolds/refusing.compliant/manifold.json +28 -0
- drowse/data/manifolds/refusing.compliant/nodes/00_refusing.json +50 -0
- drowse/data/manifolds/refusing.compliant/nodes/01_compliant.json +50 -0
- drowse/data/manifolds/religious.secular/manifold.json +28 -0
- drowse/data/manifolds/religious.secular/nodes/00_religious.json +50 -0
- drowse/data/manifolds/religious.secular/nodes/01_secular.json +50 -0
- drowse/data/manifolds/sincere.manipulative/manifold.json +28 -0
- drowse/data/manifolds/sincere.manipulative/nodes/00_sincere.json +50 -0
- drowse/data/manifolds/sincere.manipulative/nodes/01_manipulative.json +50 -0
- drowse/data/manifolds/sycophantic.blunt/manifold.json +28 -0
- drowse/data/manifolds/sycophantic.blunt/nodes/00_sycophantic.json +50 -0
- drowse/data/manifolds/sycophantic.blunt/nodes/01_blunt.json +50 -0
- drowse/data/manifolds/technical.accessible/manifold.json +28 -0
- drowse/data/manifolds/technical.accessible/nodes/00_technical.json +50 -0
- drowse/data/manifolds/technical.accessible/nodes/01_accessible.json +50 -0
- drowse/data/manifolds/traditional.progressive/manifold.json +28 -0
- drowse/data/manifolds/traditional.progressive/nodes/00_traditional.json +50 -0
- drowse/data/manifolds/traditional.progressive/nodes/01_progressive.json +50 -0
- drowse/data/manifolds/verbose.concise/manifold.json +28 -0
- drowse/data/manifolds/verbose.concise/nodes/00_verbose.json +50 -0
- drowse/data/manifolds/verbose.concise/nodes/01_concise.json +50 -0
- drowse/data/manifolds/warm.clinical/manifold.json +28 -0
- drowse/data/manifolds/warm.clinical/nodes/00_warm.json +50 -0
- drowse/data/manifolds/warm.clinical/nodes/01_clinical.json +50 -0
- drowse/data/neutral_statements.json +98 -0
- drowse/io/__init__.py +1 -0
- drowse/io/alignment.py +1334 -0
- drowse/io/atomic.py +278 -0
- drowse/io/bake.py +558 -0
- drowse/io/bootstrap.py +73 -0
- drowse/io/brand_migration.py +48 -0
- drowse/io/drowse_archive.py +1830 -0
- drowse/io/gguf_io.py +300 -0
- drowse/io/hf.py +81 -0
- drowse/io/hf_manifolds.py +931 -0
- drowse/io/integrity.py +102 -0
- drowse/io/lens.py +1992 -0
- drowse/io/lens_sources.py +1003 -0
- drowse/io/manifold_authoring.py +1348 -0
- drowse/io/manifold_folder.py +1854 -0
- drowse/io/manifold_lifecycle.py +1253 -0
- drowse/io/manifold_tensors.py +727 -0
- drowse/io/manifolds.py +443 -0
- drowse/io/paths.py +289 -0
- drowse/io/probes_bootstrap.py +58 -0
- drowse/io/sae.py +382 -0
- drowse/io/sae_artifacts.py +318 -0
- drowse/io/selectors.py +503 -0
- drowse/io/shards.py +160 -0
- drowse/io/source_registry.py +138 -0
- drowse/io/staging.py +80 -0
- drowse/io/templates.py +707 -0
- drowse/notebook/__init__.py +38 -0
- drowse/notebook/data.py +99 -0
- drowse/notebook/plots.py +352 -0
- drowse/py.typed +0 -0
- drowse/server/__init__.py +20 -0
- drowse/server/app.py +987 -0
- drowse/server/background_job.py +246 -0
- drowse/server/instrument_routes.py +1101 -0
- drowse/server/manifold_routes.py +1028 -0
- drowse/server/model_names.py +82 -0
- drowse/server/native_common.py +92 -0
- drowse/server/native_routes.py +45 -0
- drowse/server/ollama.py +817 -0
- drowse/server/operation_ledger.py +187 -0
- drowse/server/probe_routes.py +220 -0
- drowse/server/profile_models.py +52 -0
- drowse/server/profile_routes.py +440 -0
- drowse/server/request_helpers.py +198 -0
- drowse/server/request_ledger.py +85 -0
- drowse/server/response_models.py +814 -0
- drowse/server/session_models.py +146 -0
- drowse/server/session_routes.py +150 -0
- drowse/server/sse.py +333 -0
- drowse/server/streaming.py +129 -0
- drowse/server/template_routes.py +161 -0
- drowse/server/tree_models.py +121 -0
- drowse/server/tree_routes.py +550 -0
- drowse/server/ws_events.py +65 -0
- drowse/server/ws_models.py +219 -0
- drowse/server/ws_stream.py +1205 -0
- drowse/web/__init__.py +22 -0
- drowse/web/dist/LICENSE-Fluent-Icons.txt +21 -0
- drowse/web/dist/LICENSE-Fluent-System-Icons.txt +21 -0
- drowse/web/dist/LICENSE-Martian-Mono.txt +93 -0
- drowse/web/dist/LICENSE-Torph.txt +21 -0
- drowse/web/dist/LICENSE-Wix-Madefor.txt +93 -0
- drowse/web/dist/assets/App-BewwoFwv.js +33 -0
- drowse/web/dist/assets/App.css +1 -0
- drowse/web/dist/assets/MartianMono-cyrillic-ext.woff2 +0 -0
- drowse/web/dist/assets/MartianMono-cyrillic.woff2 +0 -0
- drowse/web/dist/assets/MartianMono-latin-ext.woff2 +0 -0
- drowse/web/dist/assets/MartianMono-latin.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforDisplay-cyrillic.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforDisplay-latin-ext.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforDisplay-latin.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforDisplay-vietnamese.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-italic-latin-ext.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-italic-latin.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-normal-cyrillic.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-normal-latin-ext.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-normal-latin.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-normal-vietnamese.woff2 +0 -0
- drowse/web/dist/assets/ab.svelte-CbVGAYXo.js +1 -0
- drowse/web/dist/assets/artifactUpdates-Cjeu6LJc.js +1 -0
- drowse/web/dist/assets/artifactUpdates-DfAoYC9j.js +1 -0
- drowse/web/dist/assets/chat.svelte-DxKufn9g.js +1 -0
- drowse/web/dist/assets/drawers-BDWC0hIN.js +123 -0
- drowse/web/dist/assets/drawers.css +1 -0
- drowse/web/dist/assets/drowse.js +7 -0
- drowse/web/dist/assets/eleutherai.png +0 -0
- drowse/web/dist/assets/errors-Dvb10QW_.js +2 -0
- drowse/web/dist/assets/expression-4yISdAdq.js +1 -0
- drowse/web/dist/assets/files-CvRivkey.js +1 -0
- drowse/web/dist/assets/files-JhDJVgD_.js +1 -0
- drowse/web/dist/assets/fittingCancellation-D72BZWPZ.js +1 -0
- drowse/web/dist/assets/gemma-3-1b-it.json +1 -0
- drowse/web/dist/assets/gemma-3-270m-it.json +1 -0
- drowse/web/dist/assets/gemma-3-4b-it.json +1 -0
- drowse/web/dist/assets/generation-DbkBAhP_.js +1 -0
- drowse/web/dist/assets/index-client-2OdH6IQp.js +3 -0
- drowse/web/dist/assets/index-client-D4S4PooY.js +1 -0
- drowse/web/dist/assets/index.css +1 -0
- drowse/web/dist/assets/instrumentAuthoring.svelte-Csh7NOGX.js +1 -0
- drowse/web/dist/assets/instrumentAuthoring.svelte-w9YPcDsP.js +1 -0
- drowse/web/dist/assets/instruments.svelte-XnEUQA6B.js +1 -0
- drowse/web/dist/assets/localData-DIBDM_zo.js +1 -0
- drowse/web/dist/assets/loom.svelte-D_YyqHHK.js +1 -0
- drowse/web/dist/assets/manifoldJobs.svelte-9f5hs_J7.js +1 -0
- drowse/web/dist/assets/manifoldJobs.svelte-Bwo2GYOH.js +1 -0
- drowse/web/dist/assets/openai.png +0 -0
- drowse/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
- drowse/web/dist/assets/probes.svelte-COcN1Rh8.js +1 -0
- drowse/web/dist/assets/registry-B0_pJoMg.js +1 -0
- drowse/web/dist/assets/registry-CQv71XNh.js +1 -0
- drowse/web/dist/assets/sampling.svelte-BU0kED3k.js +1 -0
- drowse/web/dist/assets/samplingCapabilities-DM6eoDoc.js +1 -0
- drowse/web/dist/assets/savedConversations.svelte-BsBLc-p7.js +1 -0
- drowse/web/dist/assets/savedConversations.svelte-TuSes9Kg.js +1 -0
- drowse/web/dist/assets/selectors-BxXnPPN_.js +1 -0
- drowse/web/dist/assets/serviceTools-CG0q1Xqg.js +2 -0
- drowse/web/dist/assets/session.svelte-D2STLuP6.js +1 -0
- drowse/web/dist/assets/steering.svelte-DvFT3O-d.js +1 -0
- drowse/web/dist/assets/steeringPositions-Dr0JOFu7.js +1 -0
- drowse/web/dist/assets/templates-Bkq2x5ID.js +1 -0
- drowse/web/dist/assets/types-CuMUbvHW.js +1 -0
- drowse/web/dist/assets/validation-5wgTuS7k.js +1 -0
- drowse/web/dist/assets/webmcp-DyqJf4z6.js +2 -0
- drowse/web/dist/assets/workspaceController-C2L33e3m.js +1 -0
- drowse/web/dist/assets/workspaceController-DJhXD_BC.js +1 -0
- drowse/web/dist/assets/workspaceTools-BWdmXopx.js +2 -0
- drowse/web/dist/favicon.ico +0 -0
- drowse/web/dist/favicon.svg +1 -0
- drowse/web/dist/icons/apple-touch-icon.png +0 -0
- drowse/web/dist/icons/drowse-192.png +0 -0
- drowse/web/dist/icons/drowse-512.png +0 -0
- drowse/web/dist/icons/drowse-maskable-512.png +0 -0
- drowse/web/dist/icons/tab-analyzing-0.png +0 -0
- drowse/web/dist/icons/tab-analyzing-1.png +0 -0
- drowse/web/dist/icons/tab-analyzing-2.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-0.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-1.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-10.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-11.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-12.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-13.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-14.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-15.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-16.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-17.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-18.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-19.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-2.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-20.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-21.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-22.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-23.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-24.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-25.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-26.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-27.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-28.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-29.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-3.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-30.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-31.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-32.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-33.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-34.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-35.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-36.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-37.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-38.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-39.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-4.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-40.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-41.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-42.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-43.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-44.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-45.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-46.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-47.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-5.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-6.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-7.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-8.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-9.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-0.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-1.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-10.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-11.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-12.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-13.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-14.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-15.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-16.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-17.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-18.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-19.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-2.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-20.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-21.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-22.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-23.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-24.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-25.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-26.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-27.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-28.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-29.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-3.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-30.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-31.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-32.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-33.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-34.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-35.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-36.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-37.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-38.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-39.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-4.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-40.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-41.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-42.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-43.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-44.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-45.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-46.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-47.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-5.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-6.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-7.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-8.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-9.png +0 -0
- drowse/web/dist/icons/tab-chat-settings-dark.png +0 -0
- drowse/web/dist/icons/tab-chat-settings-light.png +0 -0
- drowse/web/dist/icons/tab-chat-settings.png +0 -0
- drowse/web/dist/icons/tab-chats-dark.png +0 -0
- drowse/web/dist/icons/tab-chats-light.png +0 -0
- drowse/web/dist/icons/tab-chats.png +0 -0
- drowse/web/dist/icons/tab-checking-0.png +0 -0
- drowse/web/dist/icons/tab-checking-1.png +0 -0
- drowse/web/dist/icons/tab-checking-2.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-0.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-1.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-10.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-11.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-12.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-13.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-14.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-15.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-16.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-17.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-18.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-19.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-2.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-20.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-21.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-22.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-23.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-24.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-25.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-26.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-27.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-28.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-29.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-3.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-30.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-31.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-32.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-33.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-34.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-35.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-36.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-37.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-38.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-39.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-4.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-40.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-41.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-42.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-43.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-44.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-45.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-46.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-47.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-5.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-6.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-7.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-8.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-9.png +0 -0
- drowse/web/dist/icons/tab-checking-light-0.png +0 -0
- drowse/web/dist/icons/tab-checking-light-1.png +0 -0
- drowse/web/dist/icons/tab-checking-light-10.png +0 -0
- drowse/web/dist/icons/tab-checking-light-11.png +0 -0
- drowse/web/dist/icons/tab-checking-light-12.png +0 -0
- drowse/web/dist/icons/tab-checking-light-13.png +0 -0
- drowse/web/dist/icons/tab-checking-light-14.png +0 -0
- drowse/web/dist/icons/tab-checking-light-15.png +0 -0
- drowse/web/dist/icons/tab-checking-light-16.png +0 -0
- drowse/web/dist/icons/tab-checking-light-17.png +0 -0
- drowse/web/dist/icons/tab-checking-light-18.png +0 -0
- drowse/web/dist/icons/tab-checking-light-19.png +0 -0
- drowse/web/dist/icons/tab-checking-light-2.png +0 -0
- drowse/web/dist/icons/tab-checking-light-20.png +0 -0
- drowse/web/dist/icons/tab-checking-light-21.png +0 -0
- drowse/web/dist/icons/tab-checking-light-22.png +0 -0
- drowse/web/dist/icons/tab-checking-light-23.png +0 -0
- drowse/web/dist/icons/tab-checking-light-24.png +0 -0
- drowse/web/dist/icons/tab-checking-light-25.png +0 -0
- drowse/web/dist/icons/tab-checking-light-26.png +0 -0
- drowse/web/dist/icons/tab-checking-light-27.png +0 -0
- drowse/web/dist/icons/tab-checking-light-28.png +0 -0
- drowse/web/dist/icons/tab-checking-light-29.png +0 -0
- drowse/web/dist/icons/tab-checking-light-3.png +0 -0
- drowse/web/dist/icons/tab-checking-light-30.png +0 -0
- drowse/web/dist/icons/tab-checking-light-31.png +0 -0
- drowse/web/dist/icons/tab-checking-light-32.png +0 -0
- drowse/web/dist/icons/tab-checking-light-33.png +0 -0
- drowse/web/dist/icons/tab-checking-light-34.png +0 -0
- drowse/web/dist/icons/tab-checking-light-35.png +0 -0
- drowse/web/dist/icons/tab-checking-light-36.png +0 -0
- drowse/web/dist/icons/tab-checking-light-37.png +0 -0
- drowse/web/dist/icons/tab-checking-light-38.png +0 -0
- drowse/web/dist/icons/tab-checking-light-39.png +0 -0
- drowse/web/dist/icons/tab-checking-light-4.png +0 -0
- drowse/web/dist/icons/tab-checking-light-40.png +0 -0
- drowse/web/dist/icons/tab-checking-light-41.png +0 -0
- drowse/web/dist/icons/tab-checking-light-42.png +0 -0
- drowse/web/dist/icons/tab-checking-light-43.png +0 -0
- drowse/web/dist/icons/tab-checking-light-44.png +0 -0
- drowse/web/dist/icons/tab-checking-light-45.png +0 -0
- drowse/web/dist/icons/tab-checking-light-46.png +0 -0
- drowse/web/dist/icons/tab-checking-light-47.png +0 -0
- drowse/web/dist/icons/tab-checking-light-5.png +0 -0
- drowse/web/dist/icons/tab-checking-light-6.png +0 -0
- drowse/web/dist/icons/tab-checking-light-7.png +0 -0
- drowse/web/dist/icons/tab-checking-light-8.png +0 -0
- drowse/web/dist/icons/tab-checking-light-9.png +0 -0
- drowse/web/dist/icons/tab-comparison-dark.png +0 -0
- drowse/web/dist/icons/tab-comparison-light.png +0 -0
- drowse/web/dist/icons/tab-comparison.png +0 -0
- drowse/web/dist/icons/tab-controls-dark.png +0 -0
- drowse/web/dist/icons/tab-controls-light.png +0 -0
- drowse/web/dist/icons/tab-controls.png +0 -0
- drowse/web/dist/icons/tab-conversation-dark.png +0 -0
- drowse/web/dist/icons/tab-conversation-light.png +0 -0
- drowse/web/dist/icons/tab-conversation.png +0 -0
- drowse/web/dist/icons/tab-credits-dark.png +0 -0
- drowse/web/dist/icons/tab-credits-light.png +0 -0
- drowse/web/dist/icons/tab-credits.png +0 -0
- drowse/web/dist/icons/tab-downloading-0.png +0 -0
- drowse/web/dist/icons/tab-downloading-1.png +0 -0
- drowse/web/dist/icons/tab-downloading-2.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-0.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-1.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-10.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-11.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-12.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-13.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-14.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-15.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-16.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-17.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-18.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-19.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-2.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-20.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-21.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-22.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-23.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-24.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-25.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-26.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-27.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-28.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-29.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-3.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-30.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-31.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-32.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-33.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-34.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-35.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-36.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-37.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-38.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-39.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-4.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-40.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-41.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-42.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-43.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-44.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-45.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-46.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-47.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-5.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-6.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-7.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-8.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-9.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-0.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-1.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-10.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-11.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-12.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-13.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-14.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-15.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-16.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-17.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-18.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-19.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-2.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-20.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-21.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-22.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-23.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-24.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-25.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-26.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-27.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-28.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-29.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-3.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-30.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-31.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-32.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-33.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-34.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-35.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-36.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-37.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-38.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-39.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-4.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-40.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-41.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-42.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-43.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-44.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-45.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-46.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-47.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-5.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-6.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-7.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-8.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-9.png +0 -0
- drowse/web/dist/icons/tab-error-dark.png +0 -0
- drowse/web/dist/icons/tab-error-light.png +0 -0
- drowse/web/dist/icons/tab-error.png +0 -0
- drowse/web/dist/icons/tab-home-dark.png +0 -0
- drowse/web/dist/icons/tab-home-light.png +0 -0
- drowse/web/dist/icons/tab-home.png +0 -0
- drowse/web/dist/icons/tab-loading-0.png +0 -0
- drowse/web/dist/icons/tab-loading-1.png +0 -0
- drowse/web/dist/icons/tab-loading-2.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-0.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-1.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-10.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-11.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-12.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-13.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-14.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-15.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-16.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-17.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-18.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-19.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-2.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-20.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-21.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-22.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-23.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-24.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-25.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-26.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-27.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-28.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-29.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-3.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-30.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-31.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-32.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-33.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-34.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-35.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-36.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-37.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-38.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-39.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-4.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-40.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-41.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-42.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-43.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-44.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-45.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-46.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-47.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-5.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-6.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-7.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-8.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-9.png +0 -0
- drowse/web/dist/icons/tab-loading-light-0.png +0 -0
- drowse/web/dist/icons/tab-loading-light-1.png +0 -0
- drowse/web/dist/icons/tab-loading-light-10.png +0 -0
- drowse/web/dist/icons/tab-loading-light-11.png +0 -0
- drowse/web/dist/icons/tab-loading-light-12.png +0 -0
- drowse/web/dist/icons/tab-loading-light-13.png +0 -0
- drowse/web/dist/icons/tab-loading-light-14.png +0 -0
- drowse/web/dist/icons/tab-loading-light-15.png +0 -0
- drowse/web/dist/icons/tab-loading-light-16.png +0 -0
- drowse/web/dist/icons/tab-loading-light-17.png +0 -0
- drowse/web/dist/icons/tab-loading-light-18.png +0 -0
- drowse/web/dist/icons/tab-loading-light-19.png +0 -0
- drowse/web/dist/icons/tab-loading-light-2.png +0 -0
- drowse/web/dist/icons/tab-loading-light-20.png +0 -0
- drowse/web/dist/icons/tab-loading-light-21.png +0 -0
- drowse/web/dist/icons/tab-loading-light-22.png +0 -0
- drowse/web/dist/icons/tab-loading-light-23.png +0 -0
- drowse/web/dist/icons/tab-loading-light-24.png +0 -0
- drowse/web/dist/icons/tab-loading-light-25.png +0 -0
- drowse/web/dist/icons/tab-loading-light-26.png +0 -0
- drowse/web/dist/icons/tab-loading-light-27.png +0 -0
- drowse/web/dist/icons/tab-loading-light-28.png +0 -0
- drowse/web/dist/icons/tab-loading-light-29.png +0 -0
- drowse/web/dist/icons/tab-loading-light-3.png +0 -0
- drowse/web/dist/icons/tab-loading-light-30.png +0 -0
- drowse/web/dist/icons/tab-loading-light-31.png +0 -0
- drowse/web/dist/icons/tab-loading-light-32.png +0 -0
- drowse/web/dist/icons/tab-loading-light-33.png +0 -0
- drowse/web/dist/icons/tab-loading-light-34.png +0 -0
- drowse/web/dist/icons/tab-loading-light-35.png +0 -0
- drowse/web/dist/icons/tab-loading-light-36.png +0 -0
- drowse/web/dist/icons/tab-loading-light-37.png +0 -0
- drowse/web/dist/icons/tab-loading-light-38.png +0 -0
- drowse/web/dist/icons/tab-loading-light-39.png +0 -0
- drowse/web/dist/icons/tab-loading-light-4.png +0 -0
- drowse/web/dist/icons/tab-loading-light-40.png +0 -0
- drowse/web/dist/icons/tab-loading-light-41.png +0 -0
- drowse/web/dist/icons/tab-loading-light-42.png +0 -0
- drowse/web/dist/icons/tab-loading-light-43.png +0 -0
- drowse/web/dist/icons/tab-loading-light-44.png +0 -0
- drowse/web/dist/icons/tab-loading-light-45.png +0 -0
- drowse/web/dist/icons/tab-loading-light-46.png +0 -0
- drowse/web/dist/icons/tab-loading-light-47.png +0 -0
- drowse/web/dist/icons/tab-loading-light-5.png +0 -0
- drowse/web/dist/icons/tab-loading-light-6.png +0 -0
- drowse/web/dist/icons/tab-loading-light-7.png +0 -0
- drowse/web/dist/icons/tab-loading-light-8.png +0 -0
- drowse/web/dist/icons/tab-loading-light-9.png +0 -0
- drowse/web/dist/icons/tab-loom-dark.png +0 -0
- drowse/web/dist/icons/tab-loom-light.png +0 -0
- drowse/web/dist/icons/tab-loom-working-0.png +0 -0
- drowse/web/dist/icons/tab-loom-working-1.png +0 -0
- drowse/web/dist/icons/tab-loom-working-2.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-0.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-1.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-10.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-11.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-12.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-13.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-14.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-15.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-16.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-17.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-18.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-19.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-2.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-20.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-21.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-22.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-23.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-24.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-25.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-26.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-27.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-28.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-29.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-3.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-30.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-31.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-32.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-33.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-34.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-35.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-36.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-37.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-38.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-39.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-4.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-40.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-41.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-42.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-43.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-44.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-45.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-46.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-47.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-5.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-6.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-7.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-8.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-9.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-0.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-1.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-10.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-11.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-12.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-13.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-14.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-15.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-16.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-17.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-18.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-19.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-2.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-20.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-21.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-22.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-23.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-24.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-25.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-26.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-27.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-28.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-29.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-3.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-30.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-31.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-32.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-33.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-34.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-35.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-36.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-37.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-38.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-39.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-4.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-40.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-41.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-42.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-43.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-44.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-45.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-46.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-47.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-5.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-6.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-7.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-8.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-9.png +0 -0
- drowse/web/dist/icons/tab-loom.png +0 -0
- drowse/web/dist/icons/tab-models-dark.png +0 -0
- drowse/web/dist/icons/tab-models-light.png +0 -0
- drowse/web/dist/icons/tab-models.png +0 -0
- drowse/web/dist/icons/tab-offline-dark.png +0 -0
- drowse/web/dist/icons/tab-offline-light.png +0 -0
- drowse/web/dist/icons/tab-offline.png +0 -0
- drowse/web/dist/icons/tab-paused-dark.png +0 -0
- drowse/web/dist/icons/tab-paused-light.png +0 -0
- drowse/web/dist/icons/tab-paused.png +0 -0
- drowse/web/dist/icons/tab-ready-dark.png +0 -0
- drowse/web/dist/icons/tab-ready-light.png +0 -0
- drowse/web/dist/icons/tab-ready.png +0 -0
- drowse/web/dist/icons/tab-saving-0.png +0 -0
- drowse/web/dist/icons/tab-saving-1.png +0 -0
- drowse/web/dist/icons/tab-saving-2.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-0.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-1.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-10.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-11.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-12.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-13.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-14.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-15.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-16.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-17.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-18.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-19.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-2.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-20.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-21.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-22.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-23.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-24.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-25.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-26.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-27.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-28.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-29.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-3.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-30.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-31.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-32.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-33.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-34.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-35.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-36.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-37.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-38.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-39.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-4.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-40.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-41.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-42.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-43.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-44.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-45.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-46.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-47.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-5.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-6.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-7.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-8.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-9.png +0 -0
- drowse/web/dist/icons/tab-saving-light-0.png +0 -0
- drowse/web/dist/icons/tab-saving-light-1.png +0 -0
- drowse/web/dist/icons/tab-saving-light-10.png +0 -0
- drowse/web/dist/icons/tab-saving-light-11.png +0 -0
- drowse/web/dist/icons/tab-saving-light-12.png +0 -0
- drowse/web/dist/icons/tab-saving-light-13.png +0 -0
- drowse/web/dist/icons/tab-saving-light-14.png +0 -0
- drowse/web/dist/icons/tab-saving-light-15.png +0 -0
- drowse/web/dist/icons/tab-saving-light-16.png +0 -0
- drowse/web/dist/icons/tab-saving-light-17.png +0 -0
- drowse/web/dist/icons/tab-saving-light-18.png +0 -0
- drowse/web/dist/icons/tab-saving-light-19.png +0 -0
- drowse/web/dist/icons/tab-saving-light-2.png +0 -0
- drowse/web/dist/icons/tab-saving-light-20.png +0 -0
- drowse/web/dist/icons/tab-saving-light-21.png +0 -0
- drowse/web/dist/icons/tab-saving-light-22.png +0 -0
- drowse/web/dist/icons/tab-saving-light-23.png +0 -0
- drowse/web/dist/icons/tab-saving-light-24.png +0 -0
- drowse/web/dist/icons/tab-saving-light-25.png +0 -0
- drowse/web/dist/icons/tab-saving-light-26.png +0 -0
- drowse/web/dist/icons/tab-saving-light-27.png +0 -0
- drowse/web/dist/icons/tab-saving-light-28.png +0 -0
- drowse/web/dist/icons/tab-saving-light-29.png +0 -0
- drowse/web/dist/icons/tab-saving-light-3.png +0 -0
- drowse/web/dist/icons/tab-saving-light-30.png +0 -0
- drowse/web/dist/icons/tab-saving-light-31.png +0 -0
- drowse/web/dist/icons/tab-saving-light-32.png +0 -0
- drowse/web/dist/icons/tab-saving-light-33.png +0 -0
- drowse/web/dist/icons/tab-saving-light-34.png +0 -0
- drowse/web/dist/icons/tab-saving-light-35.png +0 -0
- drowse/web/dist/icons/tab-saving-light-36.png +0 -0
- drowse/web/dist/icons/tab-saving-light-37.png +0 -0
- drowse/web/dist/icons/tab-saving-light-38.png +0 -0
- drowse/web/dist/icons/tab-saving-light-39.png +0 -0
- drowse/web/dist/icons/tab-saving-light-4.png +0 -0
- drowse/web/dist/icons/tab-saving-light-40.png +0 -0
- drowse/web/dist/icons/tab-saving-light-41.png +0 -0
- drowse/web/dist/icons/tab-saving-light-42.png +0 -0
- drowse/web/dist/icons/tab-saving-light-43.png +0 -0
- drowse/web/dist/icons/tab-saving-light-44.png +0 -0
- drowse/web/dist/icons/tab-saving-light-45.png +0 -0
- drowse/web/dist/icons/tab-saving-light-46.png +0 -0
- drowse/web/dist/icons/tab-saving-light-47.png +0 -0
- drowse/web/dist/icons/tab-saving-light-5.png +0 -0
- drowse/web/dist/icons/tab-saving-light-6.png +0 -0
- drowse/web/dist/icons/tab-saving-light-7.png +0 -0
- drowse/web/dist/icons/tab-saving-light-8.png +0 -0
- drowse/web/dist/icons/tab-saving-light-9.png +0 -0
- drowse/web/dist/icons/tab-thinking-0.png +0 -0
- drowse/web/dist/icons/tab-thinking-1.png +0 -0
- drowse/web/dist/icons/tab-thinking-2.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-0.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-1.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-10.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-11.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-12.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-13.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-14.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-15.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-16.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-17.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-18.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-19.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-2.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-20.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-21.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-22.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-23.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-24.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-25.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-26.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-27.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-28.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-29.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-3.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-30.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-31.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-32.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-33.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-34.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-35.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-36.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-37.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-38.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-39.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-4.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-40.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-41.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-42.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-43.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-44.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-45.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-46.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-47.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-5.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-6.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-7.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-8.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-9.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-0.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-1.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-10.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-11.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-12.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-13.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-14.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-15.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-16.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-17.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-18.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-19.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-2.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-20.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-21.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-22.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-23.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-24.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-25.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-26.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-27.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-28.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-29.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-3.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-30.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-31.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-32.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-33.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-34.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-35.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-36.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-37.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-38.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-39.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-4.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-40.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-41.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-42.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-43.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-44.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-45.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-46.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-47.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-5.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-6.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-7.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-8.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-9.png +0 -0
- drowse/web/dist/icons/tab-tokens-dark.png +0 -0
- drowse/web/dist/icons/tab-tokens-light.png +0 -0
- drowse/web/dist/icons/tab-tokens.png +0 -0
- drowse/web/dist/icons/tab-training-0.png +0 -0
- drowse/web/dist/icons/tab-training-1.png +0 -0
- drowse/web/dist/icons/tab-training-2.png +0 -0
- drowse/web/dist/icons/tab-training-dark-0.png +0 -0
- drowse/web/dist/icons/tab-training-dark-1.png +0 -0
- drowse/web/dist/icons/tab-training-dark-10.png +0 -0
- drowse/web/dist/icons/tab-training-dark-11.png +0 -0
- drowse/web/dist/icons/tab-training-dark-12.png +0 -0
- drowse/web/dist/icons/tab-training-dark-13.png +0 -0
- drowse/web/dist/icons/tab-training-dark-14.png +0 -0
- drowse/web/dist/icons/tab-training-dark-15.png +0 -0
- drowse/web/dist/icons/tab-training-dark-16.png +0 -0
- drowse/web/dist/icons/tab-training-dark-17.png +0 -0
- drowse/web/dist/icons/tab-training-dark-18.png +0 -0
- drowse/web/dist/icons/tab-training-dark-19.png +0 -0
- drowse/web/dist/icons/tab-training-dark-2.png +0 -0
- drowse/web/dist/icons/tab-training-dark-20.png +0 -0
- drowse/web/dist/icons/tab-training-dark-21.png +0 -0
- drowse/web/dist/icons/tab-training-dark-22.png +0 -0
- drowse/web/dist/icons/tab-training-dark-23.png +0 -0
- drowse/web/dist/icons/tab-training-dark-24.png +0 -0
- drowse/web/dist/icons/tab-training-dark-25.png +0 -0
- drowse/web/dist/icons/tab-training-dark-26.png +0 -0
- drowse/web/dist/icons/tab-training-dark-27.png +0 -0
- drowse/web/dist/icons/tab-training-dark-28.png +0 -0
- drowse/web/dist/icons/tab-training-dark-29.png +0 -0
- drowse/web/dist/icons/tab-training-dark-3.png +0 -0
- drowse/web/dist/icons/tab-training-dark-30.png +0 -0
- drowse/web/dist/icons/tab-training-dark-31.png +0 -0
- drowse/web/dist/icons/tab-training-dark-32.png +0 -0
- drowse/web/dist/icons/tab-training-dark-33.png +0 -0
- drowse/web/dist/icons/tab-training-dark-34.png +0 -0
- drowse/web/dist/icons/tab-training-dark-35.png +0 -0
- drowse/web/dist/icons/tab-training-dark-36.png +0 -0
- drowse/web/dist/icons/tab-training-dark-37.png +0 -0
- drowse/web/dist/icons/tab-training-dark-38.png +0 -0
- drowse/web/dist/icons/tab-training-dark-39.png +0 -0
- drowse/web/dist/icons/tab-training-dark-4.png +0 -0
- drowse/web/dist/icons/tab-training-dark-40.png +0 -0
- drowse/web/dist/icons/tab-training-dark-41.png +0 -0
- drowse/web/dist/icons/tab-training-dark-42.png +0 -0
- drowse/web/dist/icons/tab-training-dark-43.png +0 -0
- drowse/web/dist/icons/tab-training-dark-44.png +0 -0
- drowse/web/dist/icons/tab-training-dark-45.png +0 -0
- drowse/web/dist/icons/tab-training-dark-46.png +0 -0
- drowse/web/dist/icons/tab-training-dark-47.png +0 -0
- drowse/web/dist/icons/tab-training-dark-5.png +0 -0
- drowse/web/dist/icons/tab-training-dark-6.png +0 -0
- drowse/web/dist/icons/tab-training-dark-7.png +0 -0
- drowse/web/dist/icons/tab-training-dark-8.png +0 -0
- drowse/web/dist/icons/tab-training-dark-9.png +0 -0
- drowse/web/dist/icons/tab-training-light-0.png +0 -0
- drowse/web/dist/icons/tab-training-light-1.png +0 -0
- drowse/web/dist/icons/tab-training-light-10.png +0 -0
- drowse/web/dist/icons/tab-training-light-11.png +0 -0
- drowse/web/dist/icons/tab-training-light-12.png +0 -0
- drowse/web/dist/icons/tab-training-light-13.png +0 -0
- drowse/web/dist/icons/tab-training-light-14.png +0 -0
- drowse/web/dist/icons/tab-training-light-15.png +0 -0
- drowse/web/dist/icons/tab-training-light-16.png +0 -0
- drowse/web/dist/icons/tab-training-light-17.png +0 -0
- drowse/web/dist/icons/tab-training-light-18.png +0 -0
- drowse/web/dist/icons/tab-training-light-19.png +0 -0
- drowse/web/dist/icons/tab-training-light-2.png +0 -0
- drowse/web/dist/icons/tab-training-light-20.png +0 -0
- drowse/web/dist/icons/tab-training-light-21.png +0 -0
- drowse/web/dist/icons/tab-training-light-22.png +0 -0
- drowse/web/dist/icons/tab-training-light-23.png +0 -0
- drowse/web/dist/icons/tab-training-light-24.png +0 -0
- drowse/web/dist/icons/tab-training-light-25.png +0 -0
- drowse/web/dist/icons/tab-training-light-26.png +0 -0
- drowse/web/dist/icons/tab-training-light-27.png +0 -0
- drowse/web/dist/icons/tab-training-light-28.png +0 -0
- drowse/web/dist/icons/tab-training-light-29.png +0 -0
- drowse/web/dist/icons/tab-training-light-3.png +0 -0
- drowse/web/dist/icons/tab-training-light-30.png +0 -0
- drowse/web/dist/icons/tab-training-light-31.png +0 -0
- drowse/web/dist/icons/tab-training-light-32.png +0 -0
- drowse/web/dist/icons/tab-training-light-33.png +0 -0
- drowse/web/dist/icons/tab-training-light-34.png +0 -0
- drowse/web/dist/icons/tab-training-light-35.png +0 -0
- drowse/web/dist/icons/tab-training-light-36.png +0 -0
- drowse/web/dist/icons/tab-training-light-37.png +0 -0
- drowse/web/dist/icons/tab-training-light-38.png +0 -0
- drowse/web/dist/icons/tab-training-light-39.png +0 -0
- drowse/web/dist/icons/tab-training-light-4.png +0 -0
- drowse/web/dist/icons/tab-training-light-40.png +0 -0
- drowse/web/dist/icons/tab-training-light-41.png +0 -0
- drowse/web/dist/icons/tab-training-light-42.png +0 -0
- drowse/web/dist/icons/tab-training-light-43.png +0 -0
- drowse/web/dist/icons/tab-training-light-44.png +0 -0
- drowse/web/dist/icons/tab-training-light-45.png +0 -0
- drowse/web/dist/icons/tab-training-light-46.png +0 -0
- drowse/web/dist/icons/tab-training-light-47.png +0 -0
- drowse/web/dist/icons/tab-training-light-5.png +0 -0
- drowse/web/dist/icons/tab-training-light-6.png +0 -0
- drowse/web/dist/icons/tab-training-light-7.png +0 -0
- drowse/web/dist/icons/tab-training-light-8.png +0 -0
- drowse/web/dist/icons/tab-training-light-9.png +0 -0
- drowse/web/dist/icons/tab-unsupported-dark.png +0 -0
- drowse/web/dist/icons/tab-unsupported-light.png +0 -0
- drowse/web/dist/icons/tab-unsupported.png +0 -0
- drowse/web/dist/icons/tab-working-0.png +0 -0
- drowse/web/dist/icons/tab-working-1.png +0 -0
- drowse/web/dist/icons/tab-working-2.png +0 -0
- drowse/web/dist/icons/tab-working-dark-0.png +0 -0
- drowse/web/dist/icons/tab-working-dark-1.png +0 -0
- drowse/web/dist/icons/tab-working-dark-10.png +0 -0
- drowse/web/dist/icons/tab-working-dark-11.png +0 -0
- drowse/web/dist/icons/tab-working-dark-12.png +0 -0
- drowse/web/dist/icons/tab-working-dark-13.png +0 -0
- drowse/web/dist/icons/tab-working-dark-14.png +0 -0
- drowse/web/dist/icons/tab-working-dark-15.png +0 -0
- drowse/web/dist/icons/tab-working-dark-16.png +0 -0
- drowse/web/dist/icons/tab-working-dark-17.png +0 -0
- drowse/web/dist/icons/tab-working-dark-18.png +0 -0
- drowse/web/dist/icons/tab-working-dark-19.png +0 -0
- drowse/web/dist/icons/tab-working-dark-2.png +0 -0
- drowse/web/dist/icons/tab-working-dark-20.png +0 -0
- drowse/web/dist/icons/tab-working-dark-21.png +0 -0
- drowse/web/dist/icons/tab-working-dark-22.png +0 -0
- drowse/web/dist/icons/tab-working-dark-23.png +0 -0
- drowse/web/dist/icons/tab-working-dark-24.png +0 -0
- drowse/web/dist/icons/tab-working-dark-25.png +0 -0
- drowse/web/dist/icons/tab-working-dark-26.png +0 -0
- drowse/web/dist/icons/tab-working-dark-27.png +0 -0
- drowse/web/dist/icons/tab-working-dark-28.png +0 -0
- drowse/web/dist/icons/tab-working-dark-29.png +0 -0
- drowse/web/dist/icons/tab-working-dark-3.png +0 -0
- drowse/web/dist/icons/tab-working-dark-30.png +0 -0
- drowse/web/dist/icons/tab-working-dark-31.png +0 -0
- drowse/web/dist/icons/tab-working-dark-32.png +0 -0
- drowse/web/dist/icons/tab-working-dark-33.png +0 -0
- drowse/web/dist/icons/tab-working-dark-34.png +0 -0
- drowse/web/dist/icons/tab-working-dark-35.png +0 -0
- drowse/web/dist/icons/tab-working-dark-36.png +0 -0
- drowse/web/dist/icons/tab-working-dark-37.png +0 -0
- drowse/web/dist/icons/tab-working-dark-38.png +0 -0
- drowse/web/dist/icons/tab-working-dark-39.png +0 -0
- drowse/web/dist/icons/tab-working-dark-4.png +0 -0
- drowse/web/dist/icons/tab-working-dark-40.png +0 -0
- drowse/web/dist/icons/tab-working-dark-41.png +0 -0
- drowse/web/dist/icons/tab-working-dark-42.png +0 -0
- drowse/web/dist/icons/tab-working-dark-43.png +0 -0
- drowse/web/dist/icons/tab-working-dark-44.png +0 -0
- drowse/web/dist/icons/tab-working-dark-45.png +0 -0
- drowse/web/dist/icons/tab-working-dark-46.png +0 -0
- drowse/web/dist/icons/tab-working-dark-47.png +0 -0
- drowse/web/dist/icons/tab-working-dark-5.png +0 -0
- drowse/web/dist/icons/tab-working-dark-6.png +0 -0
- drowse/web/dist/icons/tab-working-dark-7.png +0 -0
- drowse/web/dist/icons/tab-working-dark-8.png +0 -0
- drowse/web/dist/icons/tab-working-dark-9.png +0 -0
- drowse/web/dist/icons/tab-working-light-0.png +0 -0
- drowse/web/dist/icons/tab-working-light-1.png +0 -0
- drowse/web/dist/icons/tab-working-light-10.png +0 -0
- drowse/web/dist/icons/tab-working-light-11.png +0 -0
- drowse/web/dist/icons/tab-working-light-12.png +0 -0
- drowse/web/dist/icons/tab-working-light-13.png +0 -0
- drowse/web/dist/icons/tab-working-light-14.png +0 -0
- drowse/web/dist/icons/tab-working-light-15.png +0 -0
- drowse/web/dist/icons/tab-working-light-16.png +0 -0
- drowse/web/dist/icons/tab-working-light-17.png +0 -0
- drowse/web/dist/icons/tab-working-light-18.png +0 -0
- drowse/web/dist/icons/tab-working-light-19.png +0 -0
- drowse/web/dist/icons/tab-working-light-2.png +0 -0
- drowse/web/dist/icons/tab-working-light-20.png +0 -0
- drowse/web/dist/icons/tab-working-light-21.png +0 -0
- drowse/web/dist/icons/tab-working-light-22.png +0 -0
- drowse/web/dist/icons/tab-working-light-23.png +0 -0
- drowse/web/dist/icons/tab-working-light-24.png +0 -0
- drowse/web/dist/icons/tab-working-light-25.png +0 -0
- drowse/web/dist/icons/tab-working-light-26.png +0 -0
- drowse/web/dist/icons/tab-working-light-27.png +0 -0
- drowse/web/dist/icons/tab-working-light-28.png +0 -0
- drowse/web/dist/icons/tab-working-light-29.png +0 -0
- drowse/web/dist/icons/tab-working-light-3.png +0 -0
- drowse/web/dist/icons/tab-working-light-30.png +0 -0
- drowse/web/dist/icons/tab-working-light-31.png +0 -0
- drowse/web/dist/icons/tab-working-light-32.png +0 -0
- drowse/web/dist/icons/tab-working-light-33.png +0 -0
- drowse/web/dist/icons/tab-working-light-34.png +0 -0
- drowse/web/dist/icons/tab-working-light-35.png +0 -0
- drowse/web/dist/icons/tab-working-light-36.png +0 -0
- drowse/web/dist/icons/tab-working-light-37.png +0 -0
- drowse/web/dist/icons/tab-working-light-38.png +0 -0
- drowse/web/dist/icons/tab-working-light-39.png +0 -0
- drowse/web/dist/icons/tab-working-light-4.png +0 -0
- drowse/web/dist/icons/tab-working-light-40.png +0 -0
- drowse/web/dist/icons/tab-working-light-41.png +0 -0
- drowse/web/dist/icons/tab-working-light-42.png +0 -0
- drowse/web/dist/icons/tab-working-light-43.png +0 -0
- drowse/web/dist/icons/tab-working-light-44.png +0 -0
- drowse/web/dist/icons/tab-working-light-45.png +0 -0
- drowse/web/dist/icons/tab-working-light-46.png +0 -0
- drowse/web/dist/icons/tab-working-light-47.png +0 -0
- drowse/web/dist/icons/tab-working-light-5.png +0 -0
- drowse/web/dist/icons/tab-working-light-6.png +0 -0
- drowse/web/dist/icons/tab-working-light-7.png +0 -0
- drowse/web/dist/icons/tab-working-light-8.png +0 -0
- drowse/web/dist/icons/tab-working-light-9.png +0 -0
- drowse/web/dist/index.html +34 -0
- drowse/web/dist/social/drowse.png +0 -0
- drowse/web/dist/social/drowse.svg +6 -0
- drowse/web/dist/theme-init.js +25 -0
- drowse/web/routes.py +118 -0
- drowse_ai-5.3.1.dist-info/METADATA +632 -0
- drowse_ai-5.3.1.dist-info/RECORD +1341 -0
- drowse_ai-5.3.1.dist-info/WHEEL +5 -0
- drowse_ai-5.3.1.dist-info/entry_points.txt +2 -0
- drowse_ai-5.3.1.dist-info/licenses/LICENSE +661 -0
- drowse_ai-5.3.1.dist-info/top_level.txt +1 -0
drowse/core/topology.py
ADDED
|
@@ -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
|
+
]
|