Upload 9 files
Browse files
__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .dima import DIMA
|
| 2 |
+
from .dmap import DMAP
|
| 3 |
+
from .gplm import GPLM
|
| 4 |
+
from .ddpm import DDPM
|
| 5 |
+
|
| 6 |
+
__all__ = ["DIMA", "DMAP", "GPLM", "DDPM"]
|
ann.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/dima/ann.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import Any, Dict, Optional, Tuple
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from .utils import as_contig_f32, sqdist_ab
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# -------------------------
|
| 13 |
+
# Public API
|
| 14 |
+
# -------------------------
|
| 15 |
+
ANNBackend = str # "auto" | "faiss" | "pynndescent" | "sklearn" | "brute"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ANNBase:
|
| 19 |
+
"""Minimal ANN interface used by DMAP/GPLM."""
|
| 20 |
+
def build(self, X: np.ndarray) -> "ANNBase":
|
| 21 |
+
raise NotImplementedError
|
| 22 |
+
|
| 23 |
+
def search(self, Q: np.ndarray, k: int) -> Tuple[np.ndarray, np.ndarray]:
|
| 24 |
+
"""
|
| 25 |
+
Returns:
|
| 26 |
+
idx: (B,k) int64
|
| 27 |
+
D2 : (B,k) float32 (squared Euclidean distances)
|
| 28 |
+
"""
|
| 29 |
+
raise NotImplementedError
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def make_ann(
|
| 33 |
+
backend: ANNBackend = "auto",
|
| 34 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 35 |
+
n_jobs: int = -1,
|
| 36 |
+
) -> Tuple[ANNBase, str]:
|
| 37 |
+
"""
|
| 38 |
+
Create an ANN implementation.
|
| 39 |
+
backend:
|
| 40 |
+
- "auto": prefers faiss, then pynndescent, then sklearn, else brute
|
| 41 |
+
- "faiss": FAISS (if installed)
|
| 42 |
+
- "pynndescent": NNDescent (if installed)
|
| 43 |
+
- "sklearn": sklearn NearestNeighbors (if installed)
|
| 44 |
+
- "brute": exact brute force
|
| 45 |
+
ann_params:
|
| 46 |
+
- for faiss:
|
| 47 |
+
index: "flat" | "hnsw" | "ivf_flat"
|
| 48 |
+
hnsw_M: int (default 32)
|
| 49 |
+
ef_search: int (default 64)
|
| 50 |
+
ef_construction: int (default 200)
|
| 51 |
+
ivf_nlist: int (default 1024)
|
| 52 |
+
ivf_nprobe: int (default 16)
|
| 53 |
+
use_float16: bool (default False; GPU only typically)
|
| 54 |
+
- for pynndescent:
|
| 55 |
+
n_trees: int
|
| 56 |
+
n_iters: int
|
| 57 |
+
metric: str (default "euclidean")
|
| 58 |
+
- for sklearn:
|
| 59 |
+
algorithm: str (default "auto")
|
| 60 |
+
leaf_size: int (default 40)
|
| 61 |
+
metric: str (default "euclidean")
|
| 62 |
+
"""
|
| 63 |
+
ann_params = {} if ann_params is None else dict(ann_params)
|
| 64 |
+
b = (backend or "auto").lower()
|
| 65 |
+
|
| 66 |
+
if b == "auto":
|
| 67 |
+
for cand in ("faiss", "pynndescent", "sklearn", "brute"):
|
| 68 |
+
ann, used = make_ann(cand, ann_params=ann_params, n_jobs=n_jobs)
|
| 69 |
+
if used != "brute" or cand == "brute":
|
| 70 |
+
return ann, used
|
| 71 |
+
return BruteANN(), "brute"
|
| 72 |
+
|
| 73 |
+
if b == "faiss":
|
| 74 |
+
try:
|
| 75 |
+
return FaissANN(ann_params=ann_params), "faiss"
|
| 76 |
+
except Exception as e:
|
| 77 |
+
raise ImportError(
|
| 78 |
+
"FAISS backend requested but faiss is not available or failed to initialize. "
|
| 79 |
+
"Install with: pip install dima[faiss]"
|
| 80 |
+
) from e
|
| 81 |
+
|
| 82 |
+
if b == "pynndescent":
|
| 83 |
+
try:
|
| 84 |
+
return PyNNDescentANN(ann_params=ann_params), "pynndescent"
|
| 85 |
+
except Exception as e:
|
| 86 |
+
raise ImportError(
|
| 87 |
+
"pynndescent backend requested but pynndescent is not available. "
|
| 88 |
+
"Install with: pip install pynndescent"
|
| 89 |
+
) from e
|
| 90 |
+
|
| 91 |
+
if b == "sklearn":
|
| 92 |
+
try:
|
| 93 |
+
return SklearnANN(n_jobs=n_jobs, ann_params=ann_params), "sklearn"
|
| 94 |
+
except Exception as e:
|
| 95 |
+
raise ImportError(
|
| 96 |
+
"sklearn backend requested but scikit-learn is not available. "
|
| 97 |
+
"Install with: pip install scikit-learn"
|
| 98 |
+
) from e
|
| 99 |
+
|
| 100 |
+
if b == "brute":
|
| 101 |
+
return BruteANN(), "brute"
|
| 102 |
+
|
| 103 |
+
raise ValueError(f"Unknown ANN backend: {backend!r}")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# -------------------------
|
| 107 |
+
# Brute-force (no deps)
|
| 108 |
+
# -------------------------
|
| 109 |
+
class BruteANN(ANNBase):
|
| 110 |
+
def __init__(self):
|
| 111 |
+
self.X = None
|
| 112 |
+
|
| 113 |
+
def build(self, X: np.ndarray) -> "BruteANN":
|
| 114 |
+
self.X = as_contig_f32(X)
|
| 115 |
+
return self
|
| 116 |
+
|
| 117 |
+
def search(self, Q: np.ndarray, k: int) -> Tuple[np.ndarray, np.ndarray]:
|
| 118 |
+
if self.X is None:
|
| 119 |
+
raise RuntimeError("BruteANN.search called before build().")
|
| 120 |
+
X = self.X
|
| 121 |
+
Q = as_contig_f32(Q)
|
| 122 |
+
k = int(k)
|
| 123 |
+
if k <= 0:
|
| 124 |
+
raise ValueError("k must be >= 1")
|
| 125 |
+
if k > X.shape[0]:
|
| 126 |
+
k = X.shape[0]
|
| 127 |
+
|
| 128 |
+
D2 = sqdist_ab(Q, X) # (B,N)
|
| 129 |
+
idx = np.argpartition(D2, kth=k - 1, axis=1)[:, :k]
|
| 130 |
+
rows = np.arange(Q.shape[0])[:, None]
|
| 131 |
+
d2 = D2[rows, idx]
|
| 132 |
+
|
| 133 |
+
# sort within k
|
| 134 |
+
ordk = np.argsort(d2, axis=1)
|
| 135 |
+
idx = idx[rows, ordk].astype(np.int64)
|
| 136 |
+
d2 = d2[rows, ordk].astype(np.float32)
|
| 137 |
+
return idx, d2
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# -------------------------
|
| 141 |
+
# FAISS
|
| 142 |
+
# -------------------------
|
| 143 |
+
class FaissANN(ANNBase):
|
| 144 |
+
def __init__(self, ann_params: Optional[Dict[str, Any]] = None):
|
| 145 |
+
self.ann_params = {} if ann_params is None else dict(ann_params)
|
| 146 |
+
self.index = None
|
| 147 |
+
self.X = None # keep reference for possible rebuild
|
| 148 |
+
|
| 149 |
+
# delayed import
|
| 150 |
+
import faiss # type: ignore
|
| 151 |
+
self.faiss = faiss
|
| 152 |
+
|
| 153 |
+
def _build_index(self, d: int):
|
| 154 |
+
p = self.ann_params
|
| 155 |
+
faiss = self.faiss
|
| 156 |
+
|
| 157 |
+
index_kind = str(p.get("index", "flat")).lower()
|
| 158 |
+
|
| 159 |
+
if index_kind == "flat":
|
| 160 |
+
index = faiss.IndexFlatL2(d)
|
| 161 |
+
|
| 162 |
+
elif index_kind == "hnsw":
|
| 163 |
+
M = int(p.get("hnsw_M", 32))
|
| 164 |
+
index = faiss.IndexHNSWFlat(d, M)
|
| 165 |
+
# optional tuning
|
| 166 |
+
ef_search = int(p.get("ef_search", 64))
|
| 167 |
+
ef_constr = int(p.get("ef_construction", 200))
|
| 168 |
+
index.hnsw.efSearch = ef_search
|
| 169 |
+
index.hnsw.efConstruction = ef_constr
|
| 170 |
+
|
| 171 |
+
elif index_kind == "ivf_flat":
|
| 172 |
+
nlist = int(p.get("ivf_nlist", 1024))
|
| 173 |
+
quantizer = faiss.IndexFlatL2(d)
|
| 174 |
+
index = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_L2)
|
| 175 |
+
nprobe = int(p.get("ivf_nprobe", 16))
|
| 176 |
+
index.nprobe = nprobe
|
| 177 |
+
|
| 178 |
+
else:
|
| 179 |
+
raise ValueError(f"Unknown faiss index kind: {index_kind!r}")
|
| 180 |
+
|
| 181 |
+
return index
|
| 182 |
+
|
| 183 |
+
def build(self, X: np.ndarray) -> "FaissANN":
|
| 184 |
+
X = as_contig_f32(X)
|
| 185 |
+
self.X = X
|
| 186 |
+
faiss = self.faiss
|
| 187 |
+
d = int(X.shape[1])
|
| 188 |
+
|
| 189 |
+
index = self._build_index(d)
|
| 190 |
+
|
| 191 |
+
# IVF needs training
|
| 192 |
+
if hasattr(index, "is_trained") and not index.is_trained:
|
| 193 |
+
index.train(X)
|
| 194 |
+
|
| 195 |
+
index.add(X)
|
| 196 |
+
self.index = index
|
| 197 |
+
return self
|
| 198 |
+
|
| 199 |
+
def search(self, Q: np.ndarray, k: int) -> Tuple[np.ndarray, np.ndarray]:
|
| 200 |
+
if self.index is None:
|
| 201 |
+
raise RuntimeError("FaissANN.search called before build().")
|
| 202 |
+
Q = as_contig_f32(Q)
|
| 203 |
+
k = int(k)
|
| 204 |
+
if k <= 0:
|
| 205 |
+
raise ValueError("k must be >= 1")
|
| 206 |
+
|
| 207 |
+
# FAISS returns (distances, indices); for L2 these are squared distances
|
| 208 |
+
D2, I = self.index.search(Q, k)
|
| 209 |
+
return I.astype(np.int64), D2.astype(np.float32)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# -------------------------
|
| 213 |
+
# PyNNDescent
|
| 214 |
+
# -------------------------
|
| 215 |
+
class PyNNDescentANN(ANNBase):
|
| 216 |
+
def __init__(self, ann_params: Optional[Dict[str, Any]] = None):
|
| 217 |
+
self.ann_params = {} if ann_params is None else dict(ann_params)
|
| 218 |
+
self.index = None
|
| 219 |
+
self.X = None
|
| 220 |
+
|
| 221 |
+
from pynndescent import NNDescent # type: ignore
|
| 222 |
+
self.NNDescent = NNDescent
|
| 223 |
+
|
| 224 |
+
def build(self, X: np.ndarray) -> "PyNNDescentANN":
|
| 225 |
+
X = as_contig_f32(X)
|
| 226 |
+
self.X = X
|
| 227 |
+
p = self.ann_params
|
| 228 |
+
|
| 229 |
+
metric = p.get("metric", "euclidean")
|
| 230 |
+
n_trees = p.get("n_trees", None)
|
| 231 |
+
n_iters = p.get("n_iters", None)
|
| 232 |
+
|
| 233 |
+
kwargs: Dict[str, Any] = {"metric": metric}
|
| 234 |
+
if n_trees is not None:
|
| 235 |
+
kwargs["n_trees"] = int(n_trees)
|
| 236 |
+
if n_iters is not None:
|
| 237 |
+
kwargs["n_iters"] = int(n_iters)
|
| 238 |
+
|
| 239 |
+
self.index = self.NNDescent(X, **kwargs)
|
| 240 |
+
return self
|
| 241 |
+
|
| 242 |
+
def search(self, Q: np.ndarray, k: int) -> Tuple[np.ndarray, np.ndarray]:
|
| 243 |
+
if self.index is None:
|
| 244 |
+
raise RuntimeError("PyNNDescentANN.search called before build().")
|
| 245 |
+
Q = as_contig_f32(Q)
|
| 246 |
+
k = int(k)
|
| 247 |
+
if k <= 0:
|
| 248 |
+
raise ValueError("k must be >= 1")
|
| 249 |
+
|
| 250 |
+
# NNDescent returns (indices, distances) with euclidean distances (not squared)
|
| 251 |
+
I, d = self.index.query(Q, k=k)
|
| 252 |
+
D2 = (d.astype(np.float32) ** 2)
|
| 253 |
+
return I.astype(np.int64), D2
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
# -------------------------
|
| 257 |
+
# scikit-learn NearestNeighbors
|
| 258 |
+
# -------------------------
|
| 259 |
+
class SklearnANN(ANNBase):
|
| 260 |
+
def __init__(self, n_jobs: int = -1, ann_params: Optional[Dict[str, Any]] = None):
|
| 261 |
+
self.ann_params = {} if ann_params is None else dict(ann_params)
|
| 262 |
+
self.n_jobs = int(n_jobs)
|
| 263 |
+
self.nn = None
|
| 264 |
+
self.X = None
|
| 265 |
+
|
| 266 |
+
from sklearn.neighbors import NearestNeighbors # type: ignore
|
| 267 |
+
self.NearestNeighbors = NearestNeighbors
|
| 268 |
+
|
| 269 |
+
def build(self, X: np.ndarray) -> "SklearnANN":
|
| 270 |
+
X = as_contig_f32(X)
|
| 271 |
+
self.X = X
|
| 272 |
+
p = self.ann_params
|
| 273 |
+
|
| 274 |
+
algorithm = p.get("algorithm", "auto")
|
| 275 |
+
leaf_size = int(p.get("leaf_size", 40))
|
| 276 |
+
metric = p.get("metric", "euclidean")
|
| 277 |
+
|
| 278 |
+
self.nn = self.NearestNeighbors(
|
| 279 |
+
n_neighbors=1, # set later in search
|
| 280 |
+
algorithm=algorithm,
|
| 281 |
+
leaf_size=leaf_size,
|
| 282 |
+
metric=metric,
|
| 283 |
+
n_jobs=self.n_jobs,
|
| 284 |
+
)
|
| 285 |
+
self.nn.fit(X)
|
| 286 |
+
return self
|
| 287 |
+
|
| 288 |
+
def search(self, Q: np.ndarray, k: int) -> Tuple[np.ndarray, np.ndarray]:
|
| 289 |
+
if self.nn is None:
|
| 290 |
+
raise RuntimeError("SklearnANN.search called before build().")
|
| 291 |
+
Q = as_contig_f32(Q)
|
| 292 |
+
k = int(k)
|
| 293 |
+
if k <= 0:
|
| 294 |
+
raise ValueError("k must be >= 1")
|
| 295 |
+
|
| 296 |
+
self.nn.set_params(n_neighbors=k)
|
| 297 |
+
d, I = self.nn.kneighbors(Q, return_distance=True)
|
| 298 |
+
# sklearn distances are euclidean; square them
|
| 299 |
+
D2 = (d.astype(np.float32) ** 2)
|
| 300 |
+
return I.astype(np.int64), D2
|
ddim.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- src/dima/ddpm.py
|
| 2 |
+
# +++ src/dima/ddpm.py
|
| 3 |
+
# @@ -368,4 +368,204 @@
|
| 4 |
+
# return self.reverse_from_T(noise)
|
| 5 |
+
# src/dima/ddpm.py
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from typing import Any, Optional
|
| 9 |
+
|
| 10 |
+
import jax
|
| 11 |
+
import jax.numpy as jnp
|
| 12 |
+
from jax import random
|
| 13 |
+
|
| 14 |
+
from flax import linen as nn
|
| 15 |
+
from flax.training import train_state
|
| 16 |
+
from flax import struct
|
| 17 |
+
import optax
|
| 18 |
+
from .ann import ANNBackend, make_ann
|
| 19 |
+
from .ddpm import DDPM
|
| 20 |
+
# +from .ddpm import DDPM, DDIM
|
| 21 |
+
from .dmap import DMAP
|
| 22 |
+
from .gplm import GPLM
|
| 23 |
+
|
| 24 |
+
__all__ = ["DDPM", "EpsMLP", "cosine_schedule", "sinusoidal_embedding"]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ------------------------------
|
| 28 |
+
# DDIM (deterministic sampler; eta=0.0)
|
| 29 |
+
# ------------------------------
|
| 30 |
+
|
| 31 |
+
class DDIM(DDPM):
|
| 32 |
+
"""
|
| 33 |
+
DDIM sampler for D-dimensional latents.
|
| 34 |
+
|
| 35 |
+
Notes
|
| 36 |
+
-----
|
| 37 |
+
* Training is identical to DDPM (epsilon prediction).
|
| 38 |
+
* Sampling / refinement uses the DDIM update:
|
| 39 |
+
x_{t_prev} = sqrt(a_bar_prev) * x0_hat
|
| 40 |
+
+ sqrt(1 - a_bar_prev - sigma^2) * eps_pred
|
| 41 |
+
+ sigma * z,
|
| 42 |
+
where sigma = eta * sqrt( (1-a_bar_prev)/(1-a_bar_t) * (1 - a_bar_t/a_bar_prev) ).
|
| 43 |
+
|
| 44 |
+
With eta=0.0 the reverse process is deterministic conditioned on x_t.
|
| 45 |
+
|
| 46 |
+
Added (HF I/O):
|
| 47 |
+
- state_dict / save_local / load_local / from_state
|
| 48 |
+
- upload_to_huggingface / download_from_huggingface
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
def __init__(
|
| 52 |
+
self,
|
| 53 |
+
Z_iX: jnp.ndarray,
|
| 54 |
+
*,
|
| 55 |
+
eta: float = 0.0,
|
| 56 |
+
steps: Optional[int] = None,
|
| 57 |
+
**ddpm_kwargs: Any,
|
| 58 |
+
):
|
| 59 |
+
super().__init__(Z_iX, **ddpm_kwargs)
|
| 60 |
+
self.eta = float(eta)
|
| 61 |
+
self.steps = None if steps is None else int(steps)
|
| 62 |
+
|
| 63 |
+
@staticmethod
|
| 64 |
+
def _make_ddim_step(params_ema, apply_fn, alpha_bar_s, eps: float, eta: float):
|
| 65 |
+
eta = float(eta)
|
| 66 |
+
|
| 67 |
+
@jax.jit
|
| 68 |
+
def step(carry, t_pair):
|
| 69 |
+
key, x = carry # x: (B, D)
|
| 70 |
+
t, t_prev = t_pair # scalar int32
|
| 71 |
+
|
| 72 |
+
# gather ā_t and ā_prev (supports skipping)
|
| 73 |
+
a_bar_t = jnp.clip(alpha_bar_s[t], eps, 1.0)
|
| 74 |
+
a_bar_prev = jnp.clip(alpha_bar_s[t_prev], eps, 1.0)
|
| 75 |
+
|
| 76 |
+
B = x.shape[0]
|
| 77 |
+
t_batch = jnp.full((B, 1), t, dtype=jnp.int32)
|
| 78 |
+
|
| 79 |
+
eps_pred = apply_fn({"params": params_ema}, x, t_batch) # (B, D)
|
| 80 |
+
|
| 81 |
+
# x0 estimate
|
| 82 |
+
sqrt_one_minus = jnp.sqrt(jnp.clip(1.0 - a_bar_t, eps, 1.0))
|
| 83 |
+
x0_hat = (x - sqrt_one_minus * eps_pred) / jnp.sqrt(a_bar_t)
|
| 84 |
+
|
| 85 |
+
# sigma_t (controls stochasticity). eta=0 => sigma=0 (deterministic)
|
| 86 |
+
frac = (1.0 - a_bar_prev) / jnp.clip(1.0 - a_bar_t, eps, 1.0)
|
| 87 |
+
inside = 1.0 - a_bar_t / jnp.clip(a_bar_prev, eps, 1.0)
|
| 88 |
+
sigma = eta * jnp.sqrt(jnp.clip(frac * inside, 0.0, 1.0))
|
| 89 |
+
|
| 90 |
+
# direction term
|
| 91 |
+
dir_coeff = jnp.sqrt(jnp.clip(1.0 - a_bar_prev - sigma**2, 0.0, 1.0))
|
| 92 |
+
x_prev = jnp.sqrt(a_bar_prev) * x0_hat + dir_coeff * eps_pred
|
| 93 |
+
|
| 94 |
+
# optional noise injection (eta>0)
|
| 95 |
+
if eta > 0.0:
|
| 96 |
+
key, k = random.split(key)
|
| 97 |
+
z = random.normal(k, x.shape)
|
| 98 |
+
x_prev = x_prev + sigma * z
|
| 99 |
+
|
| 100 |
+
return (key, x_prev), x_prev
|
| 101 |
+
|
| 102 |
+
return step
|
| 103 |
+
|
| 104 |
+
def _t_schedule(self, t_start: int, *, steps: Optional[int] = None) -> jnp.ndarray:
|
| 105 |
+
"""
|
| 106 |
+
Descending integer timesteps from t_start -> 0.
|
| 107 |
+
|
| 108 |
+
If steps is provided (or self.steps), we use a reduced DDIM schedule
|
| 109 |
+
by rounding a linear grid and making it unique.
|
| 110 |
+
"""
|
| 111 |
+
t_start = int(t_start)
|
| 112 |
+
if t_start <= 0:
|
| 113 |
+
return jnp.array([0], dtype=jnp.int32)
|
| 114 |
+
|
| 115 |
+
S = self.steps if steps is None else int(steps)
|
| 116 |
+
if S is None or S >= (t_start + 1):
|
| 117 |
+
return jnp.arange(t_start, -1, -1, dtype=jnp.int32)
|
| 118 |
+
|
| 119 |
+
grid = jnp.linspace(float(t_start), 0.0, int(S), dtype=jnp.float32)
|
| 120 |
+
ts = jnp.unique(jnp.round(grid).astype(jnp.int32))
|
| 121 |
+
ts = ts[::-1] # descending
|
| 122 |
+
|
| 123 |
+
# guarantee endpoints
|
| 124 |
+
if ts[0] != t_start:
|
| 125 |
+
ts = jnp.concatenate([jnp.array([t_start], dtype=jnp.int32), ts])
|
| 126 |
+
if ts[-1] != 0:
|
| 127 |
+
ts = jnp.concatenate([ts, jnp.array([0], dtype=jnp.int32)])
|
| 128 |
+
return ts
|
| 129 |
+
|
| 130 |
+
def refine_latents(
|
| 131 |
+
self,
|
| 132 |
+
z0: jnp.ndarray,
|
| 133 |
+
t_start: int = 10,
|
| 134 |
+
key: Optional[jax.Array] = None,
|
| 135 |
+
add_noise: bool = True,
|
| 136 |
+
*,
|
| 137 |
+
eta: Optional[float] = None,
|
| 138 |
+
steps: Optional[int] = None,
|
| 139 |
+
) -> jnp.ndarray:
|
| 140 |
+
"""
|
| 141 |
+
Refine latents by:
|
| 142 |
+
(optional) forward-noise z0 to step t_start
|
| 143 |
+
DDIM reverse from t_start -> 0 using EMA params.
|
| 144 |
+
"""
|
| 145 |
+
z0 = jnp.asarray(z0, dtype=jnp.float32)
|
| 146 |
+
if z0.ndim != 2 or z0.shape[1] != self.D:
|
| 147 |
+
raise ValueError(f"z0 must have shape (B,{self.D}).")
|
| 148 |
+
if not (0 <= int(t_start) < self.T):
|
| 149 |
+
raise ValueError(f"t_start must be in [0, {self.T-1}]")
|
| 150 |
+
t_start = int(t_start)
|
| 151 |
+
|
| 152 |
+
# RNG
|
| 153 |
+
if key is None:
|
| 154 |
+
self.key, key = random.split(self.key)
|
| 155 |
+
else:
|
| 156 |
+
# advance internal RNG too
|
| 157 |
+
self.key, _ = random.split(key)
|
| 158 |
+
|
| 159 |
+
# optional forward noise
|
| 160 |
+
if add_noise and t_start > 0:
|
| 161 |
+
key, k_eps = random.split(key)
|
| 162 |
+
eps_noise = random.normal(k_eps, z0.shape)
|
| 163 |
+
a_bar_t = jnp.clip(self.alpha_bar_s[t_start], self.eps, 1.0) # Corrected attribute name
|
| 164 |
+
z_t = jnp.sqrt(a_bar_t) * z0 + jnp.sqrt(1.0 - a_bar_t) * eps_noise
|
| 165 |
+
else:
|
| 166 |
+
z_t = z0
|
| 167 |
+
|
| 168 |
+
# schedule
|
| 169 |
+
ts = self._t_schedule(t_start, steps=steps)
|
| 170 |
+
if ts.shape[0] == 1:
|
| 171 |
+
return z_t
|
| 172 |
+
|
| 173 |
+
t_pairs = jnp.stack([ts[:-1], ts[1:]], axis=1) # (K,2)
|
| 174 |
+
|
| 175 |
+
step = self._make_ddim_step(
|
| 176 |
+
self.state.ema_params,
|
| 177 |
+
self.state.apply_fn,
|
| 178 |
+
self.alpha_bar_s, # Corrected attribute name
|
| 179 |
+
self.eps,
|
| 180 |
+
self.eta if eta is None else float(eta),
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
(final_key, _), trace = jax.lax.scan(step, (key, z_t), xs=t_pairs)
|
| 184 |
+
|
| 185 |
+
self.key = final_key
|
| 186 |
+
return trace[-1]
|
| 187 |
+
|
| 188 |
+
def __call__(
|
| 189 |
+
self,
|
| 190 |
+
z0: jnp.ndarray,
|
| 191 |
+
t_start: int = 10,
|
| 192 |
+
key: Optional[jax.Array] = None,
|
| 193 |
+
add_noise: bool = True,
|
| 194 |
+
*,
|
| 195 |
+
eta: Optional[float] = None,
|
| 196 |
+
steps: Optional[int] = None,
|
| 197 |
+
) -> jnp.ndarray:
|
| 198 |
+
return self.refine_latents(
|
| 199 |
+
z0, t_start=t_start, key=key, add_noise=add_noise, eta=eta, steps=steps
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
def reverse_from_T(self, x_T: jnp.ndarray, *, eta: Optional[float] = None, steps: Optional[int] = None) -> jnp.ndarray:
|
| 203 |
+
x_T = jnp.asarray(x_T, dtype=jnp.float32)
|
| 204 |
+
if x_T.ndim != 2 or x_T.shape[1] != self.D:
|
| 205 |
+
raise ValueError(f"x_T must have shape (B,{self.D}).")
|
| 206 |
+
|
| 207 |
+
self.key, k0 = random.split(self.key)
|
| 208 |
+
|
| 209 |
+
ts = self._t_schedule(self.T - 1, steps=steps)
|
| 210 |
+
t_pairs = jnp.stack([ts[:-1], ts[1:]], axis=1)
|
| 211 |
+
|
| 212 |
+
step = self._make_ddim_step(
|
| 213 |
+
self.state.ema_params,
|
| 214 |
+
self.state.apply_fn,
|
| 215 |
+
self.alpha_bar_s, # Corrected attribute name
|
| 216 |
+
self.eps,
|
| 217 |
+
self.eta if eta is None else float(eta),
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
(_, _), trace = jax.lax.scan(step, (k0, x_T), xs=t_pairs)
|
| 221 |
+
return trace[-1]
|
| 222 |
+
|
| 223 |
+
def sample(self, N: int = 10_000, *, eta: Optional[float] = None, steps: Optional[int] = None) -> jnp.ndarray:
|
| 224 |
+
self.key, k = random.split(self.key)
|
| 225 |
+
noise = random.normal(k, (int(N), self.D))
|
| 226 |
+
return self.reverse_from_T(noise, eta=eta, steps=steps)
|
| 227 |
+
|
| 228 |
+
# ------------------------------------------------------------------
|
| 229 |
+
# Serialization + Hugging Face Hub I/O (integrated)
|
| 230 |
+
# ------------------------------------------------------------------
|
| 231 |
+
|
| 232 |
+
def state_dict(self):
|
| 233 |
+
"""
|
| 234 |
+
Msgpack-safe checkpoint dict (includes DDIM-specific eta/steps).
|
| 235 |
+
Uses flax.serialization.to_state_dict for TrainStateEMA to avoid tuple issues (opt_state).
|
| 236 |
+
"""
|
| 237 |
+
from flax import serialization as flax_ser # type: ignore
|
| 238 |
+
import numpy as np
|
| 239 |
+
|
| 240 |
+
return dict(
|
| 241 |
+
# DDPM core hyperparameters
|
| 242 |
+
T=int(self.T),
|
| 243 |
+
D=int(self.D),
|
| 244 |
+
hidden_dim=int(getattr(self, "hidden_dim", 0) or self.hidden_dim),
|
| 245 |
+
t_embed_dim=int(getattr(self, "t_embed_dim", 0) or self.t_embed_dim),
|
| 246 |
+
learning_rate=float(self.learning_rate),
|
| 247 |
+
ema_decay=float(self.ema_decay),
|
| 248 |
+
beta_max=float(self.beta_max),
|
| 249 |
+
batch_size=None if (self.batch_size is None) else int(self.batch_size),
|
| 250 |
+
eps=float(self.eps),
|
| 251 |
+
|
| 252 |
+
# DDIM specifics
|
| 253 |
+
eta=float(self.eta),
|
| 254 |
+
steps=None if (self.steps is None) else int(self.steps),
|
| 255 |
+
|
| 256 |
+
# RNG key (portable)
|
| 257 |
+
key=np.asarray(self.key),
|
| 258 |
+
|
| 259 |
+
# TrainStateEMA (params, ema_params, opt_state, step)
|
| 260 |
+
train_state=flax_ser.to_state_dict(self.state),
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
def save_local(self, weights_file: str = "ddim.msgpack", config_file: Optional[str] = "ddim_config.json") -> None:
|
| 264 |
+
from flax import serialization as flax_ser # type: ignore
|
| 265 |
+
import json as _json
|
| 266 |
+
|
| 267 |
+
ckpt = self.state_dict()
|
| 268 |
+
blob = flax_ser.msgpack_serialize(ckpt)
|
| 269 |
+
|
| 270 |
+
with open(weights_file, "wb") as f:
|
| 271 |
+
f.write(blob)
|
| 272 |
+
|
| 273 |
+
if config_file is not None:
|
| 274 |
+
cfg = dict(
|
| 275 |
+
T=int(ckpt["T"]),
|
| 276 |
+
D=int(ckpt["D"]),
|
| 277 |
+
hidden_dim=int(ckpt["hidden_dim"]),
|
| 278 |
+
t_embed_dim=int(ckpt["t_embed_dim"]),
|
| 279 |
+
learning_rate=float(ckpt["learning_rate"]),
|
| 280 |
+
ema_decay=float(ckpt["ema_decay"]),
|
| 281 |
+
beta_max=float(ckpt["beta_max"]),
|
| 282 |
+
batch_size=ckpt["batch_size"],
|
| 283 |
+
eps=float(ckpt["eps"]),
|
| 284 |
+
eta=float(ckpt["eta"]),
|
| 285 |
+
steps=ckpt["steps"],
|
| 286 |
+
)
|
| 287 |
+
with open(config_file, "w") as f:
|
| 288 |
+
_json.dump(cfg, f, indent=2)
|
| 289 |
+
|
| 290 |
+
@classmethod
|
| 291 |
+
def from_state(cls, ckpt, *, key: Optional[jax.Array] = None) -> "DDIM":
|
| 292 |
+
"""
|
| 293 |
+
Rehydrate a DDIM instance without retraining:
|
| 294 |
+
- rebuilds a skeleton DDIM (which builds schedules/model/optimizer)
|
| 295 |
+
- restores TrainStateEMA via flax.serialization.from_state_dict
|
| 296 |
+
"""
|
| 297 |
+
from flax import serialization as flax_ser # type: ignore
|
| 298 |
+
import numpy as np
|
| 299 |
+
|
| 300 |
+
T = int(ckpt["T"])
|
| 301 |
+
D = int(ckpt["D"])
|
| 302 |
+
hidden_dim = int(ckpt["hidden_dim"])
|
| 303 |
+
t_embed_dim = int(ckpt["t_embed_dim"])
|
| 304 |
+
learning_rate = float(ckpt["learning_rate"])
|
| 305 |
+
ema_decay = float(ckpt["ema_decay"])
|
| 306 |
+
beta_max = float(ckpt["beta_max"])
|
| 307 |
+
batch_size = ckpt.get("batch_size", None)
|
| 308 |
+
eps = float(ckpt.get("eps", 1e-5))
|
| 309 |
+
|
| 310 |
+
eta = float(ckpt.get("eta", 0.0))
|
| 311 |
+
steps = ckpt.get("steps", None)
|
| 312 |
+
steps = None if (steps is None) else int(steps)
|
| 313 |
+
|
| 314 |
+
if key is None:
|
| 315 |
+
if "key" in ckpt:
|
| 316 |
+
key = jnp.asarray(np.asarray(ckpt["key"]))
|
| 317 |
+
else:
|
| 318 |
+
key = random.PRNGKey(0)
|
| 319 |
+
|
| 320 |
+
# skeleton (no training)
|
| 321 |
+
dummy = jnp.zeros((1, D), dtype=jnp.float32)
|
| 322 |
+
obj = cls(
|
| 323 |
+
dummy,
|
| 324 |
+
T=T,
|
| 325 |
+
hidden_dim=hidden_dim,
|
| 326 |
+
t_embed_dim=t_embed_dim,
|
| 327 |
+
learning_rate=learning_rate,
|
| 328 |
+
n_iter=0,
|
| 329 |
+
ema_decay=ema_decay,
|
| 330 |
+
beta_max=beta_max,
|
| 331 |
+
batch_size=batch_size,
|
| 332 |
+
key=key,
|
| 333 |
+
verbose_every=0,
|
| 334 |
+
eps=eps,
|
| 335 |
+
eta=eta,
|
| 336 |
+
steps=steps,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
obj.state = flax_ser.from_state_dict(obj.state, ckpt["train_state"])
|
| 340 |
+
|
| 341 |
+
# restore RNG key and DDIM attrs
|
| 342 |
+
if "key" in ckpt:
|
| 343 |
+
obj.key = jnp.asarray(np.asarray(ckpt["key"]))
|
| 344 |
+
obj.eta = eta
|
| 345 |
+
obj.steps = steps
|
| 346 |
+
|
| 347 |
+
return obj
|
| 348 |
+
|
| 349 |
+
@classmethod
|
| 350 |
+
def load_local(cls, weights_file: str = "ddim.msgpack", *, key: Optional[jax.Array] = None) -> "DDIM":
|
| 351 |
+
from flax import serialization as flax_ser # type: ignore
|
| 352 |
+
|
| 353 |
+
with open(weights_file, "rb") as f:
|
| 354 |
+
ckpt = flax_ser.msgpack_restore(f.read())
|
| 355 |
+
return cls.from_state(ckpt, key=key)
|
| 356 |
+
|
| 357 |
+
def upload_to_huggingface(
|
| 358 |
+
self,
|
| 359 |
+
repo_id: str,
|
| 360 |
+
*,
|
| 361 |
+
weights_file: str = "ddim.msgpack",
|
| 362 |
+
config_file: str = "ddim_config.json",
|
| 363 |
+
token: Optional[str] = None,
|
| 364 |
+
repo_type: str = "model",
|
| 365 |
+
revision: Optional[str] = None,
|
| 366 |
+
) -> None:
|
| 367 |
+
"""
|
| 368 |
+
Upload DDIM checkpoint to HF Hub (msgpack + JSON).
|
| 369 |
+
Mirrors the DMAP/DDPM pattern.
|
| 370 |
+
"""
|
| 371 |
+
try:
|
| 372 |
+
from huggingface_hub import HfApi, HfFolder, upload_file # type: ignore
|
| 373 |
+
except Exception as e:
|
| 374 |
+
raise RuntimeError("huggingface_hub not installed. Install it (or `pip install dima[hf]`).") from e
|
| 375 |
+
|
| 376 |
+
self.save_local(weights_file=weights_file, config_file=config_file)
|
| 377 |
+
|
| 378 |
+
if token is None:
|
| 379 |
+
token = HfFolder.get_token()
|
| 380 |
+
if token is None:
|
| 381 |
+
raise RuntimeError("No HF token found. Run `huggingface-cli login`, or pass `token=...`.")
|
| 382 |
+
|
| 383 |
+
import os as _os
|
| 384 |
+
|
| 385 |
+
api = HfApi()
|
| 386 |
+
api.create_repo(repo_id=repo_id, repo_type=repo_type, exist_ok=True, token=token)
|
| 387 |
+
|
| 388 |
+
wf = _os.path.basename(weights_file)
|
| 389 |
+
cf = _os.path.basename(config_file)
|
| 390 |
+
|
| 391 |
+
upload_file(
|
| 392 |
+
path_or_fileobj=weights_file,
|
| 393 |
+
path_in_repo=wf,
|
| 394 |
+
repo_id=repo_id,
|
| 395 |
+
token=token,
|
| 396 |
+
repo_type=repo_type,
|
| 397 |
+
revision=revision,
|
| 398 |
+
)
|
| 399 |
+
upload_file(
|
| 400 |
+
path_or_fileobj=config_file,
|
| 401 |
+
path_in_repo=cf,
|
| 402 |
+
repo_id=repo_id,
|
| 403 |
+
token=token,
|
| 404 |
+
repo_type=repo_type,
|
| 405 |
+
revision=revision,
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
@classmethod
|
| 409 |
+
def download_from_huggingface(
|
| 410 |
+
cls,
|
| 411 |
+
repo_id: str,
|
| 412 |
+
*,
|
| 413 |
+
weights_file: str = "ddim.msgpack",
|
| 414 |
+
token: Optional[str] = None,
|
| 415 |
+
repo_type: str = "model",
|
| 416 |
+
revision: Optional[str] = None,
|
| 417 |
+
key: Optional[jax.Array] = None,
|
| 418 |
+
) -> "DDIM":
|
| 419 |
+
"""
|
| 420 |
+
Download DDIM checkpoint from HF Hub and return a rehydrated DDIM instance.
|
| 421 |
+
"""
|
| 422 |
+
try:
|
| 423 |
+
from huggingface_hub import hf_hub_download # type: ignore
|
| 424 |
+
except Exception as e:
|
| 425 |
+
raise RuntimeError("huggingface_hub not installed. Install it (or `pip install dima[hf]`).") from e
|
| 426 |
+
|
| 427 |
+
path = hf_hub_download(
|
| 428 |
+
repo_id=repo_id,
|
| 429 |
+
filename=weights_file,
|
| 430 |
+
token=token,
|
| 431 |
+
repo_type=repo_type,
|
| 432 |
+
revision=revision,
|
| 433 |
+
)
|
| 434 |
+
return cls.load_local(path, key=key)
|
| 435 |
+
|
| 436 |
+
# optional alias for naming symmetry
|
| 437 |
+
download_to_huggingface = download_from_huggingface
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
__all__ = ["DDPM", "DDIM", "EpsMLP", "cosine_schedule", "sinusoidal_embedding"]
|
| 441 |
+
|
| 442 |
+
# --- src/dima/dima.py
|
| 443 |
+
# +++ src/dima/dima.py
|
| 444 |
+
# @@ -15,7 +15,7 @@
|
| 445 |
+
# from flax import serialization as flax_ser
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
# @@ -374,7 +374,13 @@
|
| 450 |
+
# ddpm_init["eps"] = float(ddpm_eps)
|
| 451 |
+
|
| 452 |
+
# with jax.default_device(self.ddpm_device):
|
| 453 |
+
# - self.dm = DDPM(Z_ix_j, **ddpm_init)
|
| 454 |
+
# + sampler = str(ddpm_init.pop("sampler", "ddpm")).lower()
|
| 455 |
+
# + if sampler == "ddim":
|
| 456 |
+
# + eta = float(ddpm_init.pop("eta", 0.0))
|
| 457 |
+
# + steps = ddpm_init.pop("steps", None)
|
| 458 |
+
# + self.dm = DDIM(Z_ix_j, eta=eta, steps=steps, **ddpm_init)
|
| 459 |
+
# + else:
|
| 460 |
+
# + self.dm = DDPM(Z_ix_j, **ddpm_init)
|
| 461 |
+
|
| 462 |
+
# self.training_time = time.time() - t0
|
| 463 |
+
|
ddpm.py
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/dima/ddpm.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import Any, Optional
|
| 5 |
+
|
| 6 |
+
import jax
|
| 7 |
+
import jax.numpy as jnp
|
| 8 |
+
from jax import random
|
| 9 |
+
|
| 10 |
+
from flax import linen as nn
|
| 11 |
+
from flax.training import train_state
|
| 12 |
+
from flax import struct
|
| 13 |
+
import optax
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# ------------------------------
|
| 17 |
+
# Schedules & embeddings
|
| 18 |
+
# ------------------------------
|
| 19 |
+
def cosine_schedule(T: int, s: float = 0.008):
|
| 20 |
+
"""
|
| 21 |
+
Nichol & Dhariwal cosine schedule.
|
| 22 |
+
Returns:
|
| 23 |
+
alpha: (T,)
|
| 24 |
+
beta: (T,)
|
| 25 |
+
alpha_bar: (T,)
|
| 26 |
+
"""
|
| 27 |
+
steps = jnp.arange(T + 1, dtype=jnp.float32)
|
| 28 |
+
f = jnp.cos(((steps / T + s) / (1.0 + s)) * jnp.pi / 2.0) ** 2
|
| 29 |
+
alpha_bar_all = f / f[0]
|
| 30 |
+
alpha_bar = alpha_bar_all[1:] # (T,)
|
| 31 |
+
alpha = alpha_bar / jnp.concatenate([jnp.array([1.0], dtype=jnp.float32), alpha_bar[:-1]])
|
| 32 |
+
beta = 1.0 - alpha
|
| 33 |
+
return alpha, beta, alpha_bar
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def sinusoidal_embedding(t_idx: jnp.ndarray, dim: int) -> jnp.ndarray:
|
| 37 |
+
"""
|
| 38 |
+
t_idx: (B,1) int32
|
| 39 |
+
returns: (B,dim)
|
| 40 |
+
"""
|
| 41 |
+
if t_idx.ndim != 2 or t_idx.shape[1] != 1:
|
| 42 |
+
raise ValueError("t_idx must have shape (B,1)")
|
| 43 |
+
t = t_idx.astype(jnp.float32)
|
| 44 |
+
half = dim // 2
|
| 45 |
+
denom = float(max(half - 1, 1))
|
| 46 |
+
freqs = jnp.exp(-jnp.log(10_000.0) * jnp.arange(half, dtype=jnp.float32) / denom)
|
| 47 |
+
args = t * freqs
|
| 48 |
+
emb = jnp.concatenate([jnp.sin(args), jnp.cos(args)], axis=-1)
|
| 49 |
+
if dim % 2 == 1:
|
| 50 |
+
emb = jnp.pad(emb, ((0, 0), (0, 1)))
|
| 51 |
+
return emb
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ------------------------------
|
| 55 |
+
# Model: epsilon predictor
|
| 56 |
+
# ------------------------------
|
| 57 |
+
class EpsMLP(nn.Module):
|
| 58 |
+
"""Simple MLP epsilon-predictor for DDPM in R^D."""
|
| 59 |
+
hidden: int
|
| 60 |
+
t_dim: int
|
| 61 |
+
data_dim: int
|
| 62 |
+
|
| 63 |
+
@nn.compact
|
| 64 |
+
def __call__(self, x: jnp.ndarray, t_idx: jnp.ndarray) -> jnp.ndarray:
|
| 65 |
+
# x: (B,D), t_idx: (B,1)
|
| 66 |
+
t_emb = sinusoidal_embedding(t_idx, self.t_dim)
|
| 67 |
+
t_h = nn.Dense(self.hidden)(t_emb)
|
| 68 |
+
t_h = nn.gelu(t_h)
|
| 69 |
+
|
| 70 |
+
h = nn.Dense(self.hidden)(x)
|
| 71 |
+
h = nn.gelu(h + t_h)
|
| 72 |
+
|
| 73 |
+
t_h2 = nn.Dense(self.hidden)(t_h)
|
| 74 |
+
h = nn.Dense(self.hidden)(h)
|
| 75 |
+
h = nn.gelu(h + t_h2)
|
| 76 |
+
|
| 77 |
+
out = nn.Dense(self.data_dim)(h)
|
| 78 |
+
return out
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ------------------------------
|
| 82 |
+
# TrainState with EMA
|
| 83 |
+
# ------------------------------
|
| 84 |
+
@struct.dataclass
|
| 85 |
+
class TrainStateEMA(train_state.TrainState):
|
| 86 |
+
"""Flax TrainState extended with EMA params."""
|
| 87 |
+
ema_params: Any = struct.field(pytree_node=True)
|
| 88 |
+
|
| 89 |
+
def apply_gradients(self, *, grads, ema_decay: float):
|
| 90 |
+
updates, new_opt_state = self.tx.update(grads, self.opt_state, self.params)
|
| 91 |
+
new_params = optax.apply_updates(self.params, updates)
|
| 92 |
+
new_ema = optax.incremental_update(new_params, self.ema_params, step_size=1.0 - ema_decay)
|
| 93 |
+
return self.replace(
|
| 94 |
+
step=self.step + 1,
|
| 95 |
+
params=new_params,
|
| 96 |
+
opt_state=new_opt_state,
|
| 97 |
+
ema_params=new_ema,
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# ------------------------------
|
| 102 |
+
# DDPM
|
| 103 |
+
# ------------------------------
|
| 104 |
+
class DDPM:
|
| 105 |
+
"""
|
| 106 |
+
DDPM for D-dimensional latents.
|
| 107 |
+
|
| 108 |
+
API expected by your DIMA wrapper:
|
| 109 |
+
- DDPM(Z_train, ...): trains in __init__ (n_iter can be 0 to skip)
|
| 110 |
+
- refine_latents(z0, t_start, key, add_noise) -> z_refined
|
| 111 |
+
- __call__(...) delegates to refine_latents
|
| 112 |
+
- sample(N) -> latent samples
|
| 113 |
+
|
| 114 |
+
Added:
|
| 115 |
+
- state_dict / save_local / load_local / from_state
|
| 116 |
+
- upload_to_huggingface / download_from_huggingface
|
| 117 |
+
"""
|
| 118 |
+
|
| 119 |
+
def __init__(
|
| 120 |
+
self,
|
| 121 |
+
Z_iX: jnp.ndarray,
|
| 122 |
+
*,
|
| 123 |
+
T: int = 100,
|
| 124 |
+
hidden_dim: int = 128,
|
| 125 |
+
t_embed_dim: int = 64,
|
| 126 |
+
learning_rate: float = 1e-3,
|
| 127 |
+
n_iter: int = 20_000,
|
| 128 |
+
ema_decay: float = 0.999,
|
| 129 |
+
beta_max: float = 0.02,
|
| 130 |
+
batch_size: Optional[int] = None,
|
| 131 |
+
key: jax.Array = random.PRNGKey(0),
|
| 132 |
+
verbose_every: int = 0,
|
| 133 |
+
eps: float = 1e-5,
|
| 134 |
+
):
|
| 135 |
+
Z_iX = jnp.asarray(Z_iX, dtype=jnp.float32)
|
| 136 |
+
if Z_iX.ndim != 2:
|
| 137 |
+
raise ValueError("Z_iX must be 2D (N,D).")
|
| 138 |
+
|
| 139 |
+
self.D = int(Z_iX.shape[1])
|
| 140 |
+
self.T = int(T)
|
| 141 |
+
|
| 142 |
+
self.key = key
|
| 143 |
+
self.ema_decay = float(ema_decay)
|
| 144 |
+
self.batch_size = batch_size
|
| 145 |
+
self.verbose_every = int(verbose_every)
|
| 146 |
+
self.eps = float(eps)
|
| 147 |
+
self.beta_max = float(beta_max)
|
| 148 |
+
|
| 149 |
+
# store scalars for serialization/rebuild
|
| 150 |
+
self.learning_rate = float(learning_rate)
|
| 151 |
+
self.hidden_dim = int(hidden_dim)
|
| 152 |
+
self.t_embed_dim = int(t_embed_dim)
|
| 153 |
+
|
| 154 |
+
# schedule (cosine, clipped by beta_max)
|
| 155 |
+
alpha, beta, alpha_bar = cosine_schedule(self.T)
|
| 156 |
+
beta = jnp.minimum(beta, self.beta_max)
|
| 157 |
+
alpha = 1.0 - beta
|
| 158 |
+
alpha_bar = jnp.cumprod(alpha)
|
| 159 |
+
|
| 160 |
+
self.alpha_s = alpha.astype(jnp.float32)
|
| 161 |
+
self.beta_s = beta.astype(jnp.float32)
|
| 162 |
+
self.alpha_bar_s = alpha_bar.astype(jnp.float32)
|
| 163 |
+
|
| 164 |
+
# model + optimizer + EMA
|
| 165 |
+
self.model = EpsMLP(hidden=self.hidden_dim, t_dim=self.t_embed_dim, data_dim=self.D)
|
| 166 |
+
|
| 167 |
+
params = self.model.init(
|
| 168 |
+
self.key,
|
| 169 |
+
jnp.zeros((1, self.D), dtype=jnp.float32),
|
| 170 |
+
jnp.zeros((1, 1), dtype=jnp.int32),
|
| 171 |
+
)["params"]
|
| 172 |
+
|
| 173 |
+
tx = optax.adam(self.learning_rate)
|
| 174 |
+
self.state = TrainStateEMA.create(apply_fn=self.model.apply, params=params, tx=tx, ema_params=params)
|
| 175 |
+
|
| 176 |
+
# train
|
| 177 |
+
if int(n_iter) > 0:
|
| 178 |
+
self._train(Z_iX, int(n_iter))
|
| 179 |
+
|
| 180 |
+
# ---------- training ----------
|
| 181 |
+
|
| 182 |
+
@staticmethod
|
| 183 |
+
def _loss(params, apply_fn, x_t, t_idx, eps_true):
|
| 184 |
+
eps_pred = apply_fn({"params": params}, x_t, t_idx)
|
| 185 |
+
return jnp.mean((eps_pred - eps_true) ** 2)
|
| 186 |
+
|
| 187 |
+
@staticmethod
|
| 188 |
+
@jax.jit
|
| 189 |
+
def _train_step(
|
| 190 |
+
state: TrainStateEMA,
|
| 191 |
+
x0_batch: jnp.ndarray,
|
| 192 |
+
key: jax.Array,
|
| 193 |
+
alpha_bar_s: jnp.ndarray,
|
| 194 |
+
ema_decay: float,
|
| 195 |
+
eps: float,
|
| 196 |
+
):
|
| 197 |
+
B = x0_batch.shape[0]
|
| 198 |
+
key, k_eps, k_t = random.split(key, 3);
|
| 199 |
+
|
| 200 |
+
eps_noise = random.normal(k_eps, shape=x0_batch.shape) # (B,D)
|
| 201 |
+
t_idx = random.randint(k_t, shape=(B, 1), minval=0, maxval=alpha_bar_s.shape[0])
|
| 202 |
+
|
| 203 |
+
a_bar_t = jnp.take(alpha_bar_s, t_idx.squeeze(-1))[:, None]
|
| 204 |
+
a_bar_t = jnp.clip(a_bar_t, eps, 1.0)
|
| 205 |
+
|
| 206 |
+
x_t = jnp.sqrt(a_bar_t) * x0_batch + jnp.sqrt(1.0 - a_bar_t) * eps_noise
|
| 207 |
+
|
| 208 |
+
def loss_fn(p):
|
| 209 |
+
return DDPM._loss(p, state.apply_fn, x_t, t_idx, eps_noise)
|
| 210 |
+
|
| 211 |
+
loss, grads = jax.value_and_grad(loss_fn)(state.params)
|
| 212 |
+
new_state = state.apply_gradients(grads=grads, ema_decay=ema_decay)
|
| 213 |
+
return new_state, loss, key
|
| 214 |
+
|
| 215 |
+
def _train(self, Z_iX: jnp.ndarray, n_iter: int):
|
| 216 |
+
N = int(Z_iX.shape[0])
|
| 217 |
+
bs = N if (self.batch_size is None) else min(int(self.batch_size), N)
|
| 218 |
+
|
| 219 |
+
for it in range(n_iter):
|
| 220 |
+
if bs >= N:
|
| 221 |
+
batch = Z_iX
|
| 222 |
+
else:
|
| 223 |
+
self.key, k_perm = random.split(self.key)
|
| 224 |
+
idx = random.permutation(k_perm, N)[:bs]
|
| 225 |
+
batch = Z_iX[idx]
|
| 226 |
+
|
| 227 |
+
self.state, loss, self.key = self._train_step(
|
| 228 |
+
self.state,
|
| 229 |
+
batch,
|
| 230 |
+
self.key,
|
| 231 |
+
self.alpha_bar_s,
|
| 232 |
+
self.ema_decay,
|
| 233 |
+
self.eps,
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
if self.verbose_every and (it % self.verbose_every == 0 or it == n_iter - 1):
|
| 237 |
+
print(f"iter {it:6d} loss {float(loss):.6f}", end="\r")
|
| 238 |
+
|
| 239 |
+
if self.verbose_every:
|
| 240 |
+
print("\ntraining complete.")
|
| 241 |
+
|
| 242 |
+
# ---------- diffusion utilities ----------
|
| 243 |
+
|
| 244 |
+
@staticmethod
|
| 245 |
+
def _posterior_variance(alpha_s, beta_s, alpha_bar_s, t):
|
| 246 |
+
a_bar_t = alpha_bar_s[t]
|
| 247 |
+
a_bar_prev = jnp.where(t > 0, alpha_bar_s[t - 1], jnp.array(1.0, dtype=alpha_bar_s.dtype))
|
| 248 |
+
return ((1.0 - a_bar_prev) / (1.0 - a_bar_t)) * beta_s[t]
|
| 249 |
+
|
| 250 |
+
@staticmethod
|
| 251 |
+
def _make_sampler_step(params_ema, apply_fn, alpha_s, beta_s, alpha_bar_s, eps: float):
|
| 252 |
+
@jax.jit
|
| 253 |
+
def step(carry, _):
|
| 254 |
+
key, t, x = carry # x: (B,D)
|
| 255 |
+
key, k = random.split(key)
|
| 256 |
+
|
| 257 |
+
alpha_t = jnp.clip(alpha_s[t], eps, 1.0)
|
| 258 |
+
a_bar_t = jnp.clip(alpha_bar_s[t], eps, 1.0)
|
| 259 |
+
|
| 260 |
+
sqrt_alpha = jnp.sqrt(alpha_t)
|
| 261 |
+
sqrt_one_minus_a_bar = jnp.sqrt(jnp.clip(1.0 - a_bar_t, eps, 1.0))
|
| 262 |
+
|
| 263 |
+
B = x.shape[0]
|
| 264 |
+
t_batch = jnp.full((B, 1), t, dtype=jnp.int32)
|
| 265 |
+
|
| 266 |
+
eps_pred = apply_fn({"params": params_ema}, x, t_batch) # (B,D)
|
| 267 |
+
|
| 268 |
+
# predict x0
|
| 269 |
+
x0_hat = (x - sqrt_one_minus_a_bar * eps_pred) / jnp.sqrt(a_bar_t)
|
| 270 |
+
|
| 271 |
+
a_bar_prev = jnp.where(t > 0, alpha_bar_s[t - 1], jnp.array(1.0, dtype=alpha_bar_s.dtype))
|
| 272 |
+
denom = jnp.clip(1.0 - a_bar_t, eps, 1.0)
|
| 273 |
+
|
| 274 |
+
coef1 = jnp.sqrt(jnp.clip(a_bar_prev, eps, 1.0)) * beta_s[t] / denom
|
| 275 |
+
coef2 = sqrt_alpha * (1.0 - a_bar_prev) / denom
|
| 276 |
+
mean = coef1 * x0_hat + coef2 * x
|
| 277 |
+
|
| 278 |
+
beta_tilde = DDPM._posterior_variance(alpha_s, beta_s, alpha_bar_s, t)
|
| 279 |
+
sigma = jnp.sqrt(jnp.clip(beta_tilde, 0.0, 1.0))
|
| 280 |
+
|
| 281 |
+
z = random.normal(k, x.shape)
|
| 282 |
+
z = jnp.where(t == 0, 0.0, z);
|
| 283 |
+
|
| 284 |
+
x_prev = mean + sigma * z
|
| 285 |
+
return (key, t - 1, x_prev), x_prev
|
| 286 |
+
|
| 287 |
+
return step
|
| 288 |
+
|
| 289 |
+
# ---------- API ----------
|
| 290 |
+
|
| 291 |
+
def refine_latents(
|
| 292 |
+
self,
|
| 293 |
+
z0: jnp.ndarray,
|
| 294 |
+
t_start: int = 10,
|
| 295 |
+
key: Optional[jax.Array] = None,
|
| 296 |
+
add_noise: bool = True,
|
| 297 |
+
) -> jnp.ndarray:
|
| 298 |
+
"""
|
| 299 |
+
Refine latents by:
|
| 300 |
+
(optional) forward-noise z0 to step t_start
|
| 301 |
+
reverse-diffuse from t_start -> 0 using EMA params.
|
| 302 |
+
"""
|
| 303 |
+
z0 = jnp.asarray(z0, dtype=jnp.float32)
|
| 304 |
+
if z0.ndim != 2 or z0.shape[1] != self.D:
|
| 305 |
+
raise ValueError(f"z0 must have shape (B,{self.D}).")
|
| 306 |
+
|
| 307 |
+
if not (0 <= int(t_start) < self.T):
|
| 308 |
+
raise ValueError(f"t_start must be in [0, {self.T-1}]")
|
| 309 |
+
t_start = int(t_start)
|
| 310 |
+
|
| 311 |
+
if key is None:
|
| 312 |
+
self.key, key = random.split(self.key)
|
| 313 |
+
else:
|
| 314 |
+
# advance internal RNG too
|
| 315 |
+
self.key, _ = random.split(key)
|
| 316 |
+
|
| 317 |
+
# forward diffuse to t_start
|
| 318 |
+
key, k_eps = random.split(key)
|
| 319 |
+
eps_noise = random.normal(k_eps, z0.shape)
|
| 320 |
+
|
| 321 |
+
a_bar_t = jnp.clip(self.alpha_bar_s[t_start], self.eps, 1.0)
|
| 322 |
+
|
| 323 |
+
if add_noise:
|
| 324 |
+
z_t = jnp.sqrt(a_bar_t) * z0 + jnp.sqrt(1.0 - a_bar_t) * eps_noise
|
| 325 |
+
else:
|
| 326 |
+
z_t = z0
|
| 327 |
+
|
| 328 |
+
step = self._make_sampler_step(
|
| 329 |
+
self.state.ema_params,
|
| 330 |
+
self.state.apply_fn,
|
| 331 |
+
self.alpha_s,
|
| 332 |
+
self.beta_s,
|
| 333 |
+
self.alpha_bar_s,
|
| 334 |
+
self.eps,
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
(final_key, _, _), trace = jax.lax.scan(
|
| 338 |
+
step,
|
| 339 |
+
(key, t_start, z_t),
|
| 340 |
+
xs=None,
|
| 341 |
+
length=t_start + 1,
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
self.key = final_key
|
| 345 |
+
return trace[-1]
|
| 346 |
+
|
| 347 |
+
def __call__(
|
| 348 |
+
self,
|
| 349 |
+
z0: jnp.ndarray,
|
| 350 |
+
t_start: int = 10,
|
| 351 |
+
key: Optional[jax.Array] = None,
|
| 352 |
+
add_noise: bool = True,
|
| 353 |
+
) -> jnp.ndarray:
|
| 354 |
+
return self.refine_latents(z0, t_start=t_start, key=key, add_noise=add_noise)
|
| 355 |
+
|
| 356 |
+
def reverse_from_T(self, x_T: jnp.ndarray) -> jnp.ndarray:
|
| 357 |
+
x_T = jnp.asarray(x_T, dtype=jnp.float32)
|
| 358 |
+
if x_T.ndim != 2 or x_T.shape[1] != self.D:
|
| 359 |
+
raise ValueError(f"x_T must have shape (B,{self.D}).")
|
| 360 |
+
|
| 361 |
+
step = self._make_sampler_step(
|
| 362 |
+
self.state.ema_params,
|
| 363 |
+
self.state.apply_fn,
|
| 364 |
+
self.alpha_s,
|
| 365 |
+
self.beta_s,
|
| 366 |
+
self.alpha_bar_s,
|
| 367 |
+
self.eps,
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
self.key, k0 = random.split(self.key)
|
| 371 |
+
|
| 372 |
+
(_, _, _), trace = jax.lax.scan(
|
| 373 |
+
step,
|
| 374 |
+
(k0, self.T - 1, x_T),
|
| 375 |
+
xs=None,
|
| 376 |
+
length=self.T,
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
return trace[-1]
|
| 380 |
+
|
| 381 |
+
def sample(self, N: int = 10_000) -> jnp.ndarray:
|
| 382 |
+
self.key, k = random.split(self.key)
|
| 383 |
+
noise = random.normal(k, (int(N), self.D))
|
| 384 |
+
return self.reverse_from_T(noise)
|
| 385 |
+
|
| 386 |
+
# -------------------------
|
| 387 |
+
# Serialization helpers
|
| 388 |
+
# -------------------------
|
| 389 |
+
|
| 390 |
+
def state_dict(self):
|
| 391 |
+
"""
|
| 392 |
+
Msgpack-safe checkpoint dict.
|
| 393 |
+
Uses flax.serialization.to_state_dict to avoid tuple/namedtuple issues in opt_state.
|
| 394 |
+
"""
|
| 395 |
+
from flax import serialization as flax_ser # type: ignore
|
| 396 |
+
import numpy as np
|
| 397 |
+
|
| 398 |
+
return dict(
|
| 399 |
+
# hyperparameters needed to rebuild the object
|
| 400 |
+
T=int(self.T),
|
| 401 |
+
D=int(self.D),
|
| 402 |
+
hidden_dim=int(self.hidden_dim),
|
| 403 |
+
t_embed_dim=int(self.t_embed_dim),
|
| 404 |
+
learning_rate=float(self.learning_rate),
|
| 405 |
+
ema_decay=float(self.ema_decay),
|
| 406 |
+
beta_max=float(self.beta_max),
|
| 407 |
+
batch_size=None if (self.batch_size is None) else int(self.batch_size),
|
| 408 |
+
eps=float(self.eps),
|
| 409 |
+
|
| 410 |
+
# RNG key (store as numpy for portability)
|
| 411 |
+
key=np.asarray(self.key),
|
| 412 |
+
|
| 413 |
+
# TrainStateEMA (params, ema_params, opt_state, step) in a serializable form
|
| 414 |
+
train_state=flax_ser.to_state_dict(self.state),
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def save_local(self, weights_file: str = "ddpm.msgpack", config_file: Optional[str] = "ddpm_config.json") -> None:
|
| 419 |
+
from flax import serialization as flax_ser # type: ignore
|
| 420 |
+
import json as _json
|
| 421 |
+
|
| 422 |
+
ckpt = self.state_dict()
|
| 423 |
+
blob = flax_ser.msgpack_serialize(ckpt)
|
| 424 |
+
|
| 425 |
+
with open(weights_file, "wb") as f:
|
| 426 |
+
f.write(blob)
|
| 427 |
+
|
| 428 |
+
if config_file is not None:
|
| 429 |
+
cfg = dict(
|
| 430 |
+
T=int(ckpt["T"]),
|
| 431 |
+
D=int(ckpt["D"]),
|
| 432 |
+
hidden_dim=int(ckpt["hidden_dim"]),
|
| 433 |
+
t_embed_dim=int(ckpt["t_embed_dim"]),
|
| 434 |
+
learning_rate=float(ckpt["learning_rate"]),
|
| 435 |
+
ema_decay=float(ckpt["ema_decay"]),
|
| 436 |
+
beta_max=float(ckpt["beta_max"]),
|
| 437 |
+
batch_size=ckpt["batch_size"],
|
| 438 |
+
eps=float(ckpt["eps"]),
|
| 439 |
+
)
|
| 440 |
+
with open(config_file, "w") as f:
|
| 441 |
+
_json.dump(cfg, f, indent=2)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
@classmethod
|
| 445 |
+
|
| 446 |
+
def from_state(cls, ckpt, *, key: Optional[jax.Array] = None) -> "DDPM":
|
| 447 |
+
"""
|
| 448 |
+
Rehydrate a DDPM instance without retraining, restoring full TrainStateEMA
|
| 449 |
+
(including opt_state) via flax.serialization.from_state_dict.
|
| 450 |
+
"""
|
| 451 |
+
from flax import serialization as flax_ser # type: ignore
|
| 452 |
+
import numpy as np
|
| 453 |
+
|
| 454 |
+
T = int(ckpt["T"])
|
| 455 |
+
D = int(ckpt["D"])
|
| 456 |
+
hidden_dim = int(ckpt["hidden_dim"])
|
| 457 |
+
t_embed_dim = int(ckpt["t_embed_dim"])
|
| 458 |
+
learning_rate = float(ckpt["learning_rate"])
|
| 459 |
+
ema_decay = float(ckpt["ema_decay"])
|
| 460 |
+
beta_max = float(ckpt["beta_max"])
|
| 461 |
+
batch_size = ckpt.get("batch_size", None)
|
| 462 |
+
eps = float(ckpt.get("eps", 1e-5))
|
| 463 |
+
|
| 464 |
+
if key is None:
|
| 465 |
+
# prefer explicit passed key; else use checkpoint key; else default
|
| 466 |
+
if "key" in ckpt:
|
| 467 |
+
key = jnp.asarray(np.asarray(ckpt["key"]))
|
| 468 |
+
else:
|
| 469 |
+
key = random.PRNGKey(0)
|
| 470 |
+
|
| 471 |
+
# Build a skeleton instance (no training)
|
| 472 |
+
dummy = jnp.zeros((1, D), dtype=jnp.float32)
|
| 473 |
+
obj = cls(
|
| 474 |
+
dummy,
|
| 475 |
+
T=T,
|
| 476 |
+
hidden_dim=hidden_dim,
|
| 477 |
+
t_embed_dim=t_embed_dim,
|
| 478 |
+
learning_rate=learning_rate,
|
| 479 |
+
n_iter=0,
|
| 480 |
+
ema_decay=ema_decay,
|
| 481 |
+
beta_max=beta_max,
|
| 482 |
+
batch_size=batch_size,
|
| 483 |
+
key=key,
|
| 484 |
+
verbose_every=0,
|
| 485 |
+
eps=eps,
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
# Restore TrainStateEMA (including opt_state tuples) safely
|
| 489 |
+
obj.state = flax_ser.from_state_dict(obj.state, ckpt["train_state"])
|
| 490 |
+
|
| 491 |
+
# Restore RNG key if present (keeps reproducibility)
|
| 492 |
+
if "key" in ckpt:
|
| 493 |
+
obj.key = jnp.asarray(np.asarray(ckpt["key"]))
|
| 494 |
+
|
| 495 |
+
return obj
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
@classmethod
|
| 499 |
+
def load_local(cls, weights_file: str = "ddpm.msgpack", *, key: Optional[jax.Array] = None) -> "DDPM":
|
| 500 |
+
from flax import serialization as flax_ser # type: ignore
|
| 501 |
+
|
| 502 |
+
with open(weights_file, "rb") as f:
|
| 503 |
+
ckpt = flax_ser.msgpack_restore(f.read())
|
| 504 |
+
|
| 505 |
+
return cls.from_state(ckpt, key=key)
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def upload_to_huggingface(
|
| 509 |
+
self,
|
| 510 |
+
repo_id: str,
|
| 511 |
+
*,
|
| 512 |
+
weights_file: str = "ddpm.msgpack",
|
| 513 |
+
config_file: str = "ddpm_config.json",
|
| 514 |
+
token: Optional[str] = None,
|
| 515 |
+
repo_type: str = "model",
|
| 516 |
+
revision: Optional[str] = None,
|
| 517 |
+
) -> None:
|
| 518 |
+
"""
|
| 519 |
+
Upload DDPM weights to the Hugging Face Hub.
|
| 520 |
+
Saves locally first (msgpack + JSON), then creates repo (if needed) and uploads files.
|
| 521 |
+
"""
|
| 522 |
+
try:
|
| 523 |
+
from huggingface_hub import HfApi, HfFolder, upload_file # type: ignore
|
| 524 |
+
except Exception as e:
|
| 525 |
+
raise RuntimeError("huggingface_hub not installed. Install it (or `pip install dima[hf]`).") from e
|
| 526 |
+
|
| 527 |
+
self.save_local(weights_file=weights_file, config_file=config_file)
|
| 528 |
+
|
| 529 |
+
if token is None:
|
| 530 |
+
token = HfFolder.get_token()
|
| 531 |
+
if token is None:
|
| 532 |
+
raise RuntimeError("No HF token found. Run `huggingface-cli login`, or pass `token=...`.")
|
| 533 |
+
|
| 534 |
+
import os as _os
|
| 535 |
+
|
| 536 |
+
api = HfApi()
|
| 537 |
+
api.create_repo(repo_id=repo_id, repo_type=repo_type, exist_ok=True, token=token)
|
| 538 |
+
|
| 539 |
+
wf = _os.path.basename(weights_file)
|
| 540 |
+
cf = _os.path.basename(config_file)
|
| 541 |
+
|
| 542 |
+
upload_file(
|
| 543 |
+
path_or_fileobj=weights_file,
|
| 544 |
+
path_in_repo=wf,
|
| 545 |
+
repo_id=repo_id,
|
| 546 |
+
token=token,
|
| 547 |
+
repo_type=repo_type,
|
| 548 |
+
revision=revision,
|
| 549 |
+
)
|
| 550 |
+
upload_file(
|
| 551 |
+
path_or_fileobj=config_file,
|
| 552 |
+
path_in_repo=cf,
|
| 553 |
+
repo_id=repo_id,
|
| 554 |
+
token=token,
|
| 555 |
+
repo_type=repo_type,
|
| 556 |
+
revision=revision,
|
| 557 |
+
)
|
| 558 |
+
|
| 559 |
+
@classmethod
|
| 560 |
+
def download_from_huggingface(
|
| 561 |
+
cls,
|
| 562 |
+
repo_id: str,
|
| 563 |
+
*,
|
| 564 |
+
weights_file: str = "ddpm.msgpack",
|
| 565 |
+
token: Optional[str] = None,
|
| 566 |
+
repo_type: str = "model",
|
| 567 |
+
revision: Optional[str] = None,
|
| 568 |
+
key: Optional[jax.Array] = None,
|
| 569 |
+
) -> "DDPM":
|
| 570 |
+
"""
|
| 571 |
+
Download DDPM weights from the Hugging Face Hub and return a rehydrated DDPM instance.
|
| 572 |
+
"""
|
| 573 |
+
try:
|
| 574 |
+
from huggingface_hub import hf_hub_download # type: ignore
|
| 575 |
+
except Exception as e:
|
| 576 |
+
raise RuntimeError("huggingface_hub not installed. Install it (or `pip install dima[hf]`).") from e
|
| 577 |
+
|
| 578 |
+
path = hf_hub_download(
|
| 579 |
+
repo_id=repo_id,
|
| 580 |
+
filename=weights_file,
|
| 581 |
+
token=token,
|
| 582 |
+
repo_type=repo_type,
|
| 583 |
+
revision=revision,
|
| 584 |
+
)
|
| 585 |
+
return cls.load_local(path, key=key)
|
| 586 |
+
|
| 587 |
+
# Optional backward-compatible alias (if you ever used the other name)
|
| 588 |
+
download_to_huggingface = download_from_huggingface
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
__all__ = ["DDPM", "EpsMLP", "cosine_schedule", "sinusoidal_embedding"]
|
ddpmx.py
ADDED
|
@@ -0,0 +1,745 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# how to use
|
| 2 |
+
# ddpm = DDPM(Z_train, T=100, n_iter=20_000, key=random.PRNGKey(0))
|
| 3 |
+
#
|
| 4 |
+
# Fast unconditional samples (try 15–30 first)
|
| 5 |
+
# z = ddpm.sample_dpmpp(N=4096, num_steps=20)
|
| 6 |
+
#
|
| 7 |
+
# Fast “refine latents” starting from an intermediate noise level
|
| 8 |
+
#z_ref = ddpm.refine_latents_dpmpp(z0, t_start=30, num_steps=20, add_noise=True)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# src/dima/ddpmx.py
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from typing import Any, Optional, Dict
|
| 16 |
+
import os
|
| 17 |
+
import json
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
import jax
|
| 21 |
+
import jax.numpy as jnp
|
| 22 |
+
from jax import random
|
| 23 |
+
|
| 24 |
+
from flax import linen as nn
|
| 25 |
+
from flax.training import train_state
|
| 26 |
+
from flax import struct, serialization as flax_ser
|
| 27 |
+
import optax
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ---------------------------------------------------------------------
|
| 31 |
+
# Helpers (as in ddpmx.py)
|
| 32 |
+
# ---------------------------------------------------------------------
|
| 33 |
+
def _sigma_to_alpha_sigma_t(sigma: jnp.ndarray) -> tuple[jnp.ndarray, jnp.ndarray]:
|
| 34 |
+
"""
|
| 35 |
+
EDM-style sigma parameterization:
|
| 36 |
+
alpha_t = 1 / sqrt(1 + sigma^2)
|
| 37 |
+
sigma_t = sigma * alpha_t
|
| 38 |
+
so that x = alpha_t * x0 + sigma_t * eps
|
| 39 |
+
"""
|
| 40 |
+
alpha_t = 1.0 / jnp.sqrt(1.0 + sigma**2)
|
| 41 |
+
sigma_t = sigma * alpha_t
|
| 42 |
+
return alpha_t, sigma_t
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _make_lu_sigma_schedule(sigma_start: float, sigma_end: float, num_steps: int) -> np.ndarray:
|
| 46 |
+
"""
|
| 47 |
+
"Lu" schedule uniform in lambda = -log(sigma).
|
| 48 |
+
"""
|
| 49 |
+
sigma_start = float(max(sigma_start, 1e-12))
|
| 50 |
+
sigma_end = float(max(sigma_end, 1e-12))
|
| 51 |
+
lam_start = -np.log(sigma_start)
|
| 52 |
+
lam_end = -np.log(sigma_end)
|
| 53 |
+
lambdas = np.linspace(lam_start, lam_end, int(num_steps), dtype=np.float32)
|
| 54 |
+
sigmas = np.exp(-lambdas).astype(np.float32)
|
| 55 |
+
return sigmas
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def cosine_schedule(T: int, s: float = 0.008):
|
| 59 |
+
"""
|
| 60 |
+
Nichol & Dhariwal cosine schedule.
|
| 61 |
+
Returns alpha, beta, alpha_bar with shape (T,).
|
| 62 |
+
"""
|
| 63 |
+
steps = jnp.arange(T + 1, dtype=jnp.float32)
|
| 64 |
+
f = jnp.cos(((steps / T + s) / (1.0 + s)) * jnp.pi / 2.0) ** 2
|
| 65 |
+
alpha_bar_all = f / f[0]
|
| 66 |
+
alpha_bar = alpha_bar_all[1:] # (T,)
|
| 67 |
+
alpha = alpha_bar / jnp.concatenate([jnp.array([1.0], dtype=jnp.float32), alpha_bar[:-1]])
|
| 68 |
+
beta = 1.0 - alpha
|
| 69 |
+
return alpha, beta, alpha_bar
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def sinusoidal_embedding(t_idx: jnp.ndarray, dim: int) -> jnp.ndarray:
|
| 73 |
+
"""
|
| 74 |
+
t_idx: (B,1) int32 or float32
|
| 75 |
+
returns: (B,dim)
|
| 76 |
+
"""
|
| 77 |
+
if t_idx.ndim != 2 or t_idx.shape[1] != 1:
|
| 78 |
+
raise ValueError("t_idx must have shape (B,1)")
|
| 79 |
+
t = t_idx.astype(jnp.float32)
|
| 80 |
+
half = dim // 2
|
| 81 |
+
denom = float(max(half - 1, 1))
|
| 82 |
+
freqs = jnp.exp(-jnp.log(10_000.0) * jnp.arange(half, dtype=jnp.float32) / denom)
|
| 83 |
+
args = t * freqs
|
| 84 |
+
emb = jnp.concatenate([jnp.sin(args), jnp.cos(args)], axis=-1)
|
| 85 |
+
if dim % 2 == 1:
|
| 86 |
+
emb = jnp.pad(emb, ((0, 0), (0, 1)))
|
| 87 |
+
return emb
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class EpsMLP(nn.Module):
|
| 91 |
+
"""Simple MLP epsilon-predictor for DDPM in R^D."""
|
| 92 |
+
hidden: int
|
| 93 |
+
t_dim: int
|
| 94 |
+
data_dim: int
|
| 95 |
+
|
| 96 |
+
@nn.compact
|
| 97 |
+
def __call__(self, x: jnp.ndarray, t_idx: jnp.ndarray) -> jnp.ndarray:
|
| 98 |
+
t_emb = sinusoidal_embedding(t_idx, self.t_dim)
|
| 99 |
+
t_h = nn.Dense(self.hidden)(t_emb)
|
| 100 |
+
t_h = nn.gelu(t_h)
|
| 101 |
+
|
| 102 |
+
h = nn.Dense(self.hidden)(x)
|
| 103 |
+
h = nn.gelu(h + t_h)
|
| 104 |
+
|
| 105 |
+
t_h2 = nn.Dense(self.hidden)(t_h)
|
| 106 |
+
h = nn.Dense(self.hidden)(h)
|
| 107 |
+
h = nn.gelu(h + t_h2)
|
| 108 |
+
|
| 109 |
+
out = nn.Dense(self.data_dim)(h)
|
| 110 |
+
return out
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@struct.dataclass
|
| 114 |
+
class TrainStateEMA(train_state.TrainState):
|
| 115 |
+
"""Flax TrainState extended with EMA params."""
|
| 116 |
+
ema_params: Any = struct.field(pytree_node=True)
|
| 117 |
+
|
| 118 |
+
def apply_gradients(self, *, grads, ema_decay: float):
|
| 119 |
+
updates, new_opt_state = self.tx.update(grads, self.opt_state, self.params)
|
| 120 |
+
new_params = optax.apply_updates(self.params, updates)
|
| 121 |
+
new_ema = optax.incremental_update(new_params, self.ema_params, step_size=1.0 - ema_decay)
|
| 122 |
+
return self.replace(
|
| 123 |
+
step=self.step + 1,
|
| 124 |
+
params=new_params,
|
| 125 |
+
opt_state=new_opt_state,
|
| 126 |
+
ema_params=new_ema,
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# ---------------------------------------------------------------------
|
| 131 |
+
# DDPMX with HF upload/download integrated
|
| 132 |
+
# ---------------------------------------------------------------------
|
| 133 |
+
class DDPM:
|
| 134 |
+
"""
|
| 135 |
+
DDPM (+ fast DPM-Solver++(2M) sampler utilities) for D-dimensional latents.
|
| 136 |
+
|
| 137 |
+
Added persistence utilities:
|
| 138 |
+
- save_local / load_local
|
| 139 |
+
- upload_to_huggingface / download_from_huggingface
|
| 140 |
+
|
| 141 |
+
Serialization is done via flax.serialization.to_state_dict / from_state_dict
|
| 142 |
+
to avoid msgpack failures with non-serializable Python objects (e.g., tuples).
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
def __init__(
|
| 146 |
+
self,
|
| 147 |
+
Z_iX: jnp.ndarray,
|
| 148 |
+
*,
|
| 149 |
+
T: int = 100,
|
| 150 |
+
hidden_dim: int = 128,
|
| 151 |
+
t_embed_dim: int = 64,
|
| 152 |
+
learning_rate: float = 1e-3,
|
| 153 |
+
n_iter: int = 20_000,
|
| 154 |
+
ema_decay: float = 0.999,
|
| 155 |
+
beta_max: float = 0.02,
|
| 156 |
+
batch_size: Optional[int] = None,
|
| 157 |
+
key: jax.Array = random.PRNGKey(0),
|
| 158 |
+
verbose_every: int = 0,
|
| 159 |
+
eps: float = 1e-5,
|
| 160 |
+
):
|
| 161 |
+
Z_iX = jnp.asarray(Z_iX, dtype=jnp.float32)
|
| 162 |
+
if Z_iX.ndim != 2:
|
| 163 |
+
raise ValueError("Z_iX must be 2D (N,D).")
|
| 164 |
+
|
| 165 |
+
self.D = int(Z_iX.shape[1])
|
| 166 |
+
self.T = int(T)
|
| 167 |
+
|
| 168 |
+
# store config for checkpointing
|
| 169 |
+
self.hidden_dim = int(hidden_dim)
|
| 170 |
+
self.t_embed_dim = int(t_embed_dim)
|
| 171 |
+
self.learning_rate = float(learning_rate)
|
| 172 |
+
self.ema_decay = float(ema_decay)
|
| 173 |
+
self.beta_max = float(beta_max)
|
| 174 |
+
self.batch_size = batch_size
|
| 175 |
+
self.verbose_every = int(verbose_every)
|
| 176 |
+
self.eps = float(eps)
|
| 177 |
+
|
| 178 |
+
self.key = key
|
| 179 |
+
|
| 180 |
+
# VP schedule (cosine + clip)
|
| 181 |
+
alpha, beta, alpha_bar = cosine_schedule(self.T)
|
| 182 |
+
beta = jnp.minimum(beta, self.beta_max)
|
| 183 |
+
alpha = 1.0 - beta
|
| 184 |
+
alpha_bar = jnp.cumprod(alpha)
|
| 185 |
+
|
| 186 |
+
self.alpha_s = alpha.astype(jnp.float32)
|
| 187 |
+
self.beta_s = beta.astype(jnp.float32)
|
| 188 |
+
self.alpha_bar_s = alpha_bar.astype(jnp.float32)
|
| 189 |
+
|
| 190 |
+
# Precompute EDM-style "sigma_in" for DPM++ schedule interpolation
|
| 191 |
+
# sigma_in = sqrt((1 - a_bar) / a_bar)
|
| 192 |
+
self.sigma_in_train = jnp.sqrt(
|
| 193 |
+
jnp.clip(
|
| 194 |
+
(1.0 - self.alpha_bar_s) / jnp.clip(self.alpha_bar_s, self.eps, 1.0),
|
| 195 |
+
self.eps,
|
| 196 |
+
1e12,
|
| 197 |
+
)
|
| 198 |
+
).astype(jnp.float32)
|
| 199 |
+
|
| 200 |
+
# model + optimizer + EMA state
|
| 201 |
+
self.model = EpsMLP(hidden=self.hidden_dim, t_dim=self.t_embed_dim, data_dim=self.D)
|
| 202 |
+
params = self.model.init(
|
| 203 |
+
self.key,
|
| 204 |
+
jnp.zeros((1, self.D), dtype=jnp.float32),
|
| 205 |
+
jnp.zeros((1, 1), dtype=jnp.int32),
|
| 206 |
+
)["params"]
|
| 207 |
+
|
| 208 |
+
tx = optax.adam(self.learning_rate)
|
| 209 |
+
self.state = TrainStateEMA.create(
|
| 210 |
+
apply_fn=self.model.apply,
|
| 211 |
+
params=params,
|
| 212 |
+
tx=tx,
|
| 213 |
+
ema_params=params,
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
if int(n_iter) > 0:
|
| 217 |
+
self._train(Z_iX, int(n_iter))
|
| 218 |
+
|
| 219 |
+
# -------------------------
|
| 220 |
+
# Training
|
| 221 |
+
# -------------------------
|
| 222 |
+
@staticmethod
|
| 223 |
+
def _loss(params, apply_fn, x_t, t_idx, eps_true):
|
| 224 |
+
eps_pred = apply_fn({"params": params}, x_t, t_idx)
|
| 225 |
+
return jnp.mean((eps_pred - eps_true) ** 2)
|
| 226 |
+
|
| 227 |
+
@staticmethod
|
| 228 |
+
@jax.jit
|
| 229 |
+
def _train_step(
|
| 230 |
+
state: TrainStateEMA,
|
| 231 |
+
x0_batch: jnp.ndarray,
|
| 232 |
+
key: jax.Array,
|
| 233 |
+
alpha_bar_s: jnp.ndarray,
|
| 234 |
+
ema_decay: float,
|
| 235 |
+
eps: float,
|
| 236 |
+
):
|
| 237 |
+
B = x0_batch.shape[0]
|
| 238 |
+
key, k_eps, k_t = random.split(key, 3)
|
| 239 |
+
eps_noise = random.normal(k_eps, shape=x0_batch.shape)
|
| 240 |
+
t_idx = random.randint(k_t, shape=(B, 1), minval=0, maxval=alpha_bar_s.shape[0])
|
| 241 |
+
|
| 242 |
+
a_bar_t = jnp.take(alpha_bar_s, t_idx.squeeze(-1))[:, None]
|
| 243 |
+
a_bar_t = jnp.clip(a_bar_t, eps, 1.0)
|
| 244 |
+
x_t = jnp.sqrt(a_bar_t) * x0_batch + jnp.sqrt(1.0 - a_bar_t) * eps_noise
|
| 245 |
+
|
| 246 |
+
def loss_fn(p):
|
| 247 |
+
return DDPM._loss(p, state.apply_fn, x_t, t_idx, eps_noise)
|
| 248 |
+
|
| 249 |
+
loss, grads = jax.value_and_grad(loss_fn)(state.params)
|
| 250 |
+
new_state = state.apply_gradients(grads=grads, ema_decay=ema_decay)
|
| 251 |
+
return new_state, loss, key
|
| 252 |
+
|
| 253 |
+
def _train(self, Z_iX: jnp.ndarray, n_iter: int):
|
| 254 |
+
N = int(Z_iX.shape[0])
|
| 255 |
+
bs = N if (self.batch_size is None) else min(int(self.batch_size), N)
|
| 256 |
+
|
| 257 |
+
for it in range(n_iter):
|
| 258 |
+
if bs >= N:
|
| 259 |
+
batch = Z_iX
|
| 260 |
+
else:
|
| 261 |
+
self.key, k_perm = random.split(self.key)
|
| 262 |
+
idx = random.permutation(k_perm, N)[:bs]
|
| 263 |
+
batch = Z_iX[idx]
|
| 264 |
+
|
| 265 |
+
self.state, loss, self.key = self._train_step(
|
| 266 |
+
self.state,
|
| 267 |
+
batch,
|
| 268 |
+
self.key,
|
| 269 |
+
self.alpha_bar_s,
|
| 270 |
+
self.ema_decay,
|
| 271 |
+
self.eps,
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
if self.verbose_every and (it % self.verbose_every == 0 or it == n_iter - 1):
|
| 275 |
+
print(f"iter {it:6d} loss {float(loss):.6f}", end="\r")
|
| 276 |
+
|
| 277 |
+
if self.verbose_every:
|
| 278 |
+
print("\ntraining complete.")
|
| 279 |
+
|
| 280 |
+
# -------------------------
|
| 281 |
+
# Standard DDPM refine/sample
|
| 282 |
+
# -------------------------
|
| 283 |
+
@staticmethod
|
| 284 |
+
def _posterior_variance(alpha_s, beta_s, alpha_bar_s, t):
|
| 285 |
+
a_bar_t = alpha_bar_s[t]
|
| 286 |
+
a_bar_prev = jnp.where(t > 0, alpha_bar_s[t - 1], jnp.array(1.0, dtype=alpha_bar_s.dtype))
|
| 287 |
+
return ((1.0 - a_bar_prev) / (1.0 - a_bar_t)) * beta_s[t]
|
| 288 |
+
|
| 289 |
+
@staticmethod
|
| 290 |
+
def _make_sampler_step(params_ema, apply_fn, alpha_s, beta_s, alpha_bar_s, eps: float):
|
| 291 |
+
@jax.jit
|
| 292 |
+
def step(carry, _):
|
| 293 |
+
key, t, x = carry
|
| 294 |
+
key, k = random.split(key)
|
| 295 |
+
|
| 296 |
+
alpha_t = jnp.clip(alpha_s[t], eps, 1.0)
|
| 297 |
+
a_bar_t = jnp.clip(alpha_bar_s[t], eps, 1.0)
|
| 298 |
+
|
| 299 |
+
sqrt_alpha = jnp.sqrt(alpha_t)
|
| 300 |
+
sqrt_one_minus = jnp.sqrt(jnp.clip(1.0 - a_bar_t, eps, 1.0))
|
| 301 |
+
|
| 302 |
+
B = x.shape[0]
|
| 303 |
+
t_batch = jnp.full((B, 1), t, dtype=jnp.int32)
|
| 304 |
+
|
| 305 |
+
eps_pred = apply_fn({"params": params_ema}, x, t_batch)
|
| 306 |
+
x0_hat = (x - sqrt_one_minus * eps_pred) / jnp.sqrt(a_bar_t)
|
| 307 |
+
|
| 308 |
+
a_bar_prev = jnp.where(t > 0, alpha_bar_s[t - 1], jnp.array(1.0, dtype=alpha_bar_s.dtype))
|
| 309 |
+
denom = jnp.clip(1.0 - a_bar_t, eps, 1.0)
|
| 310 |
+
|
| 311 |
+
coef1 = jnp.sqrt(jnp.clip(a_bar_prev, eps, 1.0)) * beta_s[t] / denom
|
| 312 |
+
coef2 = sqrt_alpha * (1.0 - a_bar_prev) / denom
|
| 313 |
+
mean = coef1 * x0_hat + coef2 * x
|
| 314 |
+
|
| 315 |
+
beta_tilde = DDPM._posterior_variance(alpha_s, beta_s, alpha_bar_s, t)
|
| 316 |
+
sigma = jnp.sqrt(jnp.clip(beta_tilde, 0.0, 1.0))
|
| 317 |
+
|
| 318 |
+
z = random.normal(k, x.shape)
|
| 319 |
+
z = jnp.where(t == 0, 0.0, z)
|
| 320 |
+
x_prev = mean + sigma * z
|
| 321 |
+
|
| 322 |
+
return (key, t - 1, x_prev), x_prev
|
| 323 |
+
|
| 324 |
+
return step
|
| 325 |
+
|
| 326 |
+
def refine_latents(
|
| 327 |
+
self,
|
| 328 |
+
z0: jnp.ndarray,
|
| 329 |
+
t_start: int = 10,
|
| 330 |
+
key: Optional[jax.Array] = None,
|
| 331 |
+
add_noise: bool = True,
|
| 332 |
+
) -> jnp.ndarray:
|
| 333 |
+
z0 = jnp.asarray(z0, dtype=jnp.float32)
|
| 334 |
+
if z0.ndim != 2 or z0.shape[1] != self.D:
|
| 335 |
+
raise ValueError(f"z0 must have shape (B,{self.D}).")
|
| 336 |
+
if not (0 <= int(t_start) < self.T):
|
| 337 |
+
raise ValueError(f"t_start must be in [0, {self.T-1}]")
|
| 338 |
+
t_start = int(t_start)
|
| 339 |
+
|
| 340 |
+
if key is None:
|
| 341 |
+
self.key, key = random.split(self.key)
|
| 342 |
+
else:
|
| 343 |
+
self.key, _ = random.split(key)
|
| 344 |
+
|
| 345 |
+
key, k_eps = random.split(key)
|
| 346 |
+
eps_noise = random.normal(k_eps, z0.shape)
|
| 347 |
+
a_bar_t = jnp.clip(self.alpha_bar_s[t_start], self.eps, 1.0)
|
| 348 |
+
|
| 349 |
+
if add_noise:
|
| 350 |
+
z_t = jnp.sqrt(a_bar_t) * z0 + jnp.sqrt(1.0 - a_bar_t) * eps_noise
|
| 351 |
+
else:
|
| 352 |
+
z_t = z0
|
| 353 |
+
|
| 354 |
+
step = self._make_sampler_step(
|
| 355 |
+
self.state.ema_params,
|
| 356 |
+
self.state.apply_fn,
|
| 357 |
+
self.alpha_s,
|
| 358 |
+
self.beta_s,
|
| 359 |
+
self.alpha_bar_s,
|
| 360 |
+
self.eps,
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
(final_key, _, _), trace = jax.lax.scan(
|
| 364 |
+
step,
|
| 365 |
+
(key, t_start, z_t),
|
| 366 |
+
xs=None,
|
| 367 |
+
length=t_start + 1,
|
| 368 |
+
)
|
| 369 |
+
self.key = final_key
|
| 370 |
+
return trace[-1]
|
| 371 |
+
|
| 372 |
+
def __call__(
|
| 373 |
+
self,
|
| 374 |
+
z0: jnp.ndarray,
|
| 375 |
+
t_start: int = 10,
|
| 376 |
+
key: Optional[jax.Array] = None,
|
| 377 |
+
add_noise: bool = True,
|
| 378 |
+
) -> jnp.ndarray:
|
| 379 |
+
return self.refine_latents(z0, t_start=t_start, key=key, add_noise=add_noise)
|
| 380 |
+
|
| 381 |
+
def reverse_from_T(self, x_T: jnp.ndarray) -> jnp.ndarray:
|
| 382 |
+
x_T = jnp.asarray(x_T, dtype=jnp.float32)
|
| 383 |
+
if x_T.ndim != 2 or x_T.shape[1] != self.D:
|
| 384 |
+
raise ValueError(f"x_T must have shape (B,{self.D}).")
|
| 385 |
+
|
| 386 |
+
step = self._make_sampler_step(
|
| 387 |
+
self.state.ema_params,
|
| 388 |
+
self.state.apply_fn,
|
| 389 |
+
self.alpha_s,
|
| 390 |
+
self.beta_s,
|
| 391 |
+
self.alpha_bar_s,
|
| 392 |
+
self.eps,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
self.key, k0 = random.split(self.key)
|
| 396 |
+
(_, _, _), trace = jax.lax.scan(
|
| 397 |
+
step,
|
| 398 |
+
(k0, self.T - 1, x_T),
|
| 399 |
+
xs=None,
|
| 400 |
+
length=self.T,
|
| 401 |
+
)
|
| 402 |
+
return trace[-1]
|
| 403 |
+
|
| 404 |
+
def sample(self, N: int = 10_000) -> jnp.ndarray:
|
| 405 |
+
self.key, k = random.split(self.key)
|
| 406 |
+
noise = random.normal(k, (int(N), self.D)).astype(jnp.float32)
|
| 407 |
+
return self.reverse_from_T(noise)
|
| 408 |
+
|
| 409 |
+
# -------------------------
|
| 410 |
+
# DPM-Solver++(2M) schedule + sampler
|
| 411 |
+
# -------------------------
|
| 412 |
+
def _make_dpmpp_schedule(self, *, num_steps: int, t_start: int) -> tuple[jnp.ndarray, jnp.ndarray]:
|
| 413 |
+
"""
|
| 414 |
+
Returns:
|
| 415 |
+
sigmas_in: (K+1,) float32 decreasing, last one is 0
|
| 416 |
+
t_cont: (K,) float32 continuous "time" indices for model calls
|
| 417 |
+
"""
|
| 418 |
+
t_start = int(t_start)
|
| 419 |
+
if not (0 <= t_start < self.T):
|
| 420 |
+
raise ValueError(f"t_start must be in [0, {self.T-1}]")
|
| 421 |
+
if int(num_steps) < 1:
|
| 422 |
+
raise ValueError("num_steps must be >= 1")
|
| 423 |
+
|
| 424 |
+
sigma_start = float(self.sigma_in_train[t_start])
|
| 425 |
+
sigma_end = float(self.sigma_in_train[0])
|
| 426 |
+
|
| 427 |
+
sigmas_k = _make_lu_sigma_schedule(sigma_start, sigma_end, int(num_steps))
|
| 428 |
+
sigmas = np.concatenate([sigmas_k, np.array([0.0], np.float32)], axis=0)
|
| 429 |
+
|
| 430 |
+
sigma_train = np.array(self.sigma_in_train).astype(np.float32) # (T,)
|
| 431 |
+
log_sig_train = np.log(np.maximum(sigma_train, 1e-12))
|
| 432 |
+
t_train = np.arange(self.T, dtype=np.float32)
|
| 433 |
+
|
| 434 |
+
log_sig = np.log(np.maximum(sigmas[:-1], 1e-12))
|
| 435 |
+
t_cont = np.interp(log_sig, log_sig_train, t_train).astype(np.float32)
|
| 436 |
+
|
| 437 |
+
return jnp.array(sigmas, dtype=jnp.float32), jnp.array(t_cont, dtype=jnp.float32)
|
| 438 |
+
|
| 439 |
+
@staticmethod
|
| 440 |
+
@jax.jit
|
| 441 |
+
def _dpmpp_2m_midpoint_sample(
|
| 442 |
+
params_ema: Any,
|
| 443 |
+
apply_fn: Any,
|
| 444 |
+
x_start: jnp.ndarray, # (B,D)
|
| 445 |
+
sigmas_in: jnp.ndarray, # (K+1,)
|
| 446 |
+
t_cont: jnp.ndarray, # (K,)
|
| 447 |
+
eps: float,
|
| 448 |
+
) -> jnp.ndarray:
|
| 449 |
+
"""
|
| 450 |
+
DPM-Solver++ (2M, midpoint) sampler.
|
| 451 |
+
"""
|
| 452 |
+
sigma_s = sigmas_in[:-1] # (K,)
|
| 453 |
+
sigma_t = sigmas_in[1:] # (K,)
|
| 454 |
+
|
| 455 |
+
alpha_s, sigma_s_t = _sigma_to_alpha_sigma_t(sigma_s)
|
| 456 |
+
alpha_t, sigma_t_t = _sigma_to_alpha_sigma_t(sigma_t)
|
| 457 |
+
|
| 458 |
+
lambda_s = jnp.log(alpha_s) - jnp.log(sigma_s_t)
|
| 459 |
+
lambda_t = jnp.log(alpha_t) - jnp.log(sigma_t_t)
|
| 460 |
+
|
| 461 |
+
K = t_cont.shape[0]
|
| 462 |
+
is_first = jnp.arange(K) == 0
|
| 463 |
+
is_last = jnp.arange(K) == (K - 1)
|
| 464 |
+
|
| 465 |
+
def step(carry, inp):
|
| 466 |
+
x, m_prev, lam_prev = carry
|
| 467 |
+
(a_s, s_s, a_t, s_t, lam_s_i, lam_t_i, t_i, first_i, last_i) = inp
|
| 468 |
+
|
| 469 |
+
B = x.shape[0]
|
| 470 |
+
t_batch = jnp.full((B, 1), t_i, dtype=jnp.float32)
|
| 471 |
+
|
| 472 |
+
eps_pred = apply_fn({"params": params_ema}, x, t_batch)
|
| 473 |
+
|
| 474 |
+
a_s_b = jnp.clip(a_s, eps, 1.0)
|
| 475 |
+
x0 = (x - s_s * eps_pred) / a_s_b
|
| 476 |
+
|
| 477 |
+
h = lam_t_i - lam_s_i
|
| 478 |
+
exp_neg_h = jnp.exp(-h)
|
| 479 |
+
|
| 480 |
+
# 1st-order
|
| 481 |
+
x_first = (s_t / s_s) * x - (a_t * (exp_neg_h - 1.0)) * x0
|
| 482 |
+
|
| 483 |
+
def do_second(_):
|
| 484 |
+
h0 = lam_s_i - lam_prev
|
| 485 |
+
r0 = h0 / jnp.clip(h, 1e-12)
|
| 486 |
+
D1 = (x0 - m_prev) / jnp.clip(r0, 1e-12)
|
| 487 |
+
x_second = (s_t / s_s) * x - (a_t * (exp_neg_h - 1.0)) * (x0 + 0.5 * D1)
|
| 488 |
+
return x_second
|
| 489 |
+
|
| 490 |
+
x_next = jax.lax.cond(first_i | last_i, lambda _: x_first, do_second, operand=None)
|
| 491 |
+
return (x_next, x0, lam_s_i), x_next
|
| 492 |
+
|
| 493 |
+
xs = (
|
| 494 |
+
alpha_s, sigma_s_t,
|
| 495 |
+
alpha_t, sigma_t_t,
|
| 496 |
+
lambda_s, lambda_t,
|
| 497 |
+
t_cont, is_first, is_last
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
x0_init = jnp.zeros_like(x_start)
|
| 501 |
+
lam_init = jnp.array(0.0, dtype=jnp.float32)
|
| 502 |
+
|
| 503 |
+
(x_final, _, _), _ = jax.lax.scan(step, (x_start, x0_init, lam_init), xs)
|
| 504 |
+
return x_final
|
| 505 |
+
|
| 506 |
+
def refine_latents_dpmpp(
|
| 507 |
+
self,
|
| 508 |
+
z0: jnp.ndarray,
|
| 509 |
+
*,
|
| 510 |
+
t_start: int = 10,
|
| 511 |
+
num_steps: int = 20,
|
| 512 |
+
key: Optional[jax.Array] = None,
|
| 513 |
+
add_noise: bool = True,
|
| 514 |
+
) -> jnp.ndarray:
|
| 515 |
+
z0 = jnp.asarray(z0, dtype=jnp.float32)
|
| 516 |
+
if z0.ndim != 2 or z0.shape[1] != self.D:
|
| 517 |
+
raise ValueError(f"z0 must have shape (B,{self.D}).")
|
| 518 |
+
if not (0 <= int(t_start) < self.T):
|
| 519 |
+
raise ValueError(f"t_start must be in [0, {self.T-1}]")
|
| 520 |
+
|
| 521 |
+
if key is None:
|
| 522 |
+
self.key, key = random.split(self.key)
|
| 523 |
+
else:
|
| 524 |
+
self.key, _ = random.split(key)
|
| 525 |
+
|
| 526 |
+
# forward-noise to t_start
|
| 527 |
+
key, k_eps = random.split(key)
|
| 528 |
+
eps_noise = random.normal(k_eps, z0.shape)
|
| 529 |
+
a_bar = jnp.clip(self.alpha_bar_s[int(t_start)], self.eps, 1.0)
|
| 530 |
+
|
| 531 |
+
if add_noise:
|
| 532 |
+
x_start = jnp.sqrt(a_bar) * z0 + jnp.sqrt(1.0 - a_bar) * eps_noise
|
| 533 |
+
else:
|
| 534 |
+
x_start = z0
|
| 535 |
+
|
| 536 |
+
sigmas_in, t_cont = self._make_dpmpp_schedule(num_steps=int(num_steps), t_start=int(t_start))
|
| 537 |
+
x_final = self._dpmpp_2m_midpoint_sample(
|
| 538 |
+
self.state.ema_params,
|
| 539 |
+
self.state.apply_fn,
|
| 540 |
+
x_start,
|
| 541 |
+
sigmas_in,
|
| 542 |
+
t_cont,
|
| 543 |
+
self.eps,
|
| 544 |
+
)
|
| 545 |
+
return x_final
|
| 546 |
+
|
| 547 |
+
def reverse_from_T_dpmpp(self, x_T: jnp.ndarray, *, num_steps: int = 20) -> jnp.ndarray:
|
| 548 |
+
x_T = jnp.asarray(x_T, dtype=jnp.float32)
|
| 549 |
+
if x_T.ndim != 2 or x_T.shape[1] != self.D:
|
| 550 |
+
raise ValueError(f"x_T must have shape (B,{self.D}).")
|
| 551 |
+
|
| 552 |
+
sigmas_in, t_cont = self._make_dpmpp_schedule(num_steps=int(num_steps), t_start=self.T - 1)
|
| 553 |
+
return self._dpmpp_2m_midpoint_sample(
|
| 554 |
+
self.state.ema_params,
|
| 555 |
+
self.state.apply_fn,
|
| 556 |
+
x_T,
|
| 557 |
+
sigmas_in,
|
| 558 |
+
t_cont,
|
| 559 |
+
self.eps,
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
def sample_dpmpp(self, N: int = 10_000, *, num_steps: int = 20) -> jnp.ndarray:
|
| 563 |
+
self.key, k = random.split(self.key)
|
| 564 |
+
x_T = random.normal(k, (int(N), self.D)).astype(jnp.float32)
|
| 565 |
+
return self.reverse_from_T_dpmpp(x_T, num_steps=int(num_steps))
|
| 566 |
+
|
| 567 |
+
# -----------------------------------------------------------------
|
| 568 |
+
# Persistence: state_dict / save_local / load_local
|
| 569 |
+
# -----------------------------------------------------------------
|
| 570 |
+
def _config_dict(self) -> Dict[str, Any]:
|
| 571 |
+
return {
|
| 572 |
+
"class_name": "DDPMX",
|
| 573 |
+
"T": int(self.T),
|
| 574 |
+
"D": int(self.D),
|
| 575 |
+
"hidden_dim": int(self.hidden_dim),
|
| 576 |
+
"t_embed_dim": int(self.t_embed_dim),
|
| 577 |
+
"learning_rate": float(self.learning_rate),
|
| 578 |
+
"ema_decay": float(self.ema_decay),
|
| 579 |
+
"beta_max": float(self.beta_max),
|
| 580 |
+
"batch_size": None if self.batch_size is None else int(self.batch_size),
|
| 581 |
+
"eps": float(self.eps),
|
| 582 |
+
"key": np.array(self.key).tolist(),
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
def save_local(self, weights_file: str = "ddpmx_weights.msgpack", config_file: str = "ddpmx_config.json") -> None:
|
| 586 |
+
"""
|
| 587 |
+
Saves:
|
| 588 |
+
- config_file: JSON with hyperparams + PRNG key
|
| 589 |
+
- weights_file: msgpack with flax state_dict of TrainStateEMA
|
| 590 |
+
"""
|
| 591 |
+
cfg = self._config_dict()
|
| 592 |
+
with open(config_file, "w", encoding="utf-8") as f:
|
| 593 |
+
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
| 594 |
+
|
| 595 |
+
# Robust serialization (avoid msgpack tuple errors)
|
| 596 |
+
state_sd = flax_ser.to_state_dict(self.state)
|
| 597 |
+
blob = flax_ser.msgpack_serialize(state_sd)
|
| 598 |
+
with open(weights_file, "wb") as f:
|
| 599 |
+
f.write(blob)
|
| 600 |
+
|
| 601 |
+
@classmethod
|
| 602 |
+
def load_local(
|
| 603 |
+
cls,
|
| 604 |
+
weights_file: str,
|
| 605 |
+
config_file: str,
|
| 606 |
+
*,
|
| 607 |
+
Z_iX: Optional[jnp.ndarray] = None,
|
| 608 |
+
) -> "DDPM":
|
| 609 |
+
"""
|
| 610 |
+
Reconstructs a DDPMX instance from local files.
|
| 611 |
+
|
| 612 |
+
Z_iX is only used to provide shape (N,D) for initialization; training is skipped.
|
| 613 |
+
If Z_iX is None, a dummy array of shape (1,D) is created.
|
| 614 |
+
"""
|
| 615 |
+
with open(config_file, "r", encoding="utf-8") as f:
|
| 616 |
+
cfg = json.load(f)
|
| 617 |
+
|
| 618 |
+
D = int(cfg["D"])
|
| 619 |
+
if Z_iX is None:
|
| 620 |
+
Z_iX = jnp.zeros((1, D), dtype=jnp.float32)
|
| 621 |
+
|
| 622 |
+
# Build a fresh instance with the same architecture, skip training
|
| 623 |
+
obj = cls(
|
| 624 |
+
Z_iX,
|
| 625 |
+
T=int(cfg["T"]),
|
| 626 |
+
hidden_dim=int(cfg["hidden_dim"]),
|
| 627 |
+
t_embed_dim=int(cfg["t_embed_dim"]),
|
| 628 |
+
learning_rate=float(cfg["learning_rate"]),
|
| 629 |
+
n_iter=0,
|
| 630 |
+
ema_decay=float(cfg["ema_decay"]),
|
| 631 |
+
beta_max=float(cfg["beta_max"]),
|
| 632 |
+
batch_size=cfg["batch_size"],
|
| 633 |
+
key=random.PRNGKey(0),
|
| 634 |
+
verbose_every=0,
|
| 635 |
+
eps=float(cfg["eps"]),
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
with open(weights_file, "rb") as f:
|
| 639 |
+
state_sd = flax_ser.msgpack_restore(f.read())
|
| 640 |
+
|
| 641 |
+
obj.state = flax_ser.from_state_dict(obj.state, state_sd)
|
| 642 |
+
|
| 643 |
+
key_list = cfg.get("key", None)
|
| 644 |
+
if key_list is not None:
|
| 645 |
+
obj.key = jnp.array(key_list, dtype=jnp.uint32)
|
| 646 |
+
|
| 647 |
+
return obj
|
| 648 |
+
|
| 649 |
+
# -----------------------------------------------------------------
|
| 650 |
+
# Hugging Face Hub: upload / download
|
| 651 |
+
# -----------------------------------------------------------------
|
| 652 |
+
def upload_to_huggingface(
|
| 653 |
+
self,
|
| 654 |
+
repo_id: str,
|
| 655 |
+
*,
|
| 656 |
+
token: Optional[str] = None,
|
| 657 |
+
weights_file: str = "ddpmx_weights.msgpack",
|
| 658 |
+
config_file: str = "ddpmx_config.json",
|
| 659 |
+
repo_type: str = "model",
|
| 660 |
+
revision: Optional[str] = None,
|
| 661 |
+
) -> Dict[str, str]:
|
| 662 |
+
"""
|
| 663 |
+
Saves locally and uploads (weights_file, config_file) to Hugging Face Hub.
|
| 664 |
+
"""
|
| 665 |
+
try:
|
| 666 |
+
from huggingface_hub import create_repo, upload_file
|
| 667 |
+
except Exception as e:
|
| 668 |
+
raise RuntimeError(
|
| 669 |
+
"huggingface_hub not installed. Install it (e.g., `pip install huggingface_hub`)."
|
| 670 |
+
) from e
|
| 671 |
+
|
| 672 |
+
self.save_local(weights_file=weights_file, config_file=config_file)
|
| 673 |
+
|
| 674 |
+
create_repo(repo_id, token=token, repo_type=repo_type, exist_ok=True)
|
| 675 |
+
|
| 676 |
+
w_name = os.path.basename(weights_file)
|
| 677 |
+
c_name = os.path.basename(config_file)
|
| 678 |
+
|
| 679 |
+
upload_file(
|
| 680 |
+
path_or_fileobj=weights_file,
|
| 681 |
+
path_in_repo=w_name,
|
| 682 |
+
repo_id=repo_id,
|
| 683 |
+
repo_type=repo_type,
|
| 684 |
+
token=token,
|
| 685 |
+
revision=revision,
|
| 686 |
+
)
|
| 687 |
+
upload_file(
|
| 688 |
+
path_or_fileobj=config_file,
|
| 689 |
+
path_in_repo=c_name,
|
| 690 |
+
repo_id=repo_id,
|
| 691 |
+
repo_type=repo_type,
|
| 692 |
+
token=token,
|
| 693 |
+
revision=revision,
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
return {"repo_id": repo_id, "weights": w_name, "config": c_name}
|
| 697 |
+
|
| 698 |
+
@classmethod
|
| 699 |
+
def download_from_huggingface(
|
| 700 |
+
cls,
|
| 701 |
+
repo_id: str,
|
| 702 |
+
*,
|
| 703 |
+
token: Optional[str] = None,
|
| 704 |
+
weights_file: str = "ddpmx_weights.msgpack",
|
| 705 |
+
config_file: str = "ddpmx_config.json",
|
| 706 |
+
repo_type: str = "model",
|
| 707 |
+
revision: Optional[str] = None,
|
| 708 |
+
cache_dir: Optional[str] = None,
|
| 709 |
+
Z_iX: Optional[jnp.ndarray] = None,
|
| 710 |
+
) -> "DDPM":
|
| 711 |
+
"""
|
| 712 |
+
Downloads (weights_file, config_file) from Hugging Face Hub and reconstructs the class.
|
| 713 |
+
"""
|
| 714 |
+
try:
|
| 715 |
+
from huggingface_hub import hf_hub_download
|
| 716 |
+
except Exception as e:
|
| 717 |
+
raise RuntimeError(
|
| 718 |
+
"huggingface_hub not installed. Install it (e.g., `pip install huggingface_hub`)."
|
| 719 |
+
) from e
|
| 720 |
+
|
| 721 |
+
w_name = os.path.basename(weights_file)
|
| 722 |
+
c_name = os.path.basename(config_file)
|
| 723 |
+
|
| 724 |
+
w_path = hf_hub_download(
|
| 725 |
+
repo_id=repo_id,
|
| 726 |
+
filename=w_name,
|
| 727 |
+
repo_type=repo_type,
|
| 728 |
+
token=token,
|
| 729 |
+
revision=revision,
|
| 730 |
+
cache_dir=cache_dir,
|
| 731 |
+
)
|
| 732 |
+
c_path = hf_hub_download(
|
| 733 |
+
repo_id=repo_id,
|
| 734 |
+
filename=c_name,
|
| 735 |
+
repo_type=repo_type,
|
| 736 |
+
token=token,
|
| 737 |
+
revision=revision,
|
| 738 |
+
cache_dir=cache_dir,
|
| 739 |
+
)
|
| 740 |
+
|
| 741 |
+
return cls.load_local(w_path, c_path, Z_iX=Z_iX)
|
| 742 |
+
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
__all__ = ["DDPM", "EpsMLP", "cosine_schedule", "sinusoidal_embedding"]
|
dima.py
ADDED
|
@@ -0,0 +1,905 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/dima/dima.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import time
|
| 6 |
+
from dataclasses import asdict, dataclass
|
| 7 |
+
from typing import Any, Dict, Optional, Tuple, Union
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
import jax
|
| 12 |
+
import jax.numpy as jnp
|
| 13 |
+
from jax import random
|
| 14 |
+
|
| 15 |
+
from flax import serialization as flax_ser
|
| 16 |
+
|
| 17 |
+
from .ann import ANNBackend, make_ann
|
| 18 |
+
from .ddpm import DDPM
|
| 19 |
+
from .dmap import DMAP
|
| 20 |
+
from .gplm import GPLM
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# ----------------------------
|
| 24 |
+
# Optional: Hugging Face Hub
|
| 25 |
+
# ----------------------------
|
| 26 |
+
try:
|
| 27 |
+
from huggingface_hub import HfApi, HfFolder, upload_file, hf_hub_download # type: ignore
|
| 28 |
+
_HAS_HF = True
|
| 29 |
+
except Exception:
|
| 30 |
+
_HAS_HF = False
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
_UNSET = object()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _select_device(prefer: str = "auto"):
|
| 37 |
+
"""
|
| 38 |
+
Safe JAX device selection.
|
| 39 |
+
prefer: "auto" | "gpu" | "cpu"
|
| 40 |
+
"""
|
| 41 |
+
prefer = (prefer or "auto").lower()
|
| 42 |
+
devs = jax.devices()
|
| 43 |
+
gpu = [d for d in devs if d.platform == "gpu"]
|
| 44 |
+
cpu = [d for d in devs if d.platform == "cpu"]
|
| 45 |
+
|
| 46 |
+
if prefer in ("auto", "gpu"):
|
| 47 |
+
return gpu[0] if gpu else (cpu[0] if cpu else devs[0])
|
| 48 |
+
if prefer == "cpu":
|
| 49 |
+
return cpu[0] if cpu else devs[0]
|
| 50 |
+
return gpu[0] if gpu else (cpu[0] if cpu else devs[0])
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _np_dtype_str(x) -> str:
|
| 54 |
+
try:
|
| 55 |
+
return str(np.dtype(x))
|
| 56 |
+
except Exception:
|
| 57 |
+
return "float32"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# ----------------------------
|
| 61 |
+
# Frozen inference-only models
|
| 62 |
+
# ----------------------------
|
| 63 |
+
class FrozenDMAP:
|
| 64 |
+
"""
|
| 65 |
+
Inference-only Nyström DMAP embedder built from saved DMAP state.
|
| 66 |
+
Uses kNN in ambient space against reference points.
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def __init__(
|
| 70 |
+
self,
|
| 71 |
+
state: Dict[str, Any],
|
| 72 |
+
*,
|
| 73 |
+
ann_backend: ANNBackend = "auto",
|
| 74 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 75 |
+
n_jobs: int = -1,
|
| 76 |
+
):
|
| 77 |
+
self.k = int(state["k"])
|
| 78 |
+
self.beta = float(state["beta"])
|
| 79 |
+
self.β = self.beta
|
| 80 |
+
self.alpha = float(state["alpha"])
|
| 81 |
+
self.α = self.alpha
|
| 82 |
+
self.eps = float(state["eps"])
|
| 83 |
+
self.ε = self.eps
|
| 84 |
+
self.dtype = np.dtype(state.get("dtype", "float32"))
|
| 85 |
+
|
| 86 |
+
self.R_iX = np.ascontiguousarray(np.asarray(state["R_iX"]).astype(self.dtype, copy=False))
|
| 87 |
+
self.qalpha_i = np.asarray(state["qalpha_i"], dtype=np.float64)
|
| 88 |
+
self.qα_i = self.qalpha_i # alias
|
| 89 |
+
self.R_over_lam_ix = np.asarray(state["R_over_lam_ix"], dtype=np.float64) # (Nref,d)
|
| 90 |
+
self.R_over_λ_ix = self.R_over_lam_ix # alias
|
| 91 |
+
|
| 92 |
+
self.ann, self.ann_backend = make_ann(ann_backend, ann_params=ann_params, n_jobs=n_jobs)
|
| 93 |
+
self.ann.build(self.R_iX)
|
| 94 |
+
|
| 95 |
+
def __call__(self, R_aX: Union[np.ndarray, list], *, batch_size: Optional[int] = None) -> np.ndarray:
|
| 96 |
+
R_aX = np.asarray(R_aX)
|
| 97 |
+
single = (R_aX.ndim == 1)
|
| 98 |
+
if single:
|
| 99 |
+
R_aX = R_aX[None, :]
|
| 100 |
+
|
| 101 |
+
R_aX = np.ascontiguousarray(R_aX.astype(self.dtype, copy=False))
|
| 102 |
+
|
| 103 |
+
if batch_size is None:
|
| 104 |
+
Z = self._embed(R_aX)
|
| 105 |
+
else:
|
| 106 |
+
out = []
|
| 107 |
+
bs = int(batch_size)
|
| 108 |
+
for s in range(0, R_aX.shape[0], bs):
|
| 109 |
+
out.append(self._embed(R_aX[s : s + bs]))
|
| 110 |
+
Z = np.vstack(out)
|
| 111 |
+
|
| 112 |
+
return Z[0] if single else Z
|
| 113 |
+
|
| 114 |
+
def _embed(self, R_aX: np.ndarray) -> np.ndarray:
|
| 115 |
+
j_aK, D2_aK = self.ann.search(R_aX, self.k) # (a,k)
|
| 116 |
+
K_ai = np.exp(-self.beta * (D2_aK.astype(np.float64) / self.eps)) # (a,k)
|
| 117 |
+
|
| 118 |
+
q_a = np.maximum(K_ai.sum(axis=1), 1e-30)
|
| 119 |
+
qalpha_a = np.maximum(np.power(q_a, self.alpha), 1e-30)
|
| 120 |
+
|
| 121 |
+
qalpha_i = np.maximum(self.qalpha_i[j_aK], 1e-30)
|
| 122 |
+
Kalpha_ai = K_ai / (qalpha_a[:, None] * qalpha_i)
|
| 123 |
+
|
| 124 |
+
d_a = np.maximum(Kalpha_ai.sum(axis=1), 1e-30)
|
| 125 |
+
P_ai = Kalpha_ai / d_a[:, None]
|
| 126 |
+
|
| 127 |
+
R_over = self.R_over_lam_ix[j_aK, :] # (a,k,d)
|
| 128 |
+
Z_ax = (P_ai[:, :, None] * R_over).sum(axis=1) # (a,d)
|
| 129 |
+
return Z_ax
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _restore_gplm_as_object(
|
| 133 |
+
state: Dict[str, Any],
|
| 134 |
+
*,
|
| 135 |
+
ann_backend: ANNBackend = "auto",
|
| 136 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 137 |
+
n_jobs: int = -1,
|
| 138 |
+
) -> GPLM:
|
| 139 |
+
"""
|
| 140 |
+
Rehydrate a GPLM instance from saved state WITHOUT retraining.
|
| 141 |
+
This is deliberately done as a true GPLM instance so you also get GPLM.flow(...)
|
| 142 |
+
(assuming your GPLM class implements .flow()).
|
| 143 |
+
"""
|
| 144 |
+
obj = GPLM.__new__(GPLM) # type: ignore
|
| 145 |
+
|
| 146 |
+
obj.beta = float(state["beta"])
|
| 147 |
+
obj.β = obj.beta
|
| 148 |
+
obj.eps = float(state["eps"])
|
| 149 |
+
obj.ε = obj.eps
|
| 150 |
+
obj.pred_k = None if state.get("pred_k", None) is None else int(state["pred_k"])
|
| 151 |
+
obj.pred_κ = obj.pred_k
|
| 152 |
+
obj.dtype = np.dtype(state.get("dtype", "float32"))
|
| 153 |
+
|
| 154 |
+
obj.mean_X = np.asarray(state["mean_X"], dtype=np.float64)
|
| 155 |
+
obj.M_mX = np.asarray(state["M_mX"], dtype=np.float64)
|
| 156 |
+
|
| 157 |
+
obj.lat_mean_x = np.asarray(state["lat_mean_x"], dtype=np.float64)
|
| 158 |
+
obj.lat_std_x = np.asarray(state["lat_std_x"], dtype=np.float64)
|
| 159 |
+
|
| 160 |
+
obj.Z_mx_w = np.ascontiguousarray(np.asarray(state["Z_mx_w"]).astype(np.float64, copy=False))
|
| 161 |
+
obj.m = int(obj.Z_mx_w.shape[0])
|
| 162 |
+
|
| 163 |
+
obj.ann_Z, _ = make_ann(ann_backend, ann_params=ann_params, n_jobs=n_jobs)
|
| 164 |
+
obj.ann_Z.build(obj.Z_mx_w.astype(obj.dtype, copy=False))
|
| 165 |
+
|
| 166 |
+
return obj
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# ----------------------------
|
| 170 |
+
# Config
|
| 171 |
+
# ----------------------------
|
| 172 |
+
@dataclass
|
| 173 |
+
class DIMAConfig:
|
| 174 |
+
d: int = 32
|
| 175 |
+
beta: float = 1.0
|
| 176 |
+
ddpm_device: str = "auto" # "auto" | "cpu" | "gpu"
|
| 177 |
+
version: str = "0.2.0"
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# ----------------------------
|
| 181 |
+
# Main wrapper
|
| 182 |
+
# ----------------------------
|
| 183 |
+
class DIMA:
|
| 184 |
+
"""
|
| 185 |
+
DIMA: DMAP encoder + (latent DDPM) + GPLM decoder.
|
| 186 |
+
Public “user-facing” convention in this wrapper:
|
| 187 |
+
- Raw DMAP coordinates are the *public latent* (np.ndarray): R_ax (a,d)
|
| 188 |
+
- Normalized latents are the DDPM coordinates (jax/np): Z_ax (a,d)
|
| 189 |
+
Minimal user API (what you asked for):
|
| 190 |
+
dima = DIMA(R_iX, d=20, beta=2.0)
|
| 191 |
+
R_ax = dima(R_aX) # encode ambient -> raw latents
|
| 192 |
+
Q_aX = dima(R_ax) # decode raw latents -> ambient
|
| 193 |
+
You can still pass full dict overrides for any submodule:
|
| 194 |
+
dima = DIMA(..., dmap_kwargs={...}, gplm_kwargs={...}, ddpm_kwargs={...})
|
| 195 |
+
and you can also tweak the “headline” DDPM knobs directly in __init__ (below).
|
| 196 |
+
"""
|
| 197 |
+
|
| 198 |
+
def __init__(
|
| 199 |
+
self,
|
| 200 |
+
R_iX: np.ndarray,
|
| 201 |
+
*,
|
| 202 |
+
# main knobs
|
| 203 |
+
d: int = 32,
|
| 204 |
+
beta: float = 1.0,
|
| 205 |
+
# allow per-module override (if None -> uses global beta)
|
| 206 |
+
dmap_beta: Optional[float] = None,
|
| 207 |
+
gplm_beta: Optional[float] = None,
|
| 208 |
+
# DMAP headline knobs (everything else via dmap_kwargs)
|
| 209 |
+
dmap_alpha: float = 0.0,
|
| 210 |
+
dmap_t: float = 1.0,
|
| 211 |
+
dmap_k: Optional[int] = None,
|
| 212 |
+
# GPLM headline knobs (everything else via gplm_kwargs)
|
| 213 |
+
gplm_m: int = 1024,
|
| 214 |
+
gplm_pred_k: Optional[int] = None,
|
| 215 |
+
# DDPM headline knobs (the ones worth surfacing)
|
| 216 |
+
ddpm_T: int = 200,
|
| 217 |
+
ddpm_hidden_dim: int = 128,
|
| 218 |
+
ddpm_t_embed_dim: int = 64,
|
| 219 |
+
ddpm_learning_rate: float = 3e-4,
|
| 220 |
+
ddpm_n_iter: int = 200_000,
|
| 221 |
+
ddpm_ema_decay: float = 0.999,
|
| 222 |
+
ddpm_beta_max: float = 0.02,
|
| 223 |
+
ddpm_batch_size: int = 256,
|
| 224 |
+
ddpm_verbose_every: int = 0,
|
| 225 |
+
ddpm_eps: float = 1e-5,
|
| 226 |
+
# runtime
|
| 227 |
+
ddpm_device: str = "auto",
|
| 228 |
+
key: Optional[jax.Array] = None,
|
| 229 |
+
# ann
|
| 230 |
+
ann_backend: ANNBackend = "auto",
|
| 231 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 232 |
+
n_jobs: int = -1,
|
| 233 |
+
# “escape hatches”
|
| 234 |
+
dmap_kwargs: Optional[Dict[str, Any]] = None,
|
| 235 |
+
gplm_kwargs: Optional[Dict[str, Any]] = None,
|
| 236 |
+
ddpm_kwargs: Optional[Dict[str, Any]] = None,
|
| 237 |
+
# unicode aliases (β)
|
| 238 |
+
**kwargs: Any,
|
| 239 |
+
):
|
| 240 |
+
# ---- unicode aliases ----
|
| 241 |
+
if "β" in kwargs:
|
| 242 |
+
beta = float(kwargs.pop("β"))
|
| 243 |
+
if "β_dmap" in kwargs:
|
| 244 |
+
dmap_beta = float(kwargs.pop("β_dmap"))
|
| 245 |
+
if "β_gplm" in kwargs:
|
| 246 |
+
gplm_beta = float(kwargs.pop("β_gplm"))
|
| 247 |
+
if kwargs:
|
| 248 |
+
raise TypeError(f"Unexpected kwargs: {sorted(kwargs.keys())}")
|
| 249 |
+
|
| 250 |
+
self.config = DIMAConfig(d=int(d), beta=float(beta), ddpm_device=str(ddpm_device))
|
| 251 |
+
|
| 252 |
+
# devices + rng
|
| 253 |
+
self.ddpm_device = _select_device(ddpm_device)
|
| 254 |
+
self.cpu_device = _select_device("cpu")
|
| 255 |
+
self.rng = random.PRNGKey(0) if key is None else key
|
| 256 |
+
|
| 257 |
+
# training data
|
| 258 |
+
self.R_iX = np.asarray(R_iX)
|
| 259 |
+
if self.R_iX.ndim != 2:
|
| 260 |
+
raise ValueError("R_iX must be 2D (N,D).")
|
| 261 |
+
self.N, self.D = self.R_iX.shape
|
| 262 |
+
self.d = int(d)
|
| 263 |
+
|
| 264 |
+
# betas
|
| 265 |
+
self.beta = float(beta)
|
| 266 |
+
self.β = self.beta
|
| 267 |
+
|
| 268 |
+
self.dmap_beta = float(self.beta if dmap_beta is None else dmap_beta)
|
| 269 |
+
self.gplm_beta = float(self.beta if gplm_beta is None else gplm_beta)
|
| 270 |
+
|
| 271 |
+
t0 = time.time()
|
| 272 |
+
|
| 273 |
+
# -------------------------
|
| 274 |
+
# 1) Train DMAP (CPU)
|
| 275 |
+
# -------------------------
|
| 276 |
+
dmap_init = dict(
|
| 277 |
+
d=self.d,
|
| 278 |
+
beta=self.dmap_beta,
|
| 279 |
+
alpha=float(dmap_alpha),
|
| 280 |
+
t=float(dmap_t),
|
| 281 |
+
k=dmap_k,
|
| 282 |
+
ann_backend=ann_backend,
|
| 283 |
+
ann_params=ann_params,
|
| 284 |
+
n_jobs=n_jobs,
|
| 285 |
+
)
|
| 286 |
+
if dmap_kwargs:
|
| 287 |
+
dmap_init.update(dict(dmap_kwargs))
|
| 288 |
+
# enforce headline knobs
|
| 289 |
+
dmap_init["d"] = self.d
|
| 290 |
+
dmap_init["beta"] = self.dmap_beta
|
| 291 |
+
dmap_init["alpha"] = float(dmap_alpha)
|
| 292 |
+
dmap_init["t"] = float(dmap_t)
|
| 293 |
+
dmap_init["k"] = dmap_k
|
| 294 |
+
|
| 295 |
+
self.enc = DMAP(self.R_iX, **dmap_init)
|
| 296 |
+
|
| 297 |
+
# raw DMAP coordinates for *all* training points (Nyström OOS on training set)
|
| 298 |
+
R_ix = np.asarray(self.enc(self.R_iX), dtype=np.float64) # (N,d)
|
| 299 |
+
|
| 300 |
+
# -------------------------
|
| 301 |
+
# 2) Latent normalization for DDPM
|
| 302 |
+
# -------------------------
|
| 303 |
+
self.lat_mean_np = R_ix.mean(axis=0)
|
| 304 |
+
self.lat_std_np = np.maximum(R_ix.std(axis=0), 1e-12)
|
| 305 |
+
|
| 306 |
+
self.lat_mean_j = jax.device_put(jnp.asarray(self.lat_mean_np, dtype=jnp.float32), self.ddpm_device)
|
| 307 |
+
self.lat_std_j = jax.device_put(jnp.asarray(self.lat_std_np, dtype=jnp.float32), self.ddpm_device)
|
| 308 |
+
|
| 309 |
+
Z_ix = (R_ix - self.lat_mean_np) / self.lat_std_np # (N,d)
|
| 310 |
+
|
| 311 |
+
# -------------------------
|
| 312 |
+
# 3) Train GPLM (CPU)
|
| 313 |
+
# -------------------------
|
| 314 |
+
gplm_init = dict(
|
| 315 |
+
beta=self.gplm_beta,
|
| 316 |
+
m=int(gplm_m),
|
| 317 |
+
pred_k=gplm_pred_k,
|
| 318 |
+
ann_backend=ann_backend,
|
| 319 |
+
ann_params=ann_params,
|
| 320 |
+
n_jobs=n_jobs,
|
| 321 |
+
)
|
| 322 |
+
if gplm_kwargs:
|
| 323 |
+
gplm_init.update(dict(gplm_kwargs))
|
| 324 |
+
# enforce headline knobs
|
| 325 |
+
gplm_init["beta"] = self.gplm_beta
|
| 326 |
+
gplm_init["m"] = int(gplm_m)
|
| 327 |
+
gplm_init["pred_k"] = gplm_pred_k
|
| 328 |
+
|
| 329 |
+
self.dec = GPLM(R_ix.astype(np.float32, copy=False), self.R_iX, **gplm_init)
|
| 330 |
+
|
| 331 |
+
# -------------------------
|
| 332 |
+
# 4) Train DDPM on normalized latents (DDPM device)
|
| 333 |
+
# -------------------------
|
| 334 |
+
Z_ix_j = jax.device_put(jnp.asarray(Z_ix, dtype=jnp.float32), self.ddpm_device)
|
| 335 |
+
|
| 336 |
+
ddpm_init = dict(
|
| 337 |
+
T=int(ddpm_T),
|
| 338 |
+
hidden_dim=int(ddpm_hidden_dim),
|
| 339 |
+
t_embed_dim=int(ddpm_t_embed_dim),
|
| 340 |
+
learning_rate=float(ddpm_learning_rate),
|
| 341 |
+
n_iter=int(ddpm_n_iter),
|
| 342 |
+
ema_decay=float(ddpm_ema_decay),
|
| 343 |
+
beta_max=float(ddpm_beta_max),
|
| 344 |
+
batch_size=int(ddpm_batch_size),
|
| 345 |
+
key=self.rng,
|
| 346 |
+
verbose_every=int(ddpm_verbose_every),
|
| 347 |
+
eps=float(ddpm_eps),
|
| 348 |
+
)
|
| 349 |
+
if ddpm_kwargs:
|
| 350 |
+
ddpm_init.update(dict(ddpm_kwargs))
|
| 351 |
+
# enforce headline knobs
|
| 352 |
+
ddpm_init["T"] = int(ddpm_T)
|
| 353 |
+
ddpm_init["hidden_dim"] = int(ddpm_hidden_dim)
|
| 354 |
+
ddpm_init["t_embed_dim"] = int(ddpm_t_embed_dim)
|
| 355 |
+
ddpm_init["learning_rate"] = float(ddpm_learning_rate)
|
| 356 |
+
ddpm_init["n_iter"] = int(ddpm_n_iter)
|
| 357 |
+
ddpm_init["ema_decay"] = float(ddpm_ema_decay)
|
| 358 |
+
ddpm_init["beta_max"] = float(ddpm_beta_max)
|
| 359 |
+
ddpm_init["batch_size"] = int(ddpm_batch_size)
|
| 360 |
+
ddpm_init["verbose_every"] = int(ddpm_verbose_every)
|
| 361 |
+
ddpm_init["eps"] = float(ddpm_eps)
|
| 362 |
+
|
| 363 |
+
with jax.default_device(self.ddpm_device):
|
| 364 |
+
self.dm = DDPM(Z_ix_j, **ddpm_init)
|
| 365 |
+
|
| 366 |
+
self.training_time = time.time() - t0
|
| 367 |
+
|
| 368 |
+
# -------------------------
|
| 369 |
+
# Latent conversions
|
| 370 |
+
# -------------------------
|
| 371 |
+
def normalize(self, R_ax: Union[np.ndarray, jnp.ndarray]) -> jnp.ndarray:
|
| 372 |
+
"""raw latents (R) -> normalized latents (Z) on ddpm_device."""
|
| 373 |
+
R = np.asarray(R_ax, dtype=np.float64)
|
| 374 |
+
if R.ndim == 1:
|
| 375 |
+
R = R[None, :]
|
| 376 |
+
Z = (R - self.lat_mean_np) / self.lat_std_np
|
| 377 |
+
Zj = jnp.asarray(Z, dtype=jnp.float32)
|
| 378 |
+
return jax.device_put(Zj, self.ddpm_device)
|
| 379 |
+
|
| 380 |
+
def unnormalize(self, Z_ax: Union[np.ndarray, jnp.ndarray]) -> np.ndarray:
|
| 381 |
+
"""normalized latents (Z) -> raw latents (R) on CPU (np)."""
|
| 382 |
+
if isinstance(Z_ax, jax.Array):
|
| 383 |
+
Z_np = np.asarray(jax.device_get(Z_ax))
|
| 384 |
+
else:
|
| 385 |
+
Z_np = np.asarray(Z_ax)
|
| 386 |
+
if Z_np.ndim == 1:
|
| 387 |
+
Z_np = Z_np[None, :]
|
| 388 |
+
R = Z_np * self.lat_std_np + self.lat_mean_np
|
| 389 |
+
return np.asarray(R)
|
| 390 |
+
|
| 391 |
+
# -------------------------
|
| 392 |
+
# Encode / Decode (public: raw latents)
|
| 393 |
+
# -------------------------
|
| 394 |
+
def encode(self, R_aX: Union[np.ndarray, jnp.ndarray], *, normalize: bool = False) -> Union[np.ndarray, jnp.ndarray]:
|
| 395 |
+
"""
|
| 396 |
+
ambient -> raw DMAP latents (np) by default.
|
| 397 |
+
If normalize=True, returns normalized latents (jnp) on ddpm_device.
|
| 398 |
+
"""
|
| 399 |
+
X = np.asarray(R_aX)
|
| 400 |
+
R_raw = np.asarray(self.enc(X)) # CPU, (a,d)
|
| 401 |
+
if not normalize:
|
| 402 |
+
return R_raw
|
| 403 |
+
return self.normalize(R_raw)
|
| 404 |
+
|
| 405 |
+
def decode(
|
| 406 |
+
self,
|
| 407 |
+
R_ax: Union[np.ndarray, jnp.ndarray],
|
| 408 |
+
*,
|
| 409 |
+
refine: bool = False,
|
| 410 |
+
t_start: int = 10,
|
| 411 |
+
add_noise: bool = True,
|
| 412 |
+
key: Optional[jax.Array] = None,
|
| 413 |
+
batch_size: Optional[int] = None,
|
| 414 |
+
) -> np.ndarray:
|
| 415 |
+
"""
|
| 416 |
+
raw latent -> (optional DDPM refine in normalized coords) -> raw latent -> ambient.
|
| 417 |
+
Returns ambient np.ndarray on CPU.
|
| 418 |
+
"""
|
| 419 |
+
R_raw = np.asarray(R_ax, dtype=np.float64)
|
| 420 |
+
single = (R_raw.ndim == 1)
|
| 421 |
+
if single:
|
| 422 |
+
R_raw = R_raw[None, :]
|
| 423 |
+
|
| 424 |
+
if refine:
|
| 425 |
+
Z = self.normalize(R_raw) # on device
|
| 426 |
+
Z = self.dm.refine_latents(Z, t_start=int(t_start), key=key, add_noise=bool(add_noise))
|
| 427 |
+
R_raw = self.unnormalize(Z) # back to CPU raw
|
| 428 |
+
|
| 429 |
+
X_hat = self.dec(R_raw.astype(np.float32, copy=False), batch_size=batch_size)
|
| 430 |
+
X_hat = np.asarray(X_hat)
|
| 431 |
+
return X_hat[0] if single else X_hat
|
| 432 |
+
|
| 433 |
+
def reconstruct(
|
| 434 |
+
self,
|
| 435 |
+
R_aX: Union[np.ndarray, jnp.ndarray],
|
| 436 |
+
*,
|
| 437 |
+
refine: bool = False,
|
| 438 |
+
t_start: int = 10,
|
| 439 |
+
add_noise: bool = True,
|
| 440 |
+
key: Optional[jax.Array] = None,
|
| 441 |
+
batch_size: Optional[int] = None,
|
| 442 |
+
) -> np.ndarray:
|
| 443 |
+
"""decode(encode(X))."""
|
| 444 |
+
R_raw = self.encode(R_aX, normalize=False)
|
| 445 |
+
return self.decode(R_raw, refine=refine, t_start=t_start, add_noise=add_noise, key=key, batch_size=batch_size)
|
| 446 |
+
|
| 447 |
+
def sample(
|
| 448 |
+
self,
|
| 449 |
+
n: int,
|
| 450 |
+
*,
|
| 451 |
+
decode: bool = True,
|
| 452 |
+
batch_size: Optional[int] = None,
|
| 453 |
+
) -> Union[np.ndarray, np.ndarray]:
|
| 454 |
+
"""
|
| 455 |
+
Unconditional samples from latent DDPM.
|
| 456 |
+
If decode=True: returns ambient samples (np) on CPU.
|
| 457 |
+
If decode=False: returns raw latents (np) on CPU.
|
| 458 |
+
"""
|
| 459 |
+
with jax.default_device(self.ddpm_device):
|
| 460 |
+
Z = self.dm.sample(int(n))
|
| 461 |
+
R = self.unnormalize(Z) # raw (np)
|
| 462 |
+
if not decode:
|
| 463 |
+
return R
|
| 464 |
+
return self.dec(R.astype(np.float32, copy=False), batch_size=batch_size)
|
| 465 |
+
|
| 466 |
+
# -------------------------
|
| 467 |
+
# Geodesic-ish flow wrapper (delegates to GPLM.flow)
|
| 468 |
+
# -------------------------
|
| 469 |
+
def flow(
|
| 470 |
+
self,
|
| 471 |
+
R_ax: Union[np.ndarray, jnp.ndarray],
|
| 472 |
+
v_ax: Union[np.ndarray, jnp.ndarray],
|
| 473 |
+
*,
|
| 474 |
+
dt: float = 0.05,
|
| 475 |
+
reg: float = 1e-8,
|
| 476 |
+
keep_speed: bool = True,
|
| 477 |
+
# optional DDPM projection step (in normalized coords)
|
| 478 |
+
refine: bool = False,
|
| 479 |
+
t_start: int = 10,
|
| 480 |
+
add_noise: bool = True,
|
| 481 |
+
key: Optional[jax.Array] = None,
|
| 482 |
+
# decode return
|
| 483 |
+
decode: bool = False,
|
| 484 |
+
batch_size: Optional[int] = None,
|
| 485 |
+
) -> Union[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray, np.ndarray]]:
|
| 486 |
+
"""
|
| 487 |
+
One step of latent flow in *raw* coordinates.
|
| 488 |
+
Requires: your GPLM class implements:
|
| 489 |
+
R_next, v_next = gplm.flow(R, v, dt=..., reg=..., keep_speed=...)
|
| 490 |
+
If refine=True, we project the *position* through DDPM in normalized coords after the step.
|
| 491 |
+
(Velocity after projection is left unchanged—projection isn’t a deterministic diffeo.)
|
| 492 |
+
Returns:
|
| 493 |
+
if decode=False:
|
| 494 |
+
(R_next, v_next) both np arrays
|
| 495 |
+
if decode=True:
|
| 496 |
+
(X_next, R_next, v_next)
|
| 497 |
+
"""
|
| 498 |
+
R = np.asarray(R_ax, dtype=np.float64)
|
| 499 |
+
v = np.asarray(v_ax, dtype=np.float64)
|
| 500 |
+
single = (R.ndim == 1)
|
| 501 |
+
if single:
|
| 502 |
+
R = R[None, :]
|
| 503 |
+
v = v[None, :]
|
| 504 |
+
|
| 505 |
+
if not hasattr(self.dec, "flow"):
|
| 506 |
+
raise AttributeError(
|
| 507 |
+
"Decoder does not have .flow(). Make sure you updated GPLM to include flow()."
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
Rn, vn = self.dec.flow(R, v, dt=float(dt), reg=float(reg), keep_speed=bool(keep_speed))
|
| 511 |
+
|
| 512 |
+
if refine:
|
| 513 |
+
Z = self.normalize(Rn) # device
|
| 514 |
+
Z = self.dm.refine_latents(Z, t_start=int(t_start), key=key, add_noise=bool(add_noise))
|
| 515 |
+
Rn = self.unnormalize(Z)
|
| 516 |
+
|
| 517 |
+
if not decode:
|
| 518 |
+
if single:
|
| 519 |
+
return np.asarray(Rn[0]), np.asarray(vn[0])
|
| 520 |
+
return np.asarray(Rn), np.asarray(vn)
|
| 521 |
+
|
| 522 |
+
Xn = self.dec(Rn.astype(np.float32, copy=False), batch_size=batch_size)
|
| 523 |
+
Xn = np.asarray(Xn)
|
| 524 |
+
if single:
|
| 525 |
+
return Xn[0], np.asarray(Rn[0]), np.asarray(vn[0])
|
| 526 |
+
return Xn, np.asarray(Rn), np.asarray(vn)
|
| 527 |
+
|
| 528 |
+
# -------------------------
|
| 529 |
+
# Convenience __call__
|
| 530 |
+
# -------------------------
|
| 531 |
+
def __call__(
|
| 532 |
+
self,
|
| 533 |
+
A: Union[np.ndarray, jnp.ndarray],
|
| 534 |
+
*,
|
| 535 |
+
refine: bool = False,
|
| 536 |
+
t_start: int = 10,
|
| 537 |
+
add_noise: bool = True,
|
| 538 |
+
key: Optional[jax.Array] = None,
|
| 539 |
+
batch_size: Optional[int] = None,
|
| 540 |
+
normalize_latent: bool = False,
|
| 541 |
+
) -> Union[np.ndarray, jnp.ndarray]:
|
| 542 |
+
"""
|
| 543 |
+
Dispatch by last dimension:
|
| 544 |
+
- if A is (a,D): encode -> raw latents (np) by default
|
| 545 |
+
- if A is (a,d): decode -> ambient (np)
|
| 546 |
+
Options:
|
| 547 |
+
- normalize_latent=True only affects encoding (returns Z on device)
|
| 548 |
+
- refine/t_start/add_noise/key only affect decoding
|
| 549 |
+
"""
|
| 550 |
+
A_np = np.asarray(A)
|
| 551 |
+
if A_np.ndim == 1:
|
| 552 |
+
A_np = A_np[None, :]
|
| 553 |
+
|
| 554 |
+
if A_np.shape[1] == self.D:
|
| 555 |
+
return self.encode(A_np, normalize=bool(normalize_latent))
|
| 556 |
+
|
| 557 |
+
if A_np.shape[1] == self.d:
|
| 558 |
+
return self.decode(
|
| 559 |
+
A_np,
|
| 560 |
+
refine=bool(refine),
|
| 561 |
+
t_start=int(t_start),
|
| 562 |
+
add_noise=bool(add_noise),
|
| 563 |
+
key=key,
|
| 564 |
+
batch_size=batch_size,
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
raise ValueError(f"Input has last-dim {A_np.shape[1]}, expected D={self.D} or d={self.d}.")
|
| 568 |
+
|
| 569 |
+
# -------------------------
|
| 570 |
+
# Save / Load
|
| 571 |
+
# -------------------------
|
| 572 |
+
def _pack_encoder(self) -> Dict[str, Any]:
|
| 573 |
+
enc = self.enc
|
| 574 |
+
|
| 575 |
+
# qalpha_i (ascii/unicode)
|
| 576 |
+
qalpha_i = np.asarray(getattr(enc, "qalpha_i", getattr(enc, "qα_i")))
|
| 577 |
+
|
| 578 |
+
# 1) Try to read R_over_* from whichever name exists
|
| 579 |
+
R_over = getattr(enc, "R_over_lam_ix", None)
|
| 580 |
+
if R_over is None:
|
| 581 |
+
R_over = getattr(enc, "R_over_λ_ix", None)
|
| 582 |
+
|
| 583 |
+
# 2) If missing, compute it from R_ix and λ_x (most robust)
|
| 584 |
+
if R_over is None:
|
| 585 |
+
R_ix = getattr(enc, "R_ix", None)
|
| 586 |
+
|
| 587 |
+
# λ_x is unicode in upstream; add fallbacks for safety
|
| 588 |
+
lam = getattr(enc, "λ_x", None)
|
| 589 |
+
if lam is None:
|
| 590 |
+
lam = getattr(enc, "lam_x", None)
|
| 591 |
+
if lam is None:
|
| 592 |
+
lam = getattr(enc, "lambda_x", None)
|
| 593 |
+
|
| 594 |
+
if R_ix is None or lam is None:
|
| 595 |
+
raise AttributeError(
|
| 596 |
+
"DMAP encoder is missing R_over_{λ,lam}_ix and also lacks (R_ix, λ_x) "
|
| 597 |
+
"to reconstruct it. Please ensure your DMAP computes diffusion coords."
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
lam = np.asarray(lam, dtype=np.float64)
|
| 601 |
+
lam = np.maximum(lam, 1e-30) # avoid division by 0
|
| 602 |
+
R_ix = np.asarray(R_ix, dtype=np.float64)
|
| 603 |
+
|
| 604 |
+
R_over = (R_ix / lam[None, :]).astype(np.float64, copy=False)
|
| 605 |
+
|
| 606 |
+
return dict(
|
| 607 |
+
R_iX=np.asarray(enc.R_iX),
|
| 608 |
+
qalpha_i=qalpha_i,
|
| 609 |
+
# Always store with ASCII key expected by FrozenDMAP loader
|
| 610 |
+
R_over_lam_ix=np.asarray(R_over, dtype=np.float64),
|
| 611 |
+
k=int(enc.k),
|
| 612 |
+
beta=float(getattr(enc, "beta", getattr(enc, "β"))),
|
| 613 |
+
alpha=float(getattr(enc, "alpha", getattr(enc, "α"))),
|
| 614 |
+
eps=float(getattr(enc, "eps", getattr(enc, "ε"))),
|
| 615 |
+
dtype=_np_dtype_str(getattr(enc, "dtype", np.float32)),
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
def _pack_decoder(self) -> Dict[str, Any]:
|
| 620 |
+
return dict(
|
| 621 |
+
Z_mx_w=np.asarray(getattr(self.dec, "Z_mx_w", None)),
|
| 622 |
+
M_mX=np.asarray(self.dec.M_mX),
|
| 623 |
+
mean_X=np.asarray(getattr(self.dec, "mean_X", np.zeros((self.D,), dtype=np.float64))),
|
| 624 |
+
lat_mean_x=np.asarray(getattr(self.dec, "lat_mean_x", np.zeros((self.d,), dtype=np.float64))),
|
| 625 |
+
lat_std_x=np.asarray(getattr(self.dec, "lat_std_x", np.ones((self.d,), dtype=np.float64))),
|
| 626 |
+
beta=float(getattr(self.dec, "beta", getattr(self.dec, "β"))),
|
| 627 |
+
eps=float(getattr(self.dec, "eps", getattr(self.dec, "ε"))),
|
| 628 |
+
pred_k=getattr(self.dec, "pred_k", getattr(self.dec, "pred_κ", None)),
|
| 629 |
+
dtype=_np_dtype_str(getattr(self.dec, "dtype", np.float32)),
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
def state_dict(self) -> Dict[str, Any]:
|
| 633 |
+
dd = dict(
|
| 634 |
+
T=int(self.dm.T),
|
| 635 |
+
D=int(self.dm.D),
|
| 636 |
+
hidden_dim=int(getattr(self.dm.model, "hidden", 128)),
|
| 637 |
+
t_embed_dim=int(getattr(self.dm.model, "t_dim", 64)),
|
| 638 |
+
ema_decay=float(getattr(self.dm, "ema_decay", 0.999)),
|
| 639 |
+
beta_max=float(getattr(self.dm, "beta_max", 0.02)),
|
| 640 |
+
eps=float(getattr(self.dm, "eps", 1e-5)),
|
| 641 |
+
params=self.dm.state.params,
|
| 642 |
+
ema_params=self.dm.state.ema_params,
|
| 643 |
+
)
|
| 644 |
+
|
| 645 |
+
state = dict(
|
| 646 |
+
meta=dict(
|
| 647 |
+
N=int(self.N),
|
| 648 |
+
D=int(self.D),
|
| 649 |
+
d=int(self.d),
|
| 650 |
+
training_time=float(getattr(self, "training_time", 0.0)),
|
| 651 |
+
),
|
| 652 |
+
config=asdict(self.config),
|
| 653 |
+
latent_norm=dict(
|
| 654 |
+
mean=np.asarray(self.lat_mean_np, dtype=np.float64),
|
| 655 |
+
std=np.asarray(self.lat_std_np, dtype=np.float64),
|
| 656 |
+
),
|
| 657 |
+
encoder=self._pack_encoder(),
|
| 658 |
+
decoder=self._pack_decoder(),
|
| 659 |
+
ddpm=dd,
|
| 660 |
+
)
|
| 661 |
+
return state
|
| 662 |
+
|
| 663 |
+
def save_local(self, weights_file: str = "dima.msgpack", config_file: str = "config.json") -> None:
|
| 664 |
+
"""
|
| 665 |
+
Save full DIMA state to a msgpack file + a readable JSON config.
|
| 666 |
+
|
| 667 |
+
This version is robust to accidental tuples inside the state tree
|
| 668 |
+
(msgpack cannot serialize tuples by default).
|
| 669 |
+
"""
|
| 670 |
+
def _sanitize(x):
|
| 671 |
+
# Convert tuples -> lists recursively (msgpack-safe)
|
| 672 |
+
if isinstance(x, tuple):
|
| 673 |
+
return [_sanitize(v) for v in x]
|
| 674 |
+
if isinstance(x, list):
|
| 675 |
+
return [_sanitize(v) for v in x]
|
| 676 |
+
if isinstance(x, dict):
|
| 677 |
+
return {k: _sanitize(v) for k, v in x.items()}
|
| 678 |
+
return x
|
| 679 |
+
|
| 680 |
+
state = _sanitize(self.state_dict())
|
| 681 |
+
blob = flax_ser.msgpack_serialize(state)
|
| 682 |
+
|
| 683 |
+
with open(weights_file, "wb") as f:
|
| 684 |
+
f.write(blob)
|
| 685 |
+
|
| 686 |
+
with open(config_file, "w") as f:
|
| 687 |
+
json.dump(state["config"], f, indent=2)
|
| 688 |
+
|
| 689 |
+
return None
|
| 690 |
+
|
| 691 |
+
|
| 692 |
+
@classmethod
|
| 693 |
+
def load_local(
|
| 694 |
+
cls,
|
| 695 |
+
weights_file: str = "dima.msgpack",
|
| 696 |
+
*,
|
| 697 |
+
ddpm_device: str = "auto",
|
| 698 |
+
ann_backend: ANNBackend = "auto",
|
| 699 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 700 |
+
n_jobs: int = -1,
|
| 701 |
+
key: Optional[jax.Array] = None,
|
| 702 |
+
) -> "DIMA":
|
| 703 |
+
with open(weights_file, "rb") as f:
|
| 704 |
+
state = flax_ser.msgpack_restore(f.read())
|
| 705 |
+
|
| 706 |
+
obj = cls.__new__(cls) # bypass __init__
|
| 707 |
+
|
| 708 |
+
obj.config = DIMAConfig(**state["config"])
|
| 709 |
+
obj.ddpm_device = _select_device(ddpm_device)
|
| 710 |
+
obj.cpu_device = _select_device("cpu")
|
| 711 |
+
obj.training_time = float(state["meta"].get("training_time", 0.0))
|
| 712 |
+
|
| 713 |
+
obj.N = int(state["meta"]["N"])
|
| 714 |
+
obj.D = int(state["meta"]["D"])
|
| 715 |
+
obj.d = int(state["meta"]["d"])
|
| 716 |
+
|
| 717 |
+
# RNG
|
| 718 |
+
obj.rng = random.PRNGKey(0) if key is None else key
|
| 719 |
+
|
| 720 |
+
# beta (for display / convenience)
|
| 721 |
+
obj.beta = float(obj.config.beta)
|
| 722 |
+
obj.β = obj.beta
|
| 723 |
+
|
| 724 |
+
# latent norm
|
| 725 |
+
obj.lat_mean_np = np.asarray(state["latent_norm"]["mean"], dtype=np.float64)
|
| 726 |
+
obj.lat_std_np = np.asarray(state["latent_norm"]["std"], dtype=np.float64)
|
| 727 |
+
|
| 728 |
+
obj.lat_mean_j = jax.device_put(jnp.asarray(obj.lat_mean_np, dtype=jnp.float32), obj.ddpm_device)
|
| 729 |
+
obj.lat_std_j = jax.device_put(jnp.asarray(obj.lat_std_np, dtype=jnp.float32), obj.ddpm_device)
|
| 730 |
+
|
| 731 |
+
# frozen encoder + rehydrated decoder-as-GPLM (so flow works if GPLM.flow exists)
|
| 732 |
+
obj.enc = FrozenDMAP(state["encoder"], ann_backend=ann_backend, ann_params=ann_params, n_jobs=n_jobs)
|
| 733 |
+
obj.dec = _restore_gplm_as_object(state["decoder"], ann_backend=ann_backend, ann_params=ann_params, n_jobs=n_jobs)
|
| 734 |
+
|
| 735 |
+
# rebuild DDPM skeleton with dummy data, then load params
|
| 736 |
+
dd = state["ddpm"]
|
| 737 |
+
T = int(dd["T"])
|
| 738 |
+
D = int(dd["D"])
|
| 739 |
+
hidden_dim = int(dd["hidden_dim"])
|
| 740 |
+
t_embed_dim = int(dd["t_embed_dim"])
|
| 741 |
+
ema_decay = float(dd.get("ema_decay", 0.999))
|
| 742 |
+
beta_max = float(dd.get("beta_max", 0.02))
|
| 743 |
+
eps = float(dd.get("eps", 1e-5))
|
| 744 |
+
|
| 745 |
+
dummy = jnp.zeros((1, D), dtype=jnp.float32)
|
| 746 |
+
with jax.default_device(obj.ddpm_device):
|
| 747 |
+
obj.dm = DDPM(
|
| 748 |
+
dummy,
|
| 749 |
+
T=T,
|
| 750 |
+
hidden_dim=hidden_dim,
|
| 751 |
+
t_embed_dim=t_embed_dim,
|
| 752 |
+
learning_rate=1e-3,
|
| 753 |
+
n_iter=0, # skip training on load
|
| 754 |
+
ema_decay=ema_decay,
|
| 755 |
+
beta_max=beta_max,
|
| 756 |
+
batch_size=1,
|
| 757 |
+
key=obj.rng,
|
| 758 |
+
verbose_every=0,
|
| 759 |
+
eps=eps,
|
| 760 |
+
)
|
| 761 |
+
obj.dm.state = obj.dm.state.replace(params=dd["params"], ema_params=dd["ema_params"])
|
| 762 |
+
|
| 763 |
+
obj.R_iX = None # training data not stored by default
|
| 764 |
+
return obj
|
| 765 |
+
|
| 766 |
+
# -------------------------
|
| 767 |
+
# (Optional) HF helpers
|
| 768 |
+
# -------------------------
|
| 769 |
+
|
| 770 |
+
def upload_to_huggingface(
|
| 771 |
+
self,
|
| 772 |
+
repo_id: str,
|
| 773 |
+
*,
|
| 774 |
+
weights_file: str = "dima.msgpack",
|
| 775 |
+
config_file: str = "config.json",
|
| 776 |
+
token: Optional[str] = None,
|
| 777 |
+
repo_type: str = "model",
|
| 778 |
+
revision: Optional[str] = None,
|
| 779 |
+
) -> None:
|
| 780 |
+
"""
|
| 781 |
+
Serialize locally (weights + config) and upload them to Hugging Face Hub.
|
| 782 |
+
|
| 783 |
+
Parameters
|
| 784 |
+
----------
|
| 785 |
+
repo_id : str
|
| 786 |
+
e.g. "username/my-dima-model"
|
| 787 |
+
weights_file : str
|
| 788 |
+
Filename used both locally and in the HF repo.
|
| 789 |
+
config_file : str
|
| 790 |
+
Human-readable JSON config filename (also uploaded).
|
| 791 |
+
token : Optional[str]
|
| 792 |
+
HF token. If None, uses HfFolder.get_token().
|
| 793 |
+
repo_type : str
|
| 794 |
+
Usually "model".
|
| 795 |
+
revision : Optional[str]
|
| 796 |
+
Optional target branch/revision (if your hub client supports it).
|
| 797 |
+
"""
|
| 798 |
+
if not _HAS_HF:
|
| 799 |
+
raise RuntimeError("huggingface_hub not installed. Install it (or `pip install dima[hf]`).")
|
| 800 |
+
|
| 801 |
+
# 1) Save artifacts locally
|
| 802 |
+
self.save_local(weights_file=weights_file, config_file=config_file)
|
| 803 |
+
|
| 804 |
+
# 2) Resolve token
|
| 805 |
+
if token is None:
|
| 806 |
+
token = HfFolder.get_token()
|
| 807 |
+
if token is None:
|
| 808 |
+
raise RuntimeError("No HF token found. Provide `token=...` or run `huggingface-cli login`.")
|
| 809 |
+
|
| 810 |
+
# 3) Create repo if needed
|
| 811 |
+
api = HfApi()
|
| 812 |
+
api.create_repo(repo_id=repo_id, repo_type=repo_type, exist_ok=True, token=token)
|
| 813 |
+
|
| 814 |
+
# 4) Upload files
|
| 815 |
+
common_kwargs = dict(repo_id=repo_id, repo_type=repo_type, token=token)
|
| 816 |
+
if revision is not None:
|
| 817 |
+
common_kwargs["revision"] = revision
|
| 818 |
+
|
| 819 |
+
upload_file(
|
| 820 |
+
path_or_fileobj=weights_file,
|
| 821 |
+
path_in_repo=weights_file,
|
| 822 |
+
**common_kwargs,
|
| 823 |
+
)
|
| 824 |
+
upload_file(
|
| 825 |
+
path_or_fileobj=config_file,
|
| 826 |
+
path_in_repo=config_file,
|
| 827 |
+
**common_kwargs,
|
| 828 |
+
)
|
| 829 |
+
return None
|
| 830 |
+
|
| 831 |
+
|
| 832 |
+
@classmethod
|
| 833 |
+
def download_from_huggingface(
|
| 834 |
+
cls,
|
| 835 |
+
repo_id: str,
|
| 836 |
+
*,
|
| 837 |
+
weights_file: str = "dima.msgpack",
|
| 838 |
+
ddpm_device: str = "auto",
|
| 839 |
+
ann_backend: "ANNBackend" = "auto",
|
| 840 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 841 |
+
n_jobs: int = -1,
|
| 842 |
+
key: Optional["jax.Array"] = None,
|
| 843 |
+
token: Optional[str] = None,
|
| 844 |
+
repo_type: str = "model",
|
| 845 |
+
revision: Optional[str] = None,
|
| 846 |
+
) -> "DIMA":
|
| 847 |
+
"""
|
| 848 |
+
Download weights from HF Hub and rehydrate a DIMA object via load_local.
|
| 849 |
+
|
| 850 |
+
Returns
|
| 851 |
+
-------
|
| 852 |
+
DIMA
|
| 853 |
+
A ready-to-use (inference) DIMA instance.
|
| 854 |
+
"""
|
| 855 |
+
if not _HAS_HF:
|
| 856 |
+
raise RuntimeError("huggingface_hub not installed. Install it (or `pip install dima[hf]`).")
|
| 857 |
+
|
| 858 |
+
if token is None:
|
| 859 |
+
token = HfFolder.get_token()
|
| 860 |
+
|
| 861 |
+
dl_kwargs = dict(repo_id=repo_id, filename=weights_file, repo_type=repo_type)
|
| 862 |
+
if token is not None:
|
| 863 |
+
dl_kwargs["token"] = token
|
| 864 |
+
if revision is not None:
|
| 865 |
+
dl_kwargs["revision"] = revision
|
| 866 |
+
|
| 867 |
+
path = hf_hub_download(**dl_kwargs)
|
| 868 |
+
|
| 869 |
+
return cls.load_local(
|
| 870 |
+
path,
|
| 871 |
+
ddpm_device=ddpm_device,
|
| 872 |
+
ann_backend=ann_backend,
|
| 873 |
+
ann_params=ann_params,
|
| 874 |
+
n_jobs=n_jobs,
|
| 875 |
+
key=key,
|
| 876 |
+
)
|
| 877 |
+
|
| 878 |
+
|
| 879 |
+
# Backward-compatible aliases (the upstream file uses save_hf/load_hf)
|
| 880 |
+
def save_hf(self, repo_id: str, weights_file: str = "dima.msgpack", config_file: str = "config.json") -> None:
|
| 881 |
+
return self.upload_to_huggingface(repo_id, weights_file=weights_file, config_file=config_file)
|
| 882 |
+
|
| 883 |
+
@classmethod
|
| 884 |
+
def load_hf(
|
| 885 |
+
cls,
|
| 886 |
+
repo_id: str,
|
| 887 |
+
*,
|
| 888 |
+
weights_file: str = "dima.msgpack",
|
| 889 |
+
ddpm_device: str = "auto",
|
| 890 |
+
ann_backend: "ANNBackend" = "auto",
|
| 891 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 892 |
+
n_jobs: int = -1,
|
| 893 |
+
key: Optional["jax.Array"] = None,
|
| 894 |
+
) -> "DIMA":
|
| 895 |
+
return cls.download_from_huggingface(
|
| 896 |
+
repo_id,
|
| 897 |
+
weights_file=weights_file,
|
| 898 |
+
ddpm_device=ddpm_device,
|
| 899 |
+
ann_backend=ann_backend,
|
| 900 |
+
ann_params=ann_params,
|
| 901 |
+
n_jobs=n_jobs,
|
| 902 |
+
key=key,
|
| 903 |
+
)
|
| 904 |
+
|
| 905 |
+
__all__ = ["DIMA", "DIMAConfig"]
|
dmap.py
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/dima/dmap.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import json
|
| 4 |
+
from typing import Any, Dict, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import scipy.sparse as sp
|
| 8 |
+
from scipy.sparse.linalg import eigsh, LinearOperator, lobpcg
|
| 9 |
+
from huggingface_hub import HfApi, HfFolder, upload_file, hf_hub_download
|
| 10 |
+
|
| 11 |
+
from .ann import ANNBackend, make_ann
|
| 12 |
+
from .utils import median_eps_from_knn_d2
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def k_ideal(d: int, N: int) -> int:
|
| 16 |
+
"""
|
| 17 |
+
Heuristic for kNN graph size in diffusion maps.
|
| 18 |
+
Stable default: grows slowly with N and linearly with d.
|
| 19 |
+
"""
|
| 20 |
+
d = int(max(1, d))
|
| 21 |
+
N = int(max(2, N))
|
| 22 |
+
k = int(np.ceil(2.0 * d * np.log2(N)))
|
| 23 |
+
return int(min(max(8, k), N - 1))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _sqdist_ab(A: np.ndarray, B: np.ndarray) -> np.ndarray:
|
| 27 |
+
"""
|
| 28 |
+
Squared Euclidean distances between rows:
|
| 29 |
+
A: (a,d), B: (b,d) -> D2: (a,b)
|
| 30 |
+
"""
|
| 31 |
+
A = np.asarray(A, dtype=np.float64)
|
| 32 |
+
B = np.asarray(B, dtype=np.float64)
|
| 33 |
+
A2 = np.sum(A * A, axis=1, keepdims=True)
|
| 34 |
+
B2 = np.sum(B * B, axis=1, keepdims=True).T
|
| 35 |
+
G = A @ B.T
|
| 36 |
+
return np.maximum(A2 + B2 - 2.0 * G, 0.0)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class DMAP:
|
| 40 |
+
"""
|
| 41 |
+
Diffusion Maps encoder with Nyström out-of-sample extension.
|
| 42 |
+
|
| 43 |
+
Notation (arrays named by indices):
|
| 44 |
+
R_iX: reference ambient data
|
| 45 |
+
K_ij: kernel on graph edges (sparse CSR)
|
| 46 |
+
q_i = Σ_j K_ij
|
| 47 |
+
qα_i = (q_i)^α
|
| 48 |
+
Kα_ij = K_ij / (qα_i qα_j)
|
| 49 |
+
d_i = Σ_j Kα_ij
|
| 50 |
+
A_ij = Kα_ij / sqrt(d_i d_j) (symmetric)
|
| 51 |
+
eigsh(A) -> λ_x, u_ix
|
| 52 |
+
ψ_ix = u_ix / sqrt(d_i)
|
| 53 |
+
R_ix = (λ_x)^t ψ_ix
|
| 54 |
+
|
| 55 |
+
Nyström OOS for novel ambient R_aX:
|
| 56 |
+
K_ai = exp(-β * D2_ai / ε)
|
| 57 |
+
q_a = Σ_i K_ai, qα_a = (q_a)^α
|
| 58 |
+
Kα_ai = K_ai / (qα_a qα_i)
|
| 59 |
+
d_a = Σ_i Kα_ai
|
| 60 |
+
P_ai = Kα_ai / d_a
|
| 61 |
+
R_ax = Σ_i P_ai * (R_ix / λ_x)
|
| 62 |
+
|
| 63 |
+
This implementation uses a kNN graph, sparse eigensolver, and
|
| 64 |
+
provides optional dense refinement via streaming LOBPCG.
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def __init__(
|
| 68 |
+
self,
|
| 69 |
+
R_iX: np.ndarray,
|
| 70 |
+
*,
|
| 71 |
+
d: int = 32,
|
| 72 |
+
beta: float = 1.0,
|
| 73 |
+
alpha: float = 0.0,
|
| 74 |
+
t: float = 1.0,
|
| 75 |
+
k: Optional[int] = None,
|
| 76 |
+
eps: Optional[float] = None,
|
| 77 |
+
eps_use_kth: bool = True,
|
| 78 |
+
eps_mul: float = 1.0,
|
| 79 |
+
drop_trivial: bool = True,
|
| 80 |
+
seed: int = 0,
|
| 81 |
+
sym: str = "max",
|
| 82 |
+
dtype: Any = np.float32,
|
| 83 |
+
ann_backend: ANNBackend = "auto",
|
| 84 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 85 |
+
n_jobs: int = -1,
|
| 86 |
+
# dense refinement (O(N^2) compute, streaming memory)
|
| 87 |
+
refine_dense: bool = False,
|
| 88 |
+
stream_block: int = 4096,
|
| 89 |
+
lobpcg_maxiter: int = 3,
|
| 90 |
+
lobpcg_tol: float = 1e-6,
|
| 91 |
+
use_symmetry: bool = True,
|
| 92 |
+
# allow unicode kwargs (β, α, ε, ε_mul, ε_use_kth, ...)
|
| 93 |
+
**kwargs: Any,
|
| 94 |
+
):
|
| 95 |
+
# ---- map unicode kwargs -> ascii ----
|
| 96 |
+
if "β" in kwargs:
|
| 97 |
+
beta = kwargs.pop("β")
|
| 98 |
+
if "α" in kwargs:
|
| 99 |
+
alpha = kwargs.pop("α")
|
| 100 |
+
if "ε" in kwargs:
|
| 101 |
+
eps = kwargs.pop("ε")
|
| 102 |
+
if "ε_use_kth" in kwargs:
|
| 103 |
+
eps_use_kth = kwargs.pop("ε_use_kth")
|
| 104 |
+
if "ε_mul" in kwargs:
|
| 105 |
+
eps_mul = kwargs.pop("ε_mul")
|
| 106 |
+
if "sym" in kwargs:
|
| 107 |
+
sym = kwargs.pop("sym")
|
| 108 |
+
if kwargs:
|
| 109 |
+
raise TypeError(f"Unexpected kwargs: {sorted(kwargs.keys())}")
|
| 110 |
+
|
| 111 |
+
self.d = int(d)
|
| 112 |
+
self.k = int(k_ideal(self.d, int(np.asarray(R_iX).shape[0])) if k is None else int(k))
|
| 113 |
+
|
| 114 |
+
self.beta = float(beta)
|
| 115 |
+
self.alpha = float(alpha)
|
| 116 |
+
self.t = float(t)
|
| 117 |
+
self.drop_trivial = bool(drop_trivial)
|
| 118 |
+
self.seed = int(seed)
|
| 119 |
+
self.sym = str(sym)
|
| 120 |
+
self.dtype = dtype
|
| 121 |
+
|
| 122 |
+
# unicode aliases (so older code + packers can find them)
|
| 123 |
+
self.β = self.beta
|
| 124 |
+
self.α = self.alpha
|
| 125 |
+
|
| 126 |
+
self.refine_dense = bool(refine_dense)
|
| 127 |
+
self.stream_block = int(stream_block)
|
| 128 |
+
self.lobpcg_maxiter = int(lobpcg_maxiter)
|
| 129 |
+
self.lobpcg_tol = float(lobpcg_tol)
|
| 130 |
+
self.use_symmetry = bool(use_symmetry)
|
| 131 |
+
|
| 132 |
+
rng = np.random.default_rng(self.seed)
|
| 133 |
+
|
| 134 |
+
# reference data
|
| 135 |
+
R_iX = np.asarray(R_iX)
|
| 136 |
+
if R_iX.ndim != 2:
|
| 137 |
+
raise ValueError(f"R_iX must be 2D array, got shape {R_iX.shape}")
|
| 138 |
+
self.R_iX = np.ascontiguousarray(R_iX.astype(self.dtype, copy=False))
|
| 139 |
+
|
| 140 |
+
Nref, D = self.R_iX.shape
|
| 141 |
+
self.Nref = int(Nref)
|
| 142 |
+
self.D = int(D)
|
| 143 |
+
|
| 144 |
+
if not (2 <= self.k < Nref):
|
| 145 |
+
raise ValueError(f"Invalid k={self.k} for Nref={Nref}")
|
| 146 |
+
|
| 147 |
+
# ANN index (ambient)
|
| 148 |
+
self.ann, self.ann_backend = make_ann(ann_backend, ann_params=ann_params, n_jobs=n_jobs)
|
| 149 |
+
self.ann.build(self.R_iX)
|
| 150 |
+
|
| 151 |
+
# kNN of reference points
|
| 152 |
+
j_iK, D2_iK = self.ann.search(self.R_iX, self.k) # (Nref,k)
|
| 153 |
+
|
| 154 |
+
# eps selection
|
| 155 |
+
if eps is None:
|
| 156 |
+
self.eps = float(median_eps_from_knn_d2(D2_iK, use_kth=bool(eps_use_kth)) * float(eps_mul))
|
| 157 |
+
else:
|
| 158 |
+
self.eps = float(eps)
|
| 159 |
+
self.ε = self.eps # unicode alias
|
| 160 |
+
|
| 161 |
+
# -------------------------
|
| 162 |
+
# 1) Build sparse graph and warm-start eigensolver
|
| 163 |
+
# -------------------------
|
| 164 |
+
K_iK = np.exp(-self.beta * (D2_iK / self.eps)).astype(np.float64, copy=False)
|
| 165 |
+
|
| 166 |
+
indptr = (np.arange(Nref + 1, dtype=np.int64) * self.k)
|
| 167 |
+
indices = j_iK.reshape(-1).astype(np.int64, copy=False)
|
| 168 |
+
data = K_iK.reshape(-1)
|
| 169 |
+
|
| 170 |
+
K_ij = sp.csr_matrix((data, indices, indptr), shape=(Nref, Nref), dtype=np.float64)
|
| 171 |
+
|
| 172 |
+
# symmetrize
|
| 173 |
+
if self.sym == "max":
|
| 174 |
+
K_ij = K_ij.maximum(K_ij.T)
|
| 175 |
+
elif self.sym == "mean":
|
| 176 |
+
K_ij = (K_ij + K_ij.T) * 0.5
|
| 177 |
+
else:
|
| 178 |
+
raise ValueError(f"Unknown sym={self.sym!r}")
|
| 179 |
+
|
| 180 |
+
# degrees q_i and qalpha_i (warm)
|
| 181 |
+
q_i_warm = np.asarray(K_ij.sum(axis=1)).ravel()
|
| 182 |
+
q_i_warm = np.maximum(q_i_warm, 1e-30)
|
| 183 |
+
qalpha_i_warm = np.maximum(np.power(q_i_warm, self.alpha), 1e-30)
|
| 184 |
+
|
| 185 |
+
Qinv = sp.diags(1.0 / qalpha_i_warm, format="csr")
|
| 186 |
+
Kalpha_ij = Qinv @ K_ij @ Qinv
|
| 187 |
+
|
| 188 |
+
d_i_warm = np.asarray(Kalpha_ij.sum(axis=1)).ravel()
|
| 189 |
+
d_i_warm = np.maximum(d_i_warm, 1e-30)
|
| 190 |
+
|
| 191 |
+
Dinv_sqrt = sp.diags(1.0 / np.sqrt(d_i_warm), format="csr")
|
| 192 |
+
A_ij = Dinv_sqrt @ Kalpha_ij @ Dinv_sqrt
|
| 193 |
+
|
| 194 |
+
nev = self.d + (1 if self.drop_trivial else 0)
|
| 195 |
+
v0 = rng.normal(size=Nref).astype(np.float64)
|
| 196 |
+
lam0, u0 = eigsh(A_ij, k=nev, which="LA", v0=v0)
|
| 197 |
+
|
| 198 |
+
ord0 = np.argsort(lam0)[::-1]
|
| 199 |
+
lam0 = lam0[ord0]
|
| 200 |
+
u0 = u0[:, ord0]
|
| 201 |
+
|
| 202 |
+
# LOBPCG warm-start block (orthonormalize)
|
| 203 |
+
X0, _ = np.linalg.qr(u0.astype(np.float64, copy=False))
|
| 204 |
+
|
| 205 |
+
# -------------------------
|
| 206 |
+
# 2) Optional refinement: streaming dense LOBPCG on dense PSD operator
|
| 207 |
+
# -------------------------
|
| 208 |
+
if self.refine_dense:
|
| 209 |
+
try:
|
| 210 |
+
# Rebuild dense operator using streaming blocks without allocating full N^2 matrix
|
| 211 |
+
X = self.R_iX.astype(np.float64, copy=False)
|
| 212 |
+
X2 = np.sum(X * X, axis=1, keepdims=True)
|
| 213 |
+
|
| 214 |
+
# compute q_i, qalpha_i, d_i for dense kernel operator:
|
| 215 |
+
# q_i = Σ_j K_ij, K_ij = exp(-β*||xi-xj||^2 / eps)
|
| 216 |
+
# Kα_ij = K_ij/(qα_i qα_j)
|
| 217 |
+
# d_i = Σ_j Kα_ij
|
| 218 |
+
|
| 219 |
+
# Step A: q_i
|
| 220 |
+
ones = np.ones((Nref, 1), dtype=np.float64)
|
| 221 |
+
q_i = self._K_matmat_dense(X, X2, ones).ravel()
|
| 222 |
+
q_i = np.maximum(q_i, 1e-30)
|
| 223 |
+
qalpha_i = np.maximum(np.power(q_i, self.alpha), 1e-30)
|
| 224 |
+
|
| 225 |
+
# Step B: d_i
|
| 226 |
+
inv_qalpha = 1.0 / qalpha_i
|
| 227 |
+
V = (ones * inv_qalpha[:, None]) # (N,1) actually N x 1
|
| 228 |
+
tmp = self._K_matmat_dense(X, X2, V).ravel() # Σ_j K_ij * inv_qalpha_j
|
| 229 |
+
d_i = inv_qalpha * tmp # Σ_j K_ij/(qα_i qα_j)
|
| 230 |
+
d_i = np.maximum(d_i, 1e-30)
|
| 231 |
+
|
| 232 |
+
# Build symmetric operator A(v) = D^{-1/2} Q^{-1} K Q^{-1} D^{-1/2} v
|
| 233 |
+
inv_sqrt_d = 1.0 / np.sqrt(d_i)
|
| 234 |
+
inv_qalpha = 1.0 / qalpha_i
|
| 235 |
+
|
| 236 |
+
def matvec(v: np.ndarray) -> np.ndarray:
|
| 237 |
+
v = v.astype(np.float64, copy=False).reshape(-1, 1) # (N,1)
|
| 238 |
+
w = v * inv_sqrt_d[:, None]
|
| 239 |
+
w = w * inv_qalpha[:, None]
|
| 240 |
+
y = self._K_matmat_dense(X, X2, w)
|
| 241 |
+
y = y * inv_qalpha[:, None]
|
| 242 |
+
y = y * inv_sqrt_d[:, None]
|
| 243 |
+
return y.ravel()
|
| 244 |
+
|
| 245 |
+
Aop = LinearOperator((Nref, Nref), matvec=matvec, dtype=np.float64)
|
| 246 |
+
|
| 247 |
+
# LOBPCG refine using warm-start X0
|
| 248 |
+
lam, u = lobpcg(Aop, X0, largest=True, maxiter=self.lobpcg_maxiter, tol=self.lobpcg_tol)
|
| 249 |
+
|
| 250 |
+
ord1 = np.argsort(lam)[::-1]
|
| 251 |
+
lam = lam[ord1]
|
| 252 |
+
u = u[:, ord1]
|
| 253 |
+
|
| 254 |
+
self.q_i = q_i.astype(np.float64, copy=False)
|
| 255 |
+
self.qalpha_i = qalpha_i.astype(np.float64, copy=False)
|
| 256 |
+
self.d_i = d_i.astype(np.float64, copy=False)
|
| 257 |
+
except Exception:
|
| 258 |
+
# fallback to warm start if refinement fails
|
| 259 |
+
lam, u = lam0, u0
|
| 260 |
+
self.q_i = q_i_warm.astype(np.float64, copy=False)
|
| 261 |
+
self.qalpha_i = qalpha_i_warm.astype(np.float64, copy=False)
|
| 262 |
+
self.d_i = d_i_warm.astype(np.float64, copy=False)
|
| 263 |
+
else:
|
| 264 |
+
lam, u = lam0, u0
|
| 265 |
+
self.q_i = q_i_warm.astype(np.float64, copy=False)
|
| 266 |
+
self.qalpha_i = qalpha_i_warm.astype(np.float64, copy=False)
|
| 267 |
+
self.d_i = d_i_warm.astype(np.float64, copy=False)
|
| 268 |
+
|
| 269 |
+
# provide unicode aliases for packers / older code
|
| 270 |
+
self.qα_i = self.qalpha_i
|
| 271 |
+
self.λ_x = lam.astype(np.float64, copy=False)
|
| 272 |
+
|
| 273 |
+
# psi and drop trivial
|
| 274 |
+
psi = u / np.sqrt(self.d_i)[:, None]
|
| 275 |
+
|
| 276 |
+
if self.drop_trivial:
|
| 277 |
+
lam = lam[1:]
|
| 278 |
+
psi = psi[:, 1:]
|
| 279 |
+
u = u[:, 1:]
|
| 280 |
+
|
| 281 |
+
# diffusion coords
|
| 282 |
+
R_ix = psi * (lam ** self.t)[None, :]
|
| 283 |
+
|
| 284 |
+
# store
|
| 285 |
+
self.λ_x = lam.astype(np.float64, copy=False) # (d,)
|
| 286 |
+
self.u_ix = u.astype(np.float64, copy=False) # (Nref,d)
|
| 287 |
+
self.ψ_ix = psi.astype(np.float64, copy=False) # (Nref,d)
|
| 288 |
+
self.R_ix = R_ix.astype(np.float64, copy=False) # (Nref,d)
|
| 289 |
+
self.π_i = (self.d_i / self.d_i.sum()).astype(np.float64, copy=False)
|
| 290 |
+
|
| 291 |
+
# for Nyström: R_ix / λ_x
|
| 292 |
+
self.R_over_λ_ix = (self.R_ix / self.λ_x[None, :]).astype(np.float64, copy=False)
|
| 293 |
+
|
| 294 |
+
# --------- Dense kernel streaming utilities ----------
|
| 295 |
+
|
| 296 |
+
def _rbf_block(self, Xi: np.ndarray, Xj: np.ndarray, X2i: np.ndarray, X2j: np.ndarray) -> np.ndarray:
|
| 297 |
+
# ||xi-xj||^2 = xi^2 + xj^2 - 2 xi·xj
|
| 298 |
+
G = Xi @ Xj.T
|
| 299 |
+
D2 = np.maximum(X2i + X2j.T - 2.0 * G, 0.0)
|
| 300 |
+
return np.exp(-self.beta * (D2 / self.eps))
|
| 301 |
+
|
| 302 |
+
def _K_matmat_dense(self, X: np.ndarray, X2: np.ndarray, V: np.ndarray) -> np.ndarray:
|
| 303 |
+
"""
|
| 304 |
+
Compute (dense) K @ V without forming K explicitly, using streaming blocks.
|
| 305 |
+
X: (N,D), X2: (N,1), V: (N,m) -> out: (N,m)
|
| 306 |
+
"""
|
| 307 |
+
X = np.asarray(X, dtype=np.float64)
|
| 308 |
+
X2 = np.asarray(X2, dtype=np.float64)
|
| 309 |
+
V = np.asarray(V, dtype=np.float64)
|
| 310 |
+
N = X.shape[0]
|
| 311 |
+
bs = self.stream_block
|
| 312 |
+
|
| 313 |
+
out = np.zeros((N, V.shape[1]), dtype=np.float64)
|
| 314 |
+
|
| 315 |
+
if not self.use_symmetry:
|
| 316 |
+
for i0 in range(0, N, bs):
|
| 317 |
+
i1 = min(N, i0 + bs)
|
| 318 |
+
Xi = X[i0:i1]
|
| 319 |
+
X2i = X2[i0:i1]
|
| 320 |
+
acc = np.zeros((i1 - i0, V.shape[1]), dtype=np.float64)
|
| 321 |
+
for j0 in range(0, N, bs):
|
| 322 |
+
j1 = min(N, j0 + bs)
|
| 323 |
+
Xj = X[j0:j1]
|
| 324 |
+
X2j = X2[j0:j1]
|
| 325 |
+
Kij = self._rbf_block(Xi, Xj, X2i, X2j)
|
| 326 |
+
acc += Kij @ V[j0:j1]
|
| 327 |
+
out[i0:i1] = acc
|
| 328 |
+
return out
|
| 329 |
+
|
| 330 |
+
# symmetric tiling
|
| 331 |
+
for i0 in range(0, N, bs):
|
| 332 |
+
i1 = min(N, i0 + bs)
|
| 333 |
+
Xi = X[i0:i1]
|
| 334 |
+
X2i = X2[i0:i1]
|
| 335 |
+
Vi = V[i0:i1]
|
| 336 |
+
|
| 337 |
+
# diagonal tile
|
| 338 |
+
Kii = self._rbf_block(Xi, Xi, X2i, X2i)
|
| 339 |
+
out[i0:i1] += Kii @ Vi
|
| 340 |
+
|
| 341 |
+
for j0 in range(i1, N, bs):
|
| 342 |
+
j1 = min(N, j0 + bs)
|
| 343 |
+
Xj = X[j0:j1]
|
| 344 |
+
X2j = X2[j0:j1]
|
| 345 |
+
Vj = V[j0:j1]
|
| 346 |
+
|
| 347 |
+
Kij = self._rbf_block(Xi, Xj, X2i, X2j)
|
| 348 |
+
out[i0:i1] += Kij @ Vj
|
| 349 |
+
out[j0:j1] += Kij.T @ Vi
|
| 350 |
+
|
| 351 |
+
return out
|
| 352 |
+
|
| 353 |
+
# --------- Nyström embedding ----------
|
| 354 |
+
|
| 355 |
+
def __call__(self, R_aX: Union[np.ndarray, list], *, batch_size: Optional[int] = None) -> np.ndarray:
|
| 356 |
+
R_aX = np.asarray(R_aX)
|
| 357 |
+
single = (R_aX.ndim == 1)
|
| 358 |
+
if single:
|
| 359 |
+
R_aX = R_aX[None, :]
|
| 360 |
+
|
| 361 |
+
R_aX = np.ascontiguousarray(R_aX.astype(self.dtype, copy=False))
|
| 362 |
+
|
| 363 |
+
if batch_size is None:
|
| 364 |
+
R_ax = self._embed(R_aX)
|
| 365 |
+
else:
|
| 366 |
+
bs = int(batch_size)
|
| 367 |
+
out = []
|
| 368 |
+
for s in range(0, R_aX.shape[0], bs):
|
| 369 |
+
out.append(self._embed(R_aX[s:s + bs]))
|
| 370 |
+
R_ax = np.vstack(out)
|
| 371 |
+
|
| 372 |
+
return R_ax[0] if single else R_ax
|
| 373 |
+
|
| 374 |
+
def _embed(self, R_aX: np.ndarray) -> np.ndarray:
|
| 375 |
+
# kNN for novel points
|
| 376 |
+
j_aK, D2_aK = self.ann.search(R_aX, self.k) # (a,k)
|
| 377 |
+
|
| 378 |
+
K_ai = np.exp(-self.beta * (D2_aK.astype(np.float64) / self.eps)) # (a,k)
|
| 379 |
+
|
| 380 |
+
q_a = np.maximum(K_ai.sum(axis=1), 1e-30)
|
| 381 |
+
qalpha_a = np.maximum(np.power(q_a, self.alpha), 1e-30)
|
| 382 |
+
|
| 383 |
+
qalpha_i = np.maximum(self.qalpha_i[j_aK], 1e-30)
|
| 384 |
+
Kalpha_ai = K_ai / (qalpha_a[:, None] * qalpha_i)
|
| 385 |
+
|
| 386 |
+
d_a = np.maximum(Kalpha_ai.sum(axis=1), 1e-30)
|
| 387 |
+
P_ai = Kalpha_ai / d_a[:, None]
|
| 388 |
+
|
| 389 |
+
R_over = self.R_over_λ_ix[j_aK, :] # (a,k,d)
|
| 390 |
+
R_ax = (P_ai[:, :, None] * R_over).sum(axis=1)
|
| 391 |
+
return R_ax
|
| 392 |
+
|
| 393 |
+
# -------------------------
|
| 394 |
+
# Serialization helpers
|
| 395 |
+
# -------------------------
|
| 396 |
+
|
| 397 |
+
@staticmethod
|
| 398 |
+
def _np_dtype_str(x) -> str:
|
| 399 |
+
"""Best-effort numpy dtype string for serialization."""
|
| 400 |
+
try:
|
| 401 |
+
return str(np.dtype(x))
|
| 402 |
+
except Exception:
|
| 403 |
+
return "float32"
|
| 404 |
+
|
| 405 |
+
def state_dict(self) -> Dict[str, Any]:
|
| 406 |
+
"""
|
| 407 |
+
Minimal, inference-sufficient state for Nyström out-of-sample embedding.
|
| 408 |
+
|
| 409 |
+
The resulting dict is intentionally compatible with `FrozenDMAP` in `dima.py`
|
| 410 |
+
(keys: R_iX, qalpha_i, R_over_lam_ix, k, beta, alpha, eps, dtype).
|
| 411 |
+
"""
|
| 412 |
+
R_over = getattr(self, "R_over_λ_ix", None)
|
| 413 |
+
if R_over is None:
|
| 414 |
+
# Backward/alternate name safety
|
| 415 |
+
R_over = getattr(self, "R_over_lam_ix", None)
|
| 416 |
+
|
| 417 |
+
if R_over is None:
|
| 418 |
+
raise AttributeError("DMAP object has no Nyström matrix `R_over_λ_ix` (did training finish?).")
|
| 419 |
+
|
| 420 |
+
state: Dict[str, Any] = dict(
|
| 421 |
+
k=int(self.k),
|
| 422 |
+
beta=float(self.beta),
|
| 423 |
+
alpha=float(self.alpha),
|
| 424 |
+
eps=float(self.eps),
|
| 425 |
+
dtype=self._np_dtype_str(getattr(self, "dtype", "float32")),
|
| 426 |
+
# arrays
|
| 427 |
+
R_iX=np.asarray(self.R_iX),
|
| 428 |
+
qalpha_i=np.asarray(self.qalpha_i, dtype=np.float64),
|
| 429 |
+
R_over_lam_ix=np.asarray(R_over, dtype=np.float64),
|
| 430 |
+
# small meta (optional)
|
| 431 |
+
meta=dict(
|
| 432 |
+
Nref=int(np.asarray(self.R_iX).shape[0]),
|
| 433 |
+
D=int(np.asarray(self.R_iX).shape[1]),
|
| 434 |
+
d=int(np.asarray(R_over).shape[1]),
|
| 435 |
+
t=float(getattr(self, "t", 1.0)),
|
| 436 |
+
drop_trivial=bool(getattr(self, "drop_trivial", False)),
|
| 437 |
+
sym=str(getattr(self, "sym", "max")),
|
| 438 |
+
),
|
| 439 |
+
)
|
| 440 |
+
return state
|
| 441 |
+
|
| 442 |
+
def save_local(self, weights_file: str = "dmap.msgpack", config_file: Optional[str] = "dmap_config.json") -> None:
|
| 443 |
+
"""
|
| 444 |
+
Save DMAP weights (and optionally a lightweight JSON config) locally.
|
| 445 |
+
|
| 446 |
+
- `weights_file`: binary msgpack with arrays (via flax.serialization)
|
| 447 |
+
- `config_file`: JSON with scalar metadata only (no large arrays)
|
| 448 |
+
"""
|
| 449 |
+
# local import keeps `dima` usable without flax unless you call this
|
| 450 |
+
from flax import serialization as flax_ser # type: ignore
|
| 451 |
+
import json as _json
|
| 452 |
+
|
| 453 |
+
state = self.state_dict()
|
| 454 |
+
blob = flax_ser.msgpack_serialize(state)
|
| 455 |
+
|
| 456 |
+
with open(weights_file, "wb") as f:
|
| 457 |
+
f.write(blob)
|
| 458 |
+
|
| 459 |
+
if config_file is not None:
|
| 460 |
+
cfg = dict(
|
| 461 |
+
k=int(state["k"]),
|
| 462 |
+
beta=float(state["beta"]),
|
| 463 |
+
alpha=float(state["alpha"]),
|
| 464 |
+
eps=float(state["eps"]),
|
| 465 |
+
dtype=str(state.get("dtype", "float32")),
|
| 466 |
+
**(state.get("meta", {}) or {}),
|
| 467 |
+
)
|
| 468 |
+
with open(config_file, "w") as f:
|
| 469 |
+
_json.dump(cfg, f, indent=2)
|
| 470 |
+
|
| 471 |
+
return None
|
| 472 |
+
|
| 473 |
+
@classmethod
|
| 474 |
+
def from_state(
|
| 475 |
+
cls,
|
| 476 |
+
state: Dict[str, Any],
|
| 477 |
+
*,
|
| 478 |
+
ann_backend: ANNBackend = "auto",
|
| 479 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 480 |
+
n_jobs: int = -1,
|
| 481 |
+
) -> "DMAP":
|
| 482 |
+
"""
|
| 483 |
+
Rehydrate a DMAP object from `state_dict()` output WITHOUT retraining.
|
| 484 |
+
|
| 485 |
+
This bypasses `__init__` and rebuilds only what is needed for out-of-sample Nyström embedding:
|
| 486 |
+
reference points, normalization factors, Nyström matrix, and ANN index.
|
| 487 |
+
"""
|
| 488 |
+
# allow passing a full DIMA state dict
|
| 489 |
+
if "encoder" in state and isinstance(state["encoder"], dict):
|
| 490 |
+
state = state["encoder"] # type: ignore[assignment]
|
| 491 |
+
|
| 492 |
+
# tolerate unicode key variants
|
| 493 |
+
k = int(state["k"])
|
| 494 |
+
beta = float(state.get("beta", state.get("β")))
|
| 495 |
+
alpha = float(state.get("alpha", state.get("α")))
|
| 496 |
+
eps = float(state.get("eps", state.get("ε")))
|
| 497 |
+
|
| 498 |
+
dtype = np.dtype(state.get("dtype", "float32"))
|
| 499 |
+
R_iX = np.ascontiguousarray(np.asarray(state["R_iX"]).astype(dtype, copy=False))
|
| 500 |
+
qalpha_i = np.asarray(state.get("qalpha_i", state.get("qα_i")), dtype=np.float64)
|
| 501 |
+
|
| 502 |
+
R_over = state.get("R_over_lam_ix", state.get("R_over_λ_ix"))
|
| 503 |
+
if R_over is None:
|
| 504 |
+
raise KeyError("State must contain 'R_over_lam_ix' (or 'R_over_λ_ix').")
|
| 505 |
+
R_over = np.asarray(R_over, dtype=np.float64)
|
| 506 |
+
|
| 507 |
+
obj = cls.__new__(cls) # bypass __init__
|
| 508 |
+
|
| 509 |
+
# required for embedding
|
| 510 |
+
obj.k = k
|
| 511 |
+
obj.beta = beta
|
| 512 |
+
obj.alpha = alpha
|
| 513 |
+
obj.eps = eps
|
| 514 |
+
obj.dtype = dtype
|
| 515 |
+
obj.R_iX = R_iX
|
| 516 |
+
obj.qalpha_i = qalpha_i
|
| 517 |
+
obj.qα_i = obj.qalpha_i # alias
|
| 518 |
+
obj.R_over_λ_ix = R_over
|
| 519 |
+
obj.R_over_lam_ix = obj.R_over_λ_ix # alias for external code
|
| 520 |
+
obj.β = obj.beta
|
| 521 |
+
obj.α = obj.alpha
|
| 522 |
+
obj.ε = obj.eps
|
| 523 |
+
|
| 524 |
+
# optional metadata for introspection
|
| 525 |
+
meta = state.get("meta", {}) if isinstance(state.get("meta", {}), dict) else {}
|
| 526 |
+
obj.d = int(meta.get("d", R_over.shape[1]))
|
| 527 |
+
obj.t = float(meta.get("t", 1.0))
|
| 528 |
+
obj.drop_trivial = bool(meta.get("drop_trivial", False))
|
| 529 |
+
obj.sym = str(meta.get("sym", "max"))
|
| 530 |
+
|
| 531 |
+
# rebuild ANN index
|
| 532 |
+
obj.ann, obj.ann_backend = make_ann(ann_backend, ann_params=ann_params, n_jobs=n_jobs)
|
| 533 |
+
obj.ann.build(obj.R_iX)
|
| 534 |
+
|
| 535 |
+
return obj
|
| 536 |
+
|
| 537 |
+
@classmethod
|
| 538 |
+
def load_local(
|
| 539 |
+
cls,
|
| 540 |
+
weights_file: str = "dmap.msgpack",
|
| 541 |
+
*,
|
| 542 |
+
ann_backend: ANNBackend = "auto",
|
| 543 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 544 |
+
n_jobs: int = -1,
|
| 545 |
+
) -> "DMAP":
|
| 546 |
+
"""Load DMAP weights saved by `save_local()`."""
|
| 547 |
+
from flax import serialization as flax_ser # type: ignore
|
| 548 |
+
|
| 549 |
+
with open(weights_file, "rb") as f:
|
| 550 |
+
state = flax_ser.msgpack_restore(f.read())
|
| 551 |
+
|
| 552 |
+
return cls.from_state(state, ann_backend=ann_backend, ann_params=ann_params, n_jobs=n_jobs)
|
| 553 |
+
|
| 554 |
+
def upload_to_huggingface(
|
| 555 |
+
self,
|
| 556 |
+
repo_id: str,
|
| 557 |
+
*,
|
| 558 |
+
weights_file: str = "dmap.msgpack",
|
| 559 |
+
config_file: str = "dmap_config.json",
|
| 560 |
+
token: Optional[str] = None,
|
| 561 |
+
repo_type: str = "model",
|
| 562 |
+
revision: Optional[str] = None,
|
| 563 |
+
) -> None:
|
| 564 |
+
"""
|
| 565 |
+
Upload DMAP weights to the Hugging Face Hub.
|
| 566 |
+
|
| 567 |
+
This follows the same pattern as `DIMA.save_hf`:
|
| 568 |
+
- saves locally first (msgpack + JSON)
|
| 569 |
+
- creates the repo if needed
|
| 570 |
+
- uploads both files
|
| 571 |
+
"""
|
| 572 |
+
try:
|
| 573 |
+
from huggingface_hub import HfApi, HfFolder, upload_file # type: ignore
|
| 574 |
+
except Exception as e:
|
| 575 |
+
raise RuntimeError("huggingface_hub not installed. Install extras: `pip install dima[hf]`.") from e
|
| 576 |
+
|
| 577 |
+
self.save_local(weights_file=weights_file, config_file=config_file)
|
| 578 |
+
|
| 579 |
+
if token is None:
|
| 580 |
+
token = HfFolder.get_token()
|
| 581 |
+
if token is None:
|
| 582 |
+
raise RuntimeError("No HF token found. Run `huggingface-cli login`, or pass `token=...`.")
|
| 583 |
+
|
| 584 |
+
import os as _os
|
| 585 |
+
|
| 586 |
+
api = HfApi()
|
| 587 |
+
api.create_repo(repo_id=repo_id, repo_type=repo_type, exist_ok=True, token=token)
|
| 588 |
+
|
| 589 |
+
wf = _os.path.basename(weights_file)
|
| 590 |
+
cf = _os.path.basename(config_file)
|
| 591 |
+
|
| 592 |
+
upload_file(
|
| 593 |
+
path_or_fileobj=weights_file,
|
| 594 |
+
path_in_repo=wf,
|
| 595 |
+
repo_id=repo_id,
|
| 596 |
+
token=token,
|
| 597 |
+
repo_type=repo_type,
|
| 598 |
+
revision=revision,
|
| 599 |
+
)
|
| 600 |
+
upload_file(
|
| 601 |
+
path_or_fileobj=config_file,
|
| 602 |
+
path_in_repo=cf,
|
| 603 |
+
repo_id=repo_id,
|
| 604 |
+
token=token,
|
| 605 |
+
repo_type=repo_type,
|
| 606 |
+
revision=revision,
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
return None
|
| 610 |
+
|
| 611 |
+
@classmethod
|
| 612 |
+
def download_from_huggingface(
|
| 613 |
+
cls,
|
| 614 |
+
repo_id: str,
|
| 615 |
+
*,
|
| 616 |
+
weights_file: str = "dmap.msgpack",
|
| 617 |
+
ann_backend: ANNBackend = "auto",
|
| 618 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 619 |
+
n_jobs: int = -1,
|
| 620 |
+
token: Optional[str] = None,
|
| 621 |
+
repo_type: str = "model",
|
| 622 |
+
revision: Optional[str] = None,
|
| 623 |
+
) -> "DMAP":
|
| 624 |
+
"""
|
| 625 |
+
Download DMAP weights from Hugging Face Hub and rehydrate a DMAP instance.
|
| 626 |
+
|
| 627 |
+
Note: despite the name, this *downloads from* the Hub.
|
| 628 |
+
"""
|
| 629 |
+
try:
|
| 630 |
+
from huggingface_hub import hf_hub_download # type: ignore
|
| 631 |
+
except Exception as e:
|
| 632 |
+
raise RuntimeError("huggingface_hub not installed. Install extras: `pip install dima[hf]`.") from e
|
| 633 |
+
|
| 634 |
+
path = hf_hub_download(
|
| 635 |
+
repo_id=repo_id,
|
| 636 |
+
filename=weights_file,
|
| 637 |
+
token=token,
|
| 638 |
+
repo_type=repo_type,
|
| 639 |
+
revision=revision,
|
| 640 |
+
)
|
| 641 |
+
return cls.load_local(path, ann_backend=ann_backend, ann_params=ann_params, n_jobs=n_jobs)
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
__all__ = ["DMAP", "k_ideal"]
|
gplm.py
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/dima/gplm.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import Any, Dict, Literal, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import scipy.linalg as la
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import tempfile
|
| 11 |
+
|
| 12 |
+
from .ann import ANNBackend, make_ann
|
| 13 |
+
from .utils import fps_indices, median_eps_from_knn_d2, sqdist_ab
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
InducingMode = Literal["random_subset", "fps", "kmeans_medoids", "given"]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _kmeans2_safe(Z: np.ndarray, m: int, seed: int = 0) -> np.ndarray:
|
| 22 |
+
"""
|
| 23 |
+
KMeans centers with a safe fallback.
|
| 24 |
+
Uses scipy.cluster.vq.kmeans2 if available; otherwise samples points.
|
| 25 |
+
"""
|
| 26 |
+
Z = np.asarray(Z)
|
| 27 |
+
m = int(min(max(1, m), Z.shape[0]))
|
| 28 |
+
try:
|
| 29 |
+
from scipy.cluster.vq import kmeans2 # type: ignore
|
| 30 |
+
|
| 31 |
+
C, _ = kmeans2(Z.astype(np.float64, copy=False), m, minit="points", seed=seed)
|
| 32 |
+
return C.astype(Z.dtype, copy=False)
|
| 33 |
+
except Exception:
|
| 34 |
+
rng = np.random.default_rng(seed)
|
| 35 |
+
idx = rng.choice(Z.shape[0], size=m, replace=False)
|
| 36 |
+
return Z[idx]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _to_builtin(x: Any) -> Any:
|
| 40 |
+
"""
|
| 41 |
+
Convierte tipos numpy / tuplas / dicts anidados a tipos nativos serializables.
|
| 42 |
+
- Para msgpack (flax.serialization) las ndarrays se dejan como ndarrays.
|
| 43 |
+
- Para JSON, este helper se usa solo sobre objetos escalares/dicts (sin ndarrays).
|
| 44 |
+
"""
|
| 45 |
+
if x is None:
|
| 46 |
+
return None
|
| 47 |
+
if isinstance(x, (bool, int, float, str)):
|
| 48 |
+
return x
|
| 49 |
+
if isinstance(x, (np.integer, np.floating)):
|
| 50 |
+
return x.item()
|
| 51 |
+
if isinstance(x, (tuple, list)):
|
| 52 |
+
return [_to_builtin(v) for v in x]
|
| 53 |
+
if isinstance(x, dict):
|
| 54 |
+
return {str(k): _to_builtin(v) for k, v in x.items()}
|
| 55 |
+
# fallback conservador
|
| 56 |
+
return str(x)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class GPLM:
|
| 60 |
+
"""
|
| 61 |
+
Inducing-point / Nyström GP (kernel ridge) decoder on latents.
|
| 62 |
+
|
| 63 |
+
Entrenamiento:
|
| 64 |
+
- Construye puntos inductores Z_mx_w (en latente blanqueado o no),
|
| 65 |
+
- Estima eps (si no se provee) a partir de distancias kNN,
|
| 66 |
+
- Resuelve M_mX por Cholesky (Nyström KRR/GP mean).
|
| 67 |
+
|
| 68 |
+
Además:
|
| 69 |
+
- decode() vía __call__.
|
| 70 |
+
- flow() integra un paso tipo generalized-leapfrog (geodesic flow) sobre el pullback manifold.
|
| 71 |
+
- NUEVO: save_local/load_local + upload_to_huggingface/download_from_huggingface.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
def __init__(
|
| 75 |
+
self,
|
| 76 |
+
R_ix: np.ndarray,
|
| 77 |
+
R_iX: np.ndarray,
|
| 78 |
+
*,
|
| 79 |
+
# ASCII
|
| 80 |
+
beta: float = 1.0,
|
| 81 |
+
# eps estimation
|
| 82 |
+
eps: Optional[float] = None,
|
| 83 |
+
k_eps: int = 256,
|
| 84 |
+
eps_use_kth: bool = True,
|
| 85 |
+
eps_mul: float = 1.0,
|
| 86 |
+
# regularization
|
| 87 |
+
sigma2: float = 1e-5,
|
| 88 |
+
jitter: float = 1e-8,
|
| 89 |
+
# inducing
|
| 90 |
+
m: int = 1024,
|
| 91 |
+
inducing: InducingMode = "kmeans_medoids",
|
| 92 |
+
Z_mx: Optional[np.ndarray] = None,
|
| 93 |
+
seed: int = 0,
|
| 94 |
+
# preprocess
|
| 95 |
+
center_X: bool = True,
|
| 96 |
+
whiten_latent: bool = False,
|
| 97 |
+
dtype: Any = np.float32,
|
| 98 |
+
# compute/memory
|
| 99 |
+
fit_block: int = 8192,
|
| 100 |
+
# inference
|
| 101 |
+
pred_k: Optional[int] = None,
|
| 102 |
+
ann_backend: ANNBackend = "auto",
|
| 103 |
+
ann_params: Optional[Dict[str, Any]] = None,
|
| 104 |
+
n_jobs: int = -1,
|
| 105 |
+
# unicode aliases
|
| 106 |
+
**kwargs: Any,
|
| 107 |
+
):
|
| 108 |
+
# ---- map unicode kwargs -> ascii
|
| 109 |
+
if "β" in kwargs:
|
| 110 |
+
beta = kwargs.pop("β")
|
| 111 |
+
if "ε" in kwargs:
|
| 112 |
+
eps = kwargs.pop("ε")
|
| 113 |
+
if "κ_eps" in kwargs:
|
| 114 |
+
k_eps = kwargs.pop("κ_eps")
|
| 115 |
+
if "ε_use_kth" in kwargs:
|
| 116 |
+
eps_use_kth = kwargs.pop("ε_use_kth")
|
| 117 |
+
if "ε_mul" in kwargs:
|
| 118 |
+
eps_mul = kwargs.pop("ε_mul")
|
| 119 |
+
if "σ2" in kwargs:
|
| 120 |
+
sigma2 = kwargs.pop("σ2")
|
| 121 |
+
if "pred_κ" in kwargs:
|
| 122 |
+
pred_k = kwargs.pop("pred_κ")
|
| 123 |
+
if kwargs:
|
| 124 |
+
raise TypeError(f"Unexpected kwargs: {sorted(kwargs.keys())}")
|
| 125 |
+
|
| 126 |
+
# ---- store params (and keep enough meta to reconstruct on load)
|
| 127 |
+
self.beta = float(beta)
|
| 128 |
+
self.β = self.beta
|
| 129 |
+
|
| 130 |
+
self.sigma2 = float(sigma2)
|
| 131 |
+
self.σ2 = self.sigma2
|
| 132 |
+
|
| 133 |
+
self.jitter = float(jitter)
|
| 134 |
+
self.seed = int(seed)
|
| 135 |
+
|
| 136 |
+
self.dtype = dtype
|
| 137 |
+
self.dtype_str = str(np.dtype(dtype))
|
| 138 |
+
|
| 139 |
+
self.fit_block = int(fit_block)
|
| 140 |
+
self.center_X = bool(center_X)
|
| 141 |
+
self.whiten_latent = bool(whiten_latent)
|
| 142 |
+
self.inducing = inducing
|
| 143 |
+
|
| 144 |
+
self.ann_backend = ann_backend
|
| 145 |
+
self.ann_params = ann_params if ann_params is None else dict(ann_params)
|
| 146 |
+
self.n_jobs = int(n_jobs)
|
| 147 |
+
|
| 148 |
+
# ---- validate / cast
|
| 149 |
+
R_ix = np.ascontiguousarray(np.asarray(R_ix).astype(self.dtype, copy=False))
|
| 150 |
+
R_iX = np.ascontiguousarray(np.asarray(R_iX).astype(self.dtype, copy=False))
|
| 151 |
+
if R_ix.ndim != 2 or R_iX.ndim != 2 or R_ix.shape[0] != R_iX.shape[0]:
|
| 152 |
+
raise ValueError("R_ix must be (N,d) and R_iX must be (N,D) with same N.")
|
| 153 |
+
|
| 154 |
+
self.R_ix = R_ix
|
| 155 |
+
self.R_iX = R_iX
|
| 156 |
+
self.N, self.d_lat = R_ix.shape
|
| 157 |
+
_, self.D = R_iX.shape
|
| 158 |
+
|
| 159 |
+
# ---- center output
|
| 160 |
+
if self.center_X:
|
| 161 |
+
self.mean_X = R_iX.mean(axis=0).astype(np.float64)
|
| 162 |
+
Y = (R_iX.astype(np.float64) - self.mean_X[None, :])
|
| 163 |
+
else:
|
| 164 |
+
self.mean_X = np.zeros((self.D,), dtype=np.float64)
|
| 165 |
+
Y = R_iX.astype(np.float64)
|
| 166 |
+
|
| 167 |
+
# ---- latent whitening (optional)
|
| 168 |
+
Ztrain = R_ix.astype(np.float64)
|
| 169 |
+
if self.whiten_latent:
|
| 170 |
+
self.lat_mean_x = Ztrain.mean(axis=0)
|
| 171 |
+
self.lat_std_x = np.maximum(Ztrain.std(axis=0), 1e-12)
|
| 172 |
+
Ztrain_w = (Ztrain - self.lat_mean_x) / self.lat_std_x
|
| 173 |
+
else:
|
| 174 |
+
self.lat_mean_x = np.zeros((self.d_lat,), dtype=np.float64)
|
| 175 |
+
self.lat_std_x = np.ones((self.d_lat,), dtype=np.float64)
|
| 176 |
+
Ztrain_w = Ztrain
|
| 177 |
+
self.R_ix_w = Ztrain_w # (N,d) float64
|
| 178 |
+
|
| 179 |
+
# ---- ANN on training latents (eps + medoids snapping)
|
| 180 |
+
self.ann_train, _ = make_ann(self.ann_backend, ann_params=self.ann_params, n_jobs=self.n_jobs)
|
| 181 |
+
self.ann_train.build(self.R_ix_w.astype(self.dtype, copy=False))
|
| 182 |
+
|
| 183 |
+
# ---- eps via kNN distances on latents
|
| 184 |
+
if eps is None:
|
| 185 |
+
k_eps = int(min(max(8, int(k_eps)), self.N - 1))
|
| 186 |
+
j_iK1, D2_iK1 = self.ann_train.search(self.R_ix_w.astype(self.dtype, copy=False), k_eps + 1)
|
| 187 |
+
|
| 188 |
+
i = np.arange(self.N)[:, None]
|
| 189 |
+
is_self = (j_iK1 == i)
|
| 190 |
+
|
| 191 |
+
if np.any(is_self):
|
| 192 |
+
D2_iK = np.empty((self.N, k_eps), dtype=np.float64)
|
| 193 |
+
for ii in range(self.N):
|
| 194 |
+
keep = (j_iK1[ii] != ii)
|
| 195 |
+
D2_iK[ii] = D2_iK1[ii][keep][:k_eps]
|
| 196 |
+
else:
|
| 197 |
+
D2_iK = D2_iK1[:, :k_eps].astype(np.float64, copy=False)
|
| 198 |
+
|
| 199 |
+
eps_hat = median_eps_from_knn_d2(D2_iK, use_kth=bool(eps_use_kth))
|
| 200 |
+
else:
|
| 201 |
+
eps_hat = float(eps)
|
| 202 |
+
|
| 203 |
+
eps_hat *= float(eps_mul)
|
| 204 |
+
if eps_hat <= 0:
|
| 205 |
+
raise ValueError("eps must be > 0.")
|
| 206 |
+
self.eps = float(eps_hat)
|
| 207 |
+
self.ε = self.eps
|
| 208 |
+
|
| 209 |
+
# ---- choose inducing points (in whitened latent space)
|
| 210 |
+
rng = np.random.default_rng(self.seed)
|
| 211 |
+
m_eff = int(min(max(1, int(m)), self.N))
|
| 212 |
+
|
| 213 |
+
if Z_mx is not None:
|
| 214 |
+
Zm = np.asarray(Z_mx, dtype=np.float64)
|
| 215 |
+
if Zm.ndim != 2 or Zm.shape[1] != self.d_lat:
|
| 216 |
+
raise ValueError("Z_mx must be (m, d_lat).")
|
| 217 |
+
Zm_w = (Zm - self.lat_mean_x) / self.lat_std_x
|
| 218 |
+
else:
|
| 219 |
+
if inducing == "random_subset":
|
| 220 |
+
idx = rng.choice(self.N, size=m_eff, replace=False)
|
| 221 |
+
Zm_w = self.R_ix_w[idx]
|
| 222 |
+
elif inducing == "fps":
|
| 223 |
+
idx = fps_indices(self.R_ix_w, m=m_eff, seed=self.seed) # fps_indices is not defined
|
| 224 |
+
# For now, fallback to random_subset if fps_indices is not available
|
| 225 |
+
# idx = rng.choice(self.N, size=m_eff, replace=False)
|
| 226 |
+
Zm_w = self.R_ix_w[idx]
|
| 227 |
+
elif inducing == "kmeans_medoids":
|
| 228 |
+
C = _kmeans2_safe(self.R_ix_w, m_eff, seed=self.seed).astype(np.float64, copy=False)
|
| 229 |
+
j_cm, _ = self.ann_train.search(C.astype(self.dtype, copy=False), 1)
|
| 230 |
+
idx = j_cm.reshape(-1).astype(np.int64)
|
| 231 |
+
|
| 232 |
+
# de-duplicate and refill if needed
|
| 233 |
+
idx_u = np.unique(idx)
|
| 234 |
+
if idx_u.size < m_eff:
|
| 235 |
+
needed = m_eff - idx_u.size
|
| 236 |
+
pool = np.setdiff1d(np.arange(self.N), idx_u, assume_unique=False)
|
| 237 |
+
extra = rng.choice(pool, size=needed, replace=False) if pool.size >= needed else rng.choice(self.N, size=needed, replace=True)
|
| 238 |
+
idx = np.concatenate([idx_u, extra])
|
| 239 |
+
else:
|
| 240 |
+
idx = idx_u[:m_eff]
|
| 241 |
+
Zm_w = self.R_ix_w[idx]
|
| 242 |
+
elif inducing == "given":
|
| 243 |
+
raise ValueError("Provide Z_mx when inducing='given'.")
|
| 244 |
+
else:
|
| 245 |
+
raise ValueError(f"Unknown inducing mode: {inducing!r}")
|
| 246 |
+
|
| 247 |
+
self.Z_mx_w = np.ascontiguousarray(Zm_w.astype(np.float64, copy=False))
|
| 248 |
+
self.m = int(self.Z_mx_w.shape[0])
|
| 249 |
+
|
| 250 |
+
# store raw inducing points (unwhitened) for convenience
|
| 251 |
+
self.Z_mx = (self.Z_mx_w * self.lat_std_x[None, :]) + self.lat_mean_x[None, :]
|
| 252 |
+
|
| 253 |
+
# ---- ANN on inducing points for fast prediction
|
| 254 |
+
self.ann_Z, _ = make_ann(self.ann_backend, ann_params=self.ann_params, n_jobs=self.n_jobs)
|
| 255 |
+
self.ann_Z.build(self.Z_mx_w.astype(self.dtype, copy=False))
|
| 256 |
+
|
| 257 |
+
# pred_k
|
| 258 |
+
if pred_k is None:
|
| 259 |
+
self.pred_k = None
|
| 260 |
+
else:
|
| 261 |
+
self.pred_k = int(min(max(1, int(pred_k)), self.m))
|
| 262 |
+
self.pred_κ = self.pred_k # unicode alias
|
| 263 |
+
|
| 264 |
+
# ---- W_mm and reduced solve
|
| 265 |
+
D2_mm = sqdist_ab(self.Z_mx_w, self.Z_mx_w)
|
| 266 |
+
W_mm = np.exp(-self.beta * (D2_mm.astype(np.float64) / self.eps))
|
| 267 |
+
W_mm.flat[:: self.m + 1] += self.jitter
|
| 268 |
+
self.W_mm = W_mm # (m,m)
|
| 269 |
+
|
| 270 |
+
G_mm = np.zeros((self.m, self.m), dtype=np.float64)
|
| 271 |
+
B_mX = np.zeros((self.m, self.D), dtype=np.float64)
|
| 272 |
+
bs = int(self.fit_block)
|
| 273 |
+
|
| 274 |
+
for i0 in range(0, self.N, bs):
|
| 275 |
+
i1 = min(self.N, i0 + bs)
|
| 276 |
+
Zi = self.R_ix_w[i0:i1] # (b,d)
|
| 277 |
+
D2_im = sqdist_ab(Zi, self.Z_mx_w)
|
| 278 |
+
C_im = np.exp(-self.beta * (D2_im.astype(np.float64) / self.eps))
|
| 279 |
+
G_mm += C_im.T @ C_im
|
| 280 |
+
B_mX += C_im.T @ Y[i0:i1]
|
| 281 |
+
|
| 282 |
+
A_mm = G_mm + self.sigma2 * W_mm
|
| 283 |
+
A_mm.flat[:: self.m + 1] += self.jitter
|
| 284 |
+
|
| 285 |
+
cF = la.cho_factor(A_mm, lower=True, check_finite=False)
|
| 286 |
+
self.M_mX = la.cho_solve(cF, B_mX, check_finite=False) # (m,D)
|
| 287 |
+
|
| 288 |
+
# ---------------------------
|
| 289 |
+
# Inference
|
| 290 |
+
# ---------------------------
|
| 291 |
+
def __call__(self, R_ax: Union[np.ndarray, list], *, batch_size: Optional[int] = None) -> np.ndarray:
|
| 292 |
+
R_ax = np.asarray(R_ax)
|
| 293 |
+
single = (R_ax.ndim == 1)
|
| 294 |
+
if single:
|
| 295 |
+
R_ax = R_ax[None, :]
|
| 296 |
+
R_ax = np.ascontiguousarray(R_ax.astype(self.dtype, copy=False))
|
| 297 |
+
|
| 298 |
+
if batch_size is None:
|
| 299 |
+
Y = self._decode(R_ax)
|
| 300 |
+
else:
|
| 301 |
+
bs = int(batch_size)
|
| 302 |
+
out = []
|
| 303 |
+
for s in range(0, R_ax.shape[0], bs):
|
| 304 |
+
out.append(self._decode(R_ax[s : s + bs]))
|
| 305 |
+
Y = np.vstack(out)
|
| 306 |
+
|
| 307 |
+
return Y[0] if single else Y
|
| 308 |
+
|
| 309 |
+
def _decode(self, R_ax: np.ndarray) -> np.ndarray:
|
| 310 |
+
Za = R_ax.astype(np.float64, copy=False)
|
| 311 |
+
Za_w = (Za - self.lat_mean_x) / self.lat_std_x
|
| 312 |
+
|
| 313 |
+
if self.pred_k is None or self.pred_k == self.m:
|
| 314 |
+
D2_am = sqdist_ab(Za_w, self.Z_mx_w)
|
| 315 |
+
C_am = np.exp(-self.beta * (D2_am.astype(np.float64) / self.eps))
|
| 316 |
+
Y = C_am @ self.M_mX
|
| 317 |
+
else:
|
| 318 |
+
j_aK, D2_aK = self.ann_Z.search(Za_w.astype(self.dtype, copy=False), self.pred_k)
|
| 319 |
+
W = np.exp(-self.beta * (D2_aK.astype(np.float64) / self.eps)) # (a,k)
|
| 320 |
+
M = self.M_mX[j_aK] # (a,k,D)
|
| 321 |
+
Y = np.sum(W[:, :, None] * M, axis=1) # (a,D)
|
| 322 |
+
|
| 323 |
+
return Y + self.mean_X[None, :]
|
| 324 |
+
|
| 325 |
+
# ============================================================
|
| 326 |
+
# Geodesic flow on pullback manifold (no C_mm storage)
|
| 327 |
+
# ============================================================
|
| 328 |
+
def _rbf_cache_single(self, r_x: np.ndarray, *, idx_m: Optional[np.ndarray]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 329 |
+
"""
|
| 330 |
+
Cache kernel terms at position r (single point).
|
| 331 |
+
"""
|
| 332 |
+
c = self.beta / self.eps
|
| 333 |
+
inv_std = 1.0 / self.lat_std_x
|
| 334 |
+
|
| 335 |
+
r = r_x.astype(np.float64, copy=False)
|
| 336 |
+
rw = (r - self.lat_mean_x) / self.lat_std_x
|
| 337 |
+
|
| 338 |
+
Zw = self.Z_mx_w if idx_m is None else self.Z_mx_w[idx_m]
|
| 339 |
+
Dw = rw[None, :] - Zw
|
| 340 |
+
D2 = np.sum(Dw * Dw, axis=1)
|
| 341 |
+
|
| 342 |
+
k_m = np.exp(-c * D2) # (k,)
|
| 343 |
+
Dw_over = Dw * inv_std[None, :] # (k,d) = (r-z)/std^2
|
| 344 |
+
grad_k = -(2.0 * c) * (k_m[:, None] * Dw_over) # (k,d)
|
| 345 |
+
|
| 346 |
+
return k_m, Dw_over, grad_k
|
| 347 |
+
|
| 348 |
+
def _metric_from_gradM(
|
| 349 |
+
self,
|
| 350 |
+
grad_k: np.ndarray, # (k,d)
|
| 351 |
+
M_kX: np.ndarray, # (k,D)
|
| 352 |
+
*,
|
| 353 |
+
D_block: int = 8192,
|
| 354 |
+
lam: float = 1e-10,
|
| 355 |
+
) -> Tuple[np.ndarray, Tuple[np.ndarray, bool]]:
|
| 356 |
+
"""
|
| 357 |
+
Compute pullback metric g = J^T J without forming C_mm.
|
| 358 |
+
"""
|
| 359 |
+
k, d = grad_k.shape
|
| 360 |
+
D = M_kX.shape[1]
|
| 361 |
+
|
| 362 |
+
g = np.zeros((d, d), dtype=np.float64)
|
| 363 |
+
for j0 in range(0, D, int(D_block)):
|
| 364 |
+
j1 = min(D, j0 + int(D_block))
|
| 365 |
+
Mb = M_kX[:, j0:j1] # (k,block)
|
| 366 |
+
Jb = Mb.T @ grad_k # (block,d)
|
| 367 |
+
g += Jb.T @ Jb # (d,d)
|
| 368 |
+
|
| 369 |
+
g = 0.5 * (g + g.T)
|
| 370 |
+
g.flat[:: d + 1] += float(lam)
|
| 371 |
+
|
| 372 |
+
cF = la.cho_factor(g, lower=True, check_finite=False)
|
| 373 |
+
return g, cF
|
| 374 |
+
|
| 375 |
+
def _force_from_cache_noC(
|
| 376 |
+
self,
|
| 377 |
+
k_m: np.ndarray, # (k,)
|
| 378 |
+
Dw_over: np.ndarray, # (k,d)
|
| 379 |
+
v_x: np.ndarray, # (d,)
|
| 380 |
+
M_kX: np.ndarray, # (k,D)
|
| 381 |
+
*,
|
| 382 |
+
D_block: int = 8192,
|
| 383 |
+
) -> np.ndarray:
|
| 384 |
+
"""
|
| 385 |
+
Geodesic momentum force without storing C_mm, using blocked contractions over ambient dim D.
|
| 386 |
+
"""
|
| 387 |
+
c = self.beta / self.eps
|
| 388 |
+
v = v_x.astype(np.float64, copy=False)
|
| 389 |
+
|
| 390 |
+
inv_std2 = (1.0 / self.lat_std_x) ** 2 # (d,)
|
| 391 |
+
s_m = Dw_over @ v # (k,)
|
| 392 |
+
S = -(2.0 * c) * (k_m * s_m) # (k,)
|
| 393 |
+
|
| 394 |
+
term1 = (4.0 * c * c) * (k_m * s_m)[:, None] * Dw_over
|
| 395 |
+
term2 = (2.0 * c) * k_m[:, None] * (v[None, :] * inv_std2[None, :])
|
| 396 |
+
T = (term1 - term2).T # (d,k)
|
| 397 |
+
|
| 398 |
+
d = v.shape[0]
|
| 399 |
+
D = M_kX.shape[1]
|
| 400 |
+
f = np.zeros((d,), dtype=np.float64)
|
| 401 |
+
|
| 402 |
+
for j0 in range(0, D, int(D_block)):
|
| 403 |
+
j1 = min(D, j0 + int(D_block))
|
| 404 |
+
Mb = M_kX[:, j0:j1] # (k,block)
|
| 405 |
+
Jv_b = S @ Mb # (block,)
|
| 406 |
+
Hv_b = T @ Mb # (d,block)
|
| 407 |
+
f += Hv_b @ Jv_b # (d,)
|
| 408 |
+
|
| 409 |
+
return f
|
| 410 |
+
|
| 411 |
+
def flow(
|
| 412 |
+
self,
|
| 413 |
+
R_ax: Union[np.ndarray, list],
|
| 414 |
+
v_ax: Union[np.ndarray, list],
|
| 415 |
+
*,
|
| 416 |
+
eps: float = 1e-2,
|
| 417 |
+
K_p: int = 5,
|
| 418 |
+
K_q: int = 5,
|
| 419 |
+
D_block: int = 8192,
|
| 420 |
+
lam: float = 1e-10,
|
| 421 |
+
) -> Tuple[np.ndarray, np.ndarray]:
|
| 422 |
+
"""
|
| 423 |
+
One generalized-leapfrog step for geodesic flow on the pullback manifold.
|
| 424 |
+
"""
|
| 425 |
+
R = np.asarray(R_ax, dtype=np.float64)
|
| 426 |
+
v = np.asarray(v_ax, dtype=np.float64)
|
| 427 |
+
single = (R.ndim == 1)
|
| 428 |
+
|
| 429 |
+
if single:
|
| 430 |
+
R = R[None, :]
|
| 431 |
+
v = v[None, :]
|
| 432 |
+
|
| 433 |
+
if R.ndim != 2 or v.ndim != 2 or R.shape != v.shape or R.shape[1] != self.d_lat:
|
| 434 |
+
raise ValueError(f"Expected R_ax and v_ax shape (A,{self.d_lat}) (or ({self.d_lat},)).")
|
| 435 |
+
|
| 436 |
+
A, _d = R.shape
|
| 437 |
+
|
| 438 |
+
# Optional inducing subset indices per point
|
| 439 |
+
if self.pred_k is None or self.pred_k == self.m:
|
| 440 |
+
idx_aK = None
|
| 441 |
+
else:
|
| 442 |
+
Rw = (R - self.lat_mean_x[None, :]) / self.lat_std_x[None, :]
|
| 443 |
+
idx_aK, _ = self.ann_Z.search(Rw.astype(self.dtype, copy=False), self.pred_k)
|
| 444 |
+
idx_aK = idx_aK.astype(np.int64, copy=False)
|
| 445 |
+
|
| 446 |
+
R_next = np.empty_like(R)
|
| 447 |
+
v_next = np.empty_like(v)
|
| 448 |
+
|
| 449 |
+
for a in range(A):
|
| 450 |
+
idx = None if idx_aK is None else idx_aK[a]
|
| 451 |
+
M_kX = self.M_mX if idx is None else self.M_mX[idx]
|
| 452 |
+
|
| 453 |
+
r_n = R[a]
|
| 454 |
+
v_n = v[a]
|
| 455 |
+
|
| 456 |
+
# geometry at r_n
|
| 457 |
+
k_m_n, Dw_over_n, grad_k_n = self._rbf_cache_single(r_n, idx_m=idx)
|
| 458 |
+
g_n, cF_n = self._metric_from_gradM(grad_k_n, M_kX, D_block=D_block, lam=lam)
|
| 459 |
+
|
| 460 |
+
# momentum p_n = g(r_n) v_n
|
| 461 |
+
p_n = g_n @ v_n
|
| 462 |
+
|
| 463 |
+
# (1) implicit half-step in momentum
|
| 464 |
+
p = p_n.copy()
|
| 465 |
+
for _ in range(int(K_p)):
|
| 466 |
+
v_k = la.cho_solve(cF_n, p, check_finite=False)
|
| 467 |
+
f_k = self._force_from_cache_noC(k_m_n, Dw_over_n, v_k, M_kX, D_block=D_block)
|
| 468 |
+
p = p_n + 0.5 * float(eps) * f_k
|
| 469 |
+
p_half = p
|
| 470 |
+
|
| 471 |
+
# (2) implicit position update
|
| 472 |
+
v_half_n = la.cho_solve(cF_n, p_half, check_finite=False)
|
| 473 |
+
r = r_n + float(eps) * v_half_n
|
| 474 |
+
for _ in range(int(K_q)):
|
| 475 |
+
k_m_r, Dw_over_r, grad_k_r = self._rbf_cache_single(r, idx_m=idx)
|
| 476 |
+
g_r, cF_r = self._metric_from_gradM(grad_k_r, M_kX, D_block=D_block, lam=lam)
|
| 477 |
+
v_half_r = la.cho_solve(cF_r, p_half, check_finite=False)
|
| 478 |
+
r = r_n + 0.5 * float(eps) * (v_half_n + v_half_r)
|
| 479 |
+
r_np1 = r
|
| 480 |
+
|
| 481 |
+
# (3) explicit half-step in momentum at r_{n+1}
|
| 482 |
+
k_m_np1, Dw_over_np1, grad_k_np1 = self._rbf_cache_single(r_np1, idx_m=idx)
|
| 483 |
+
g_np1, cF_np1 = self._metric_from_gradM(grad_k_np1, M_kX, D_block=D_block, lam=lam)
|
| 484 |
+
v_mid = la.cho_solve(cF_np1, p_half, check_finite=False)
|
| 485 |
+
f_np1 = self._force_from_cache_noC(k_m_np1, Dw_over_np1, v_mid, M_kX, D_block=D_block)
|
| 486 |
+
|
| 487 |
+
p_np1 = p_half + 0.5 * float(eps) * f_np1
|
| 488 |
+
v_np1 = la.cho_solve(cF_np1, p_np1, check_finite=False)
|
| 489 |
+
|
| 490 |
+
R_next[a] = r_np1
|
| 491 |
+
v_next[a] = v_np1
|
| 492 |
+
|
| 493 |
+
if single:
|
| 494 |
+
return R_next[0], v_next[0]
|
| 495 |
+
return R_next, v_next
|
| 496 |
+
|
| 497 |
+
# ============================================================
|
| 498 |
+
# (NEW) Serialization + Hugging Face Hub
|
| 499 |
+
# ============================================================
|
| 500 |
+
def config_dict(self) -> Dict[str, Any]:
|
| 501 |
+
"""
|
| 502 |
+
Config mínima (sin arrays grandes) para inspección/reproducibilidad.
|
| 503 |
+
"""
|
| 504 |
+
return {
|
| 505 |
+
"class": "GPLM",
|
| 506 |
+
"beta": float(self.beta),
|
| 507 |
+
"eps": float(self.eps),
|
| 508 |
+
"sigma2": float(self.sigma2),
|
| 509 |
+
"jitter": float(self.jitter),
|
| 510 |
+
"seed": int(self.seed),
|
| 511 |
+
"center_X": bool(self.center_X),
|
| 512 |
+
"whiten_latent": bool(self.whiten_latent),
|
| 513 |
+
"inducing": str(self.inducing),
|
| 514 |
+
"fit_block": int(self.fit_block),
|
| 515 |
+
"pred_k": None if self.pred_k is None else int(self.pred_k),
|
| 516 |
+
"ann_backend": str(self.ann_backend),
|
| 517 |
+
"ann_params": None if self.ann_params is None else _to_builtin(self.ann_params),
|
| 518 |
+
"n_jobs": int(self.n_jobs),
|
| 519 |
+
"dtype": str(self.dtype_str),
|
| 520 |
+
"d_lat": int(self.d_lat),
|
| 521 |
+
"D": int(self.D),
|
| 522 |
+
"m": int(self.m),
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
def state_dict(self) -> Dict[str, Any]:
|
| 526 |
+
"""
|
| 527 |
+
Estado completo necesario para inferencia/flow (sin datos de entrenamiento completos).
|
| 528 |
+
"""
|
| 529 |
+
st: Dict[str, Any] = {
|
| 530 |
+
"config": self.config_dict(),
|
| 531 |
+
"M_mX": np.asarray(self.M_mX, dtype=np.float64),
|
| 532 |
+
"Z_mx_w": np.asarray(self.Z_mx_w, dtype=np.float64),
|
| 533 |
+
"mean_X": np.asarray(self.mean_X, dtype=np.float64),
|
| 534 |
+
"lat_mean_x": np.asarray(self.lat_mean_x, dtype=np.float64),
|
| 535 |
+
"lat_std_x": np.asarray(self.lat_std_x, dtype=np.float64),
|
| 536 |
+
}
|
| 537 |
+
return st
|
| 538 |
+
|
| 539 |
+
@classmethod
|
| 540 |
+
def from_state_dict(cls, st: Dict[str, Any]) -> "GPLM":
|
| 541 |
+
"""
|
| 542 |
+
Reconstruye un objeto GPLM entrenado SIN re-entrenar.
|
| 543 |
+
"""
|
| 544 |
+
if "config" not in st:
|
| 545 |
+
raise ValueError("state_dict inválido: falta 'config'.")
|
| 546 |
+
|
| 547 |
+
cfg = st["config"]
|
| 548 |
+
# construir instancia vacía
|
| 549 |
+
obj = cls.__new__(cls)
|
| 550 |
+
|
| 551 |
+
# meta/config
|
| 552 |
+
obj.beta = float(cfg["beta"])
|
| 553 |
+
obj.β = obj.beta
|
| 554 |
+
obj.eps = float(cfg["eps"])
|
| 555 |
+
obj.ε = obj.eps
|
| 556 |
+
obj.sigma2 = float(cfg["sigma2"])
|
| 557 |
+
obj.σ2 = obj.sigma2
|
| 558 |
+
obj.jitter = float(cfg["jitter"])
|
| 559 |
+
obj.seed = int(cfg["seed"])
|
| 560 |
+
obj.center_X = bool(cfg["center_X"])
|
| 561 |
+
obj.whiten_latent = bool(cfg["whiten_latent"])
|
| 562 |
+
obj.inducing = cfg.get("inducing", "given")
|
| 563 |
+
obj.fit_block = int(cfg["fit_block"])
|
| 564 |
+
obj.pred_k = cfg["pred_k"] if cfg["pred_k"] is None else int(cfg["pred_k"])
|
| 565 |
+
obj.pred_κ = obj.pred_k
|
| 566 |
+
obj.ann_backend = cfg.get("ann_backend", "auto")
|
| 567 |
+
obj.ann_params = cfg.get("ann_params", None)
|
| 568 |
+
obj.n_jobs = int(cfg.get("n_jobs", -1))
|
| 569 |
+
|
| 570 |
+
obj.dtype_str = str(cfg.get("dtype", "float32"))
|
| 571 |
+
obj.dtype = np.dtype(obj.dtype_str).type
|
| 572 |
+
|
| 573 |
+
obj.d_lat = int(cfg["d_lat"])
|
| 574 |
+
obj.D = int(cfg["D"])
|
| 575 |
+
obj.m = int(cfg["m"])
|
| 576 |
+
|
| 577 |
+
# arrays
|
| 578 |
+
obj.M_mX = np.asarray(st["M_mX"], dtype=np.float64)
|
| 579 |
+
obj.Z_mx_w = np.asarray(st["Z_mx_w"], dtype=np.float64)
|
| 580 |
+
obj.mean_X = np.asarray(st["mean_X"], dtype=np.float64)
|
| 581 |
+
obj.lat_mean_x = np.asarray(st["lat_mean_x"], dtype=np.float64)
|
| 582 |
+
obj.lat_std_x = np.asarray(st["lat_std_x"], dtype=np.float64)
|
| 583 |
+
|
| 584 |
+
# derived
|
| 585 |
+
obj.Z_mx = (obj.Z_mx_w * obj.lat_std_x[None, :]) + obj.lat_mean_x[None, :]
|
| 586 |
+
|
| 587 |
+
# ANN on inducing points (needed if pred_k is used)
|
| 588 |
+
obj.ann_Z, _ = make_ann(obj.ann_backend, ann_params=obj.ann_params, n_jobs=obj.n_jobs)
|
| 589 |
+
obj.ann_Z.build(obj.Z_mx_w.astype(obj.dtype, copy=False))
|
| 590 |
+
|
| 591 |
+
# no training data kept
|
| 592 |
+
obj.R_ix = None
|
| 593 |
+
obj.R_iX = None
|
| 594 |
+
obj.R_ix_w = None
|
| 595 |
+
obj.ann_train = None
|
| 596 |
+
obj.N = 0 # unknown/not needed for inference
|
| 597 |
+
|
| 598 |
+
return obj
|
| 599 |
+
|
| 600 |
+
def save_local(self, weights_file: str = "gplm.msgpack", config_file: str = "gplm_config.json") -> None:
|
| 601 |
+
"""
|
| 602 |
+
Guarda:
|
| 603 |
+
- weights_file: msgpack con state_dict (arrays + config)
|
| 604 |
+
- config_file: JSON legible con la config
|
| 605 |
+
"""
|
| 606 |
+
st = self.state_dict()
|
| 607 |
+
cfg = st["config"]
|
| 608 |
+
|
| 609 |
+
with open(config_file, "w", encoding="utf-8") as f:
|
| 610 |
+
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
| 611 |
+
|
| 612 |
+
# msgpack (preferentemente flax.serialization)
|
| 613 |
+
try:
|
| 614 |
+
import flax.serialization as flax_ser # type: ignore
|
| 615 |
+
except Exception as e:
|
| 616 |
+
raise RuntimeError("flax.serialization no está disponible; instale flax o use un backend alternativo.") from e
|
| 617 |
+
|
| 618 |
+
blob = flax_ser.msgpack_serialize(st)
|
| 619 |
+
with open(weights_file, "wb") as f:
|
| 620 |
+
f.write(blob)
|
| 621 |
+
|
| 622 |
+
@classmethod
|
| 623 |
+
def load_local(cls, weights_file: str = "gplm.msgpack") -> "GPLM":
|
| 624 |
+
"""
|
| 625 |
+
Carga desde msgpack (state_dict completo) y reconstruye el objeto.
|
| 626 |
+
"""
|
| 627 |
+
try:
|
| 628 |
+
import flax.serialization as flax_ser # type: ignore
|
| 629 |
+
except Exception as e:
|
| 630 |
+
raise RuntimeError("flax.serialization no está disponible; instale flax o use un backend alternativo.") from e
|
| 631 |
+
|
| 632 |
+
with open(weights_file, "rb") as f:
|
| 633 |
+
blob = f.read()
|
| 634 |
+
|
| 635 |
+
st = flax_ser.msgpack_restore(blob)
|
| 636 |
+
return cls.from_state_dict(st)
|
| 637 |
+
|
| 638 |
+
def upload_to_huggingface(
|
| 639 |
+
self,
|
| 640 |
+
repo_id: str,
|
| 641 |
+
*,
|
| 642 |
+
token: Optional[str] = None,
|
| 643 |
+
weights_file: str = "gplm.msgpack",
|
| 644 |
+
config_file: str = "gplm_config.json",
|
| 645 |
+
repo_type: str = "model",
|
| 646 |
+
revision: Optional[str] = None,
|
| 647 |
+
) -> None:
|
| 648 |
+
"""
|
| 649 |
+
Sube (weights + config) a Hugging Face Hub.
|
| 650 |
+
"""
|
| 651 |
+
try:
|
| 652 |
+
from huggingface_hub import HfApi, create_repo # type: ignore
|
| 653 |
+
except Exception as e:
|
| 654 |
+
raise RuntimeError("huggingface_hub no está instalado. Instale con `pip install huggingface_hub`.") from e
|
| 655 |
+
|
| 656 |
+
if token is None:
|
| 657 |
+
raise ValueError("token es requerido para subir al Hub (HUGGINGFACE_TOKEN/HF_TOKEN).")
|
| 658 |
+
|
| 659 |
+
with tempfile.TemporaryDirectory() as td:
|
| 660 |
+
wpath = os.path.join(td, weights_file)
|
| 661 |
+
cpath = os.path.join(td, config_file)
|
| 662 |
+
|
| 663 |
+
self.save_local(weights_file=wpath, config_file=cpath)
|
| 664 |
+
|
| 665 |
+
create_repo(repo_id, token=token, repo_type=repo_type, exist_ok=True)
|
| 666 |
+
api = HfApi(token=token)
|
| 667 |
+
|
| 668 |
+
api.upload_file(
|
| 669 |
+
path_or_fileobj=wpath,
|
| 670 |
+
path_in_repo=weights_file,
|
| 671 |
+
repo_id=repo_id,
|
| 672 |
+
repo_type=repo_type,
|
| 673 |
+
revision=revision,
|
| 674 |
+
)
|
| 675 |
+
api.upload_file(
|
| 676 |
+
path_or_fileobj=cpath,
|
| 677 |
+
path_in_repo=config_file,
|
| 678 |
+
repo_id=repo_id,
|
| 679 |
+
repo_type=repo_type,
|
| 680 |
+
revision=revision,
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
@classmethod
|
| 684 |
+
def download_from_huggingface(
|
| 685 |
+
cls,
|
| 686 |
+
repo_id: str,
|
| 687 |
+
*,
|
| 688 |
+
token: Optional[str] = None,
|
| 689 |
+
weights_file: str = "gplm.msgpack",
|
| 690 |
+
repo_type: str = "model",
|
| 691 |
+
revision: Optional[str] = None,
|
| 692 |
+
) -> "GPLM":
|
| 693 |
+
"""
|
| 694 |
+
Descarga weights_file desde el Hub y reconstruye el objeto sin re-entrenar.
|
| 695 |
+
"""
|
| 696 |
+
try:
|
| 697 |
+
from huggingface_hub import hf_hub_download # type: ignore
|
| 698 |
+
except Exception as e:
|
| 699 |
+
raise RuntimeError("huggingface_hub no está instalado. Instale con `pip install huggingface_hub`.") from e
|
| 700 |
+
|
| 701 |
+
local_path = hf_hub_download(
|
| 702 |
+
repo_id=repo_id,
|
| 703 |
+
filename=weights_file,
|
| 704 |
+
repo_type=repo_type,
|
| 705 |
+
token=token,
|
| 706 |
+
revision=revision,
|
| 707 |
+
)
|
| 708 |
+
return cls.load_local(local_path)
|
| 709 |
+
|
| 710 |
+
|
| 711 |
+
|
| 712 |
+
__all__ = ["GPLM", "InducingMode"]
|
utils.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/dima/utils.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from dataclasses import asdict, is_dataclass
|
| 5 |
+
from typing import Any, Dict, Iterable, Iterator, Optional, Sequence, Tuple, Union
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
ArrayLike = Union[np.ndarray, Sequence[float]]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# -------------------------
|
| 13 |
+
# Basic math helpers
|
| 14 |
+
# -------------------------
|
| 15 |
+
def ensure_2d(X: np.ndarray) -> np.ndarray:
|
| 16 |
+
"""Ensure X is 2D: (D,) -> (1,D)."""
|
| 17 |
+
X = np.asarray(X)
|
| 18 |
+
return X[None, :] if X.ndim == 1 else X
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def as_contig_f32(X: np.ndarray) -> np.ndarray:
|
| 22 |
+
"""Contiguous float32 array (good default for ANN + kernels)."""
|
| 23 |
+
return np.ascontiguousarray(np.asarray(X, dtype=np.float32))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def sqdist_ab(A: np.ndarray, B: np.ndarray) -> np.ndarray:
|
| 27 |
+
"""
|
| 28 |
+
Squared Euclidean distances between rows:
|
| 29 |
+
A: (a,d), B: (b,d) -> D2: (a,b)
|
| 30 |
+
"""
|
| 31 |
+
A = np.asarray(A)
|
| 32 |
+
B = np.asarray(B)
|
| 33 |
+
A2 = np.sum(A * A, axis=1, keepdims=True) # (a,1)
|
| 34 |
+
B2 = np.sum(B * B, axis=1, keepdims=True).T # (1,b)
|
| 35 |
+
G = A @ B.T # (a,b)
|
| 36 |
+
return np.maximum(A2 + B2 - 2.0 * G, 0.0)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def rbf_from_D2(D2: np.ndarray, *, beta: float, eps: float) -> np.ndarray:
|
| 40 |
+
"""RBF kernel weights from squared distances."""
|
| 41 |
+
eps = float(eps)
|
| 42 |
+
if eps <= 0:
|
| 43 |
+
raise ValueError("eps must be > 0")
|
| 44 |
+
return np.exp(-float(beta) * (np.asarray(D2) / eps))
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# -------------------------
|
| 48 |
+
# ε heuristics
|
| 49 |
+
# -------------------------
|
| 50 |
+
def median_eps_from_knn_d2(D2_iK: np.ndarray, *, use_kth: bool = True) -> float:
|
| 51 |
+
"""
|
| 52 |
+
Median bandwidth from kNN squared distances.
|
| 53 |
+
D2_iK: (N,K) squared distances to K nearest neighbors (excluding self).
|
| 54 |
+
- use_kth=True: use the Kth neighbor distance per point, then median over points
|
| 55 |
+
- use_kth=False: use all distances, then median
|
| 56 |
+
"""
|
| 57 |
+
D2_iK = np.asarray(D2_iK)
|
| 58 |
+
if D2_iK.size == 0:
|
| 59 |
+
return 1.0
|
| 60 |
+
v = D2_iK[:, -1] if use_kth else D2_iK.reshape(-1)
|
| 61 |
+
eps = float(np.median(v))
|
| 62 |
+
return max(eps, 1e-12)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def median_eps_from_pairs(X: np.ndarray, *, max_pairs: int = 200_000, seed: int = 0) -> float:
|
| 66 |
+
"""
|
| 67 |
+
Median of random-pair squared distances (rough fallback if you don't have kNN distances).
|
| 68 |
+
"""
|
| 69 |
+
X = np.asarray(X)
|
| 70 |
+
N = X.shape[0]
|
| 71 |
+
if N < 2:
|
| 72 |
+
return 1.0
|
| 73 |
+
|
| 74 |
+
rng = np.random.default_rng(seed)
|
| 75 |
+
p = int(min(max_pairs, N * (N - 1) // 2))
|
| 76 |
+
|
| 77 |
+
i = rng.integers(0, N, size=p, endpoint=False)
|
| 78 |
+
j = rng.integers(0, N, size=p, endpoint=False)
|
| 79 |
+
mask = (i != j)
|
| 80 |
+
i = i[mask]
|
| 81 |
+
j = j[mask]
|
| 82 |
+
if i.size == 0:
|
| 83 |
+
return 1.0
|
| 84 |
+
|
| 85 |
+
D2 = np.sum((X[i] - X[j]) ** 2, axis=1)
|
| 86 |
+
eps = float(np.median(D2))
|
| 87 |
+
return max(eps, 1e-12)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# -------------------------
|
| 91 |
+
# Inducing / landmark selection
|
| 92 |
+
# -------------------------
|
| 93 |
+
def fps_indices(X: np.ndarray, m: int, *, seed: int = 0) -> np.ndarray:
|
| 94 |
+
"""
|
| 95 |
+
Farthest Point Sampling indices (O(N*m)).
|
| 96 |
+
Good for space-filling inducing points / landmarks.
|
| 97 |
+
X: (N,d)
|
| 98 |
+
Returns idx: (m,)
|
| 99 |
+
"""
|
| 100 |
+
X = np.asarray(X)
|
| 101 |
+
N = X.shape[0]
|
| 102 |
+
m = int(min(max(1, m), N))
|
| 103 |
+
|
| 104 |
+
rng = np.random.default_rng(seed)
|
| 105 |
+
idx = np.empty(m, dtype=np.int64)
|
| 106 |
+
|
| 107 |
+
idx[0] = int(rng.integers(0, N))
|
| 108 |
+
d2 = np.sum((X - X[idx[0]]) ** 2, axis=1)
|
| 109 |
+
|
| 110 |
+
for t in range(1, m):
|
| 111 |
+
idx[t] = int(np.argmax(d2))
|
| 112 |
+
new_d2 = np.sum((X - X[idx[t]]) ** 2, axis=1)
|
| 113 |
+
d2 = np.minimum(d2, new_d2)
|
| 114 |
+
|
| 115 |
+
return idx
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# -------------------------
|
| 119 |
+
# Batching utilities
|
| 120 |
+
# -------------------------
|
| 121 |
+
def batched_range(n: int, batch_size: int) -> Iterator[Tuple[int, int]]:
|
| 122 |
+
"""Yield (start, end) slices covering [0, n) in batches."""
|
| 123 |
+
bs = int(batch_size)
|
| 124 |
+
if bs <= 0:
|
| 125 |
+
raise ValueError("batch_size must be > 0")
|
| 126 |
+
for s in range(0, int(n), bs):
|
| 127 |
+
yield s, min(int(n), s + bs)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def batch_iter(X: np.ndarray, batch_size: int) -> Iterator[np.ndarray]:
|
| 131 |
+
"""Yield contiguous batches from X."""
|
| 132 |
+
X = np.asarray(X)
|
| 133 |
+
for s, e in batched_range(X.shape[0], batch_size):
|
| 134 |
+
yield X[s:e]
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# -------------------------
|
| 138 |
+
# Metrics
|
| 139 |
+
# -------------------------
|
| 140 |
+
def rmse(a: np.ndarray, b: np.ndarray) -> float:
|
| 141 |
+
a = np.asarray(a)
|
| 142 |
+
b = np.asarray(b)
|
| 143 |
+
return float(np.sqrt(np.mean((a - b) ** 2)))
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def mse(a: np.ndarray, b: np.ndarray) -> float:
|
| 147 |
+
a = np.asarray(a)
|
| 148 |
+
b = np.asarray(b)
|
| 149 |
+
return float(np.mean((a - b) ** 2))
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# -------------------------
|
| 153 |
+
# Device helpers (JAX optional)
|
| 154 |
+
# -------------------------
|
| 155 |
+
def get_jax_device(prefer: str = "auto"):
|
| 156 |
+
"""
|
| 157 |
+
Safe JAX device selection.
|
| 158 |
+
prefer: "auto" | "gpu" | "cpu"
|
| 159 |
+
- returns a jax Device if jax is installed, else None
|
| 160 |
+
"""
|
| 161 |
+
prefer = (prefer or "auto").lower()
|
| 162 |
+
try:
|
| 163 |
+
import jax # local import
|
| 164 |
+
except Exception:
|
| 165 |
+
return None
|
| 166 |
+
|
| 167 |
+
devs = jax.devices()
|
| 168 |
+
gpu = [d for d in devs if d.platform == "gpu"]
|
| 169 |
+
cpu = [d for d in devs if d.platform == "cpu"]
|
| 170 |
+
|
| 171 |
+
if prefer in ("auto", "gpu"):
|
| 172 |
+
return gpu[0] if gpu else cpu[0] if cpu else devs[0]
|
| 173 |
+
if prefer == "cpu":
|
| 174 |
+
return cpu[0] if cpu else devs[0]
|
| 175 |
+
# fallback
|
| 176 |
+
return gpu[0] if gpu else cpu[0] if cpu else devs[0]
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# -------------------------
|
| 180 |
+
# JSON helpers (for configs)
|
| 181 |
+
# -------------------------
|
| 182 |
+
def to_jsonable(x: Any) -> Any:
|
| 183 |
+
"""
|
| 184 |
+
Convert common objects (numpy scalars/arrays, dataclasses) into JSON-serializable types.
|
| 185 |
+
"""
|
| 186 |
+
if is_dataclass(x):
|
| 187 |
+
return {k: to_jsonable(v) for k, v in asdict(x).items()}
|
| 188 |
+
|
| 189 |
+
if isinstance(x, (np.floating, np.integer)):
|
| 190 |
+
return x.item()
|
| 191 |
+
|
| 192 |
+
if isinstance(x, np.ndarray):
|
| 193 |
+
# prefer list for small arrays; for large arrays you typically store separately
|
| 194 |
+
return x.tolist()
|
| 195 |
+
|
| 196 |
+
if isinstance(x, dict):
|
| 197 |
+
return {str(k): to_jsonable(v) for k, v in x.items()}
|
| 198 |
+
|
| 199 |
+
if isinstance(x, (list, tuple)):
|
| 200 |
+
return [to_jsonable(v) for v in x]
|
| 201 |
+
|
| 202 |
+
return x
|