geneflowdecoder 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ # -*- coding: utf-8 -*-
2
+ """GeneFlowDecoder: z -> X decoder (Gene Transformer + Rectified Flow)."""
3
+
4
+ from ._core import GeneFlowDecoder, FlowConfig
5
+ from ._version import __version__
6
+
7
+ __all__ = [
8
+ "GeneFlowDecoder",
9
+ "FlowConfig",
10
+ "__version__",
11
+ ]
12
+
13
+ __doc__ = GeneFlowDecoder.__doc__ or ""
@@ -0,0 +1,567 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ GeneFlowDecoder
4
+ ===============
5
+
6
+ z (fixed cell-state representation) --> X (log-normalized expression, log1p space)
7
+
8
+ Public API
9
+ ----------
10
+ model = GeneFlowDecoder(n_genes=2048, z_dim=256)
11
+ model.fit(X, z) # train
12
+ X_hat = model(z) # sample (generative)
13
+ X_hat = model.predict(z) # deterministic (NB mean)
14
+
15
+ This module is framework-agnostic beyond ``torch`` and ``numpy``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ from dataclasses import dataclass, field
22
+ from typing import Optional, Tuple, Union
23
+
24
+ import numpy as np
25
+
26
+ try:
27
+ import torch
28
+ import torch.nn as nn
29
+ import torch.nn.functional as F
30
+ _no_grad = torch.no_grad
31
+ except Exception: # pragma: no cover - soft dep for import-only environments
32
+ torch = None # type: ignore
33
+ nn = object # type: ignore
34
+ F = object # type: ignore
35
+
36
+ def _no_grad(fn): # identity decorator when torch is absent
37
+ return fn
38
+
39
+ Tensor = object # placeholder for type annotations only
40
+
41
+ try:
42
+ from torch import Tensor as _TorchTensor # noqa: F401
43
+ Tensor = _TorchTensor # type: ignore
44
+ except Exception:
45
+ pass
46
+
47
+ Array = Union[np.ndarray, torch.Tensor]
48
+
49
+
50
+ def _as_tensor(x: Array, device: torch.device) -> torch.Tensor:
51
+ if isinstance(x, np.ndarray):
52
+ x = torch.from_numpy(x)
53
+ return x.to(device, non_blocking=True)
54
+
55
+
56
+ # ----------------------------------------------------------------------
57
+ # 1. Conditional modulation & transformer blocks
58
+ # ----------------------------------------------------------------------
59
+
60
+ class _AdaLNModulation(nn.Module):
61
+ def __init__(self, d: int, cond_dim: int):
62
+ super().__init__()
63
+ self.norm = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
64
+ self.proj = nn.Sequential(
65
+ nn.Linear(cond_dim, d), nn.SiLU(), nn.Linear(d, 3 * d)
66
+ )
67
+ nn.init.zeros_(self.proj[-1].weight)
68
+ nn.init.zeros_(self.proj[-1].bias)
69
+
70
+ def forward(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
71
+ g, b, gate = self.proj(z).chunk(3, dim=-1)
72
+ g, b, gate = g.unsqueeze(1), b.unsqueeze(1), gate.unsqueeze(1).sigmoid()
73
+ return gate * (self.norm(x) * (1 + g) + b)
74
+
75
+
76
+ class _CondBlock(nn.Module):
77
+ def __init__(self, d: int, cond_dim: int, n_heads: int, mlp_ratio: float = 4.0):
78
+ super().__init__()
79
+ self.attn_norm = _AdaLNModulation(d, cond_dim)
80
+ self.attn = nn.MultiheadAttention(d, n_heads, batch_first=True)
81
+ self.mlp_norm = _AdaLNModulation(d, cond_dim)
82
+ h = int(d * mlp_ratio)
83
+ self.mlp = nn.Sequential(nn.Linear(d, h), nn.GELU(), nn.Linear(h, d))
84
+
85
+ def forward(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
86
+ h = self.attn_norm(x, z)
87
+ h, _ = self.attn(h, h, h, need_weights=False)
88
+ x = x + h
89
+ return x + self.mlp(self.mlp_norm(x, z))
90
+
91
+
92
+ class _GeneTransformer(nn.Module):
93
+ def __init__(
94
+ self,
95
+ n_genes: int,
96
+ d: int,
97
+ depth: int,
98
+ n_heads: int,
99
+ cond_dim: int,
100
+ chunk_size: int,
101
+ ckpt: bool,
102
+ ):
103
+ super().__init__()
104
+ self.d = d
105
+ self.n_genes = n_genes
106
+ self.chunk_size = chunk_size
107
+ self.gene_embed = nn.Embedding(n_genes, d)
108
+ nn.init.normal_(self.gene_embed.weight, std=0.02)
109
+ self.pos_embed = nn.Parameter(torch.zeros(1, n_genes, d))
110
+ nn.init.normal_(self.pos_embed, std=0.02)
111
+
112
+ self.blocks = nn.ModuleList(
113
+ [_CondBlock(d, cond_dim, n_heads) for _ in range(depth)]
114
+ )
115
+ self.head_norm = nn.LayerNorm(d, eps=1e-6)
116
+ self._ckpt = ckpt
117
+
118
+ def _run(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
119
+ for blk in self.blocks:
120
+ if self._ckpt and self.training:
121
+ x = torch.utils.checkpoint.checkpoint(
122
+ blk, x, z, use_reentrant=False
123
+ )
124
+ else:
125
+ x = blk(x, z)
126
+ return self.head_norm(x)
127
+
128
+ def forward(self, gene_ids: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
129
+ B, G = gene_ids.shape[0], gene_ids.shape[-1]
130
+ if G <= self.chunk_size:
131
+ h = self.gene_embed(gene_ids) + self.pos_embed[:, :G, :]
132
+ return self._run(h, z)
133
+
134
+ outs: list[torch.Tensor] = []
135
+ s = 0
136
+ while s < G:
137
+ e = min(s + self.chunk_size, G)
138
+ h = self.gene_embed(gene_ids[..., s:e]) + self.pos_embed[:, s:e, :]
139
+ outs.append(self._run(h, z))
140
+ s = e
141
+ return torch.cat(outs, dim=1)
142
+
143
+
144
+ # ----------------------------------------------------------------------
145
+ # 2. Negative-binomial output head
146
+ # ----------------------------------------------------------------------
147
+
148
+ class _NBHead(nn.Module):
149
+ def __init__(self, d: int, n_genes: int):
150
+ super().__init__()
151
+ self.mu_proj = nn.Linear(d, n_genes)
152
+ self.theta = nn.Parameter(torch.zeros(n_genes))
153
+ nn.init.zeros_(self.mu_proj.weight)
154
+ nn.init.zeros_(self.mu_proj.bias)
155
+
156
+ def forward(self, h: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
157
+ return self.mu_proj(h), F.softplus(self.theta) + 1e-3
158
+
159
+ @staticmethod
160
+ def nll(
161
+ log_mu: torch.Tensor,
162
+ theta: torch.Tensor,
163
+ x: torch.Tensor,
164
+ mask: Optional[torch.Tensor] = None,
165
+ ) -> torch.Tensor:
166
+ mu = log_mu.exp().clamp(min=1e-6)
167
+ th = theta.unsqueeze(0).expand_as(x)
168
+ ll = (
169
+ torch.lgamma(x + th)
170
+ - torch.lgamma(th)
171
+ - torch.lgamma(x + 1)
172
+ + th * (th.log() - (th + mu).log())
173
+ + x * (mu.log() - (th + mu).log())
174
+ )
175
+ if mask is not None:
176
+ ll = ll * mask
177
+ return -ll.sum() / mask.sum().clamp(min=1.0)
178
+ return -ll.mean()
179
+
180
+
181
+ @dataclass
182
+ class FlowConfig:
183
+ """Hyper-parameters of the conditional flow matching objective."""
184
+
185
+ sigma_min: float = 1e-4
186
+ nb_weight: float = 1.0
187
+ corr_weight: float = 0.1
188
+
189
+
190
+ # ----------------------------------------------------------------------
191
+ # 3. GeneFlowDecoder
192
+ # ----------------------------------------------------------------------
193
+
194
+ class GeneFlowDecoder(nn.Module):
195
+ """
196
+ Conditional Rectified-Flow decoder from a **fixed** cell-state vector *z*
197
+ to a log1p expression profile *X*.
198
+
199
+ Parameters
200
+ ----------
201
+ n_genes : int
202
+ Number of genes (output dimension).
203
+ z_dim : int
204
+ Dimensionality of the cell-state representation.
205
+ d, depth, n_heads : int
206
+ Transformer width, number of layers, number of attention heads.
207
+ chunk_size : int
208
+ Chunk size for the gene axis (enables >10k gene panels on one GPU).
209
+ gradient_checkpoint : bool
210
+ Enable activation checkpointing inside the transformer.
211
+ cfg : FlowConfig | None
212
+ Flow / loss hyper-parameters.
213
+ freeze_z : bool, default ``True``
214
+ When ``True`` (recommended), *z* is detached everywhere: it is treated
215
+ as a fixed, non-trainable conditioning signal and is never updated.
216
+ device : ``"auto"`` | ``str`` | ``torch.device``
217
+ Where to place the model. ``"auto"`` picks CUDA if available.
218
+
219
+ Examples
220
+ --------
221
+ >>> import numpy as np
222
+ >>> from geneflowdecoder import GeneFlowDecoder
223
+ >>> G, D = 2048, 128
224
+ >>> model = GeneFlowDecoder(n_genes=G, z_dim=D, d=512, depth=8)
225
+ >>> X = np.random.randn(1024, G).astype("float32")
226
+ >>> z = np.random.randn(1024, D).astype("float32")
227
+ >>> model.fit(X, z, epochs=10, batch_size=64)
228
+ >>> X_hat = model(z[:4]) # sampled, log1p space
229
+ >>> X_rec = model.predict(z[:4]) # deterministic NB mean
230
+ """
231
+
232
+ def __init__(
233
+ self,
234
+ n_genes: int,
235
+ z_dim: int,
236
+ *,
237
+ d: int = 768,
238
+ depth: int = 16,
239
+ n_heads: int = 12,
240
+ chunk_size: int = 2048,
241
+ gradient_checkpoint: bool = True,
242
+ cfg: Optional[FlowConfig] = None,
243
+ freeze_z: bool = True,
244
+ device: str = "auto",
245
+ ):
246
+ super().__init__()
247
+ if device == "auto":
248
+ device = "cuda" if torch.cuda.is_available() else "cpu"
249
+ self.device = torch.device(device)
250
+ self.n_genes = int(n_genes)
251
+ self.z_dim = int(z_dim)
252
+ self.freeze_z = bool(freeze_z)
253
+ self._cfg = cfg or FlowConfig()
254
+
255
+ self.backbone = _GeneTransformer(
256
+ n_genes, d, depth, n_heads, z_dim, chunk_size, gradient_checkpoint
257
+ )
258
+ self.nb_head = _NBHead(d, n_genes)
259
+ self.vel_proj = nn.Linear(d, n_genes)
260
+ self.time_mlp = nn.Sequential(
261
+ nn.Linear(128, d), nn.SiLU(), nn.Linear(d, d)
262
+ )
263
+ self._gene_ids = nn.Parameter(
264
+ torch.arange(self.n_genes), requires_grad=False
265
+ )
266
+ self.to(self.device)
267
+
268
+ # ----------------------------------------------------------------
269
+ # internals
270
+ # ----------------------------------------------------------------
271
+ def _sincos(self, t: torch.Tensor) -> torch.Tensor:
272
+ half = 64
273
+ freqs = torch.exp(
274
+ -math.log(10000.0) * torch.arange(half, device=t.device) / (half - 1)
275
+ )
276
+ a = t[:, None] * freqs[None, :]
277
+ e = torch.cat([a.sin(), a.cos()], dim=-1)
278
+ if e.shape[-1] < 128:
279
+ e = F.pad(e, (0, 128 - e.shape[-1]))
280
+ return e
281
+
282
+ def _maybe_detach(
283
+ self, z: torch.Tensor, *, snapshot: bool = False
284
+ ) -> torch.Tensor:
285
+ """冻结 z 时截断计算图;``snapshot=True`` 再额外拷一份独立副本。
286
+
287
+ snapshot 只在 fit 入口用:把当轮 z 固化下来,之后外部 encoder
288
+ 若继续 backward / 原地更新,训练集 z 的数值也不会跟着变。
289
+ 单次推理(forward / predict)不开 snapshot,省一次显存拷贝。
290
+ """
291
+ if not self.freeze_z:
292
+ return z
293
+ z = z.detach()
294
+ return z.clone() if snapshot else z
295
+
296
+ def _velocity(
297
+ self, x_t: torch.Tensor, t: torch.Tensor, z: torch.Tensor
298
+ ) -> torch.Tensor:
299
+ z = self._maybe_detach(z)
300
+ B = x_t.shape[0]
301
+ t_emb = self.time_mlp(self._sincos(t)).unsqueeze(1)
302
+ gids = self._gene_ids.unsqueeze(0).expand(B, -1)
303
+ h = self.backbone(gids, z) + t_emb
304
+ return self.vel_proj(h)
305
+
306
+ # ----------------------------------------------------------------
307
+ # objective
308
+ # ----------------------------------------------------------------
309
+ def _loss(
310
+ self,
311
+ x: torch.Tensor,
312
+ z: torch.Tensor,
313
+ mask: Optional[torch.Tensor] = None,
314
+ ) -> torch.Tensor:
315
+ z = self._maybe_detach(z)
316
+ B = x.shape[0]
317
+ t = torch.rand(B, device=self.device).clamp(
318
+ self._cfg.sigma_min, 1.0 - self._cfg.sigma_min
319
+ )
320
+ eps = torch.randn_like(x)
321
+ x_t = x.lerp(eps, t.unsqueeze(1))
322
+
323
+ v_pred = self._velocity(x_t, t, z)
324
+ v_target = eps - x # rectified-flow target
325
+ mse = F.mse_loss(v_pred, v_target, reduction="none")
326
+ if mask is not None:
327
+ mse = mse * mask
328
+ mse = mse.sum() / mask.sum().clamp(min=1.0)
329
+ else:
330
+ mse = mse.mean()
331
+
332
+ x0_hat = x_t.lerp(v_pred.detach(), t.unsqueeze(1))
333
+
334
+ nb = torch.tensor(0.0, device=self.device)
335
+ if self._cfg.nb_weight > 0:
336
+ gids = self._gene_ids.unsqueeze(0).expand(B, -1)
337
+ log_mu, theta = self.nb_head(self.backbone(gids, z))
338
+ nb = self.nb_head.nll(log_mu, theta, x, mask)
339
+
340
+ corr = torch.tensor(0.0, device=self.device)
341
+ if self._cfg.corr_weight > 0 and mask is None:
342
+ corr = self._corr(x0_hat, x)
343
+
344
+ return (
345
+ mse
346
+ + self._cfg.nb_weight * nb
347
+ + self._cfg.corr_weight * corr
348
+ )
349
+
350
+ @staticmethod
351
+ def _corr(p: torch.Tensor, q: torch.Tensor) -> torch.Tensor:
352
+ p = p - p.mean(1, keepdim=True)
353
+ q = q - q.mean(1, keepdim=True)
354
+ denom = p.norm(dim=1) * q.norm(dim=1) + 1e-8
355
+ return (1 - (p * q).sum(dim=1) / denom).mean()
356
+
357
+ # ----------------------------------------------------------------
358
+ # fit
359
+ # ----------------------------------------------------------------
360
+ def fit(
361
+ self,
362
+ X: Array,
363
+ z: Array,
364
+ *,
365
+ X_val: Optional[Array] = None,
366
+ z_val: Optional[Array] = None,
367
+ epochs: int = 200,
368
+ batch_size: int = 64,
369
+ lr: float = 2e-4,
370
+ weight_decay: float = 0.02,
371
+ patience: int = 20,
372
+ num_workers: int = 0,
373
+ verbose: int = 1,
374
+ ) -> "GeneFlowDecoder":
375
+ """
376
+ Train the decoder.
377
+
378
+ Parameters
379
+ ----------
380
+ X : array, shape ``(N, n_genes)``
381
+ Log1p-normalized expression matrix.
382
+ z : array, shape ``(N, z_dim)``
383
+ Cell-state vectors. **Never updated** when ``freeze_z=True``.
384
+ X_val, z_val : arrays, optional
385
+ Validation split. If given, early stopping is enabled and the
386
+ best weights (lowest validation loss) are restored at the end.
387
+ epochs, batch_size, lr, weight_decay, patience
388
+ Standard optimisation knobs.
389
+ num_workers : int
390
+ Workers for the training DataLoader.
391
+ verbose : int
392
+ 0 = silent, 1 = epoch logs.
393
+
394
+ Returns
395
+ -------
396
+ self
397
+ """
398
+ X = _as_tensor(X, self.device)
399
+ z = self._maybe_detach(_as_tensor(z, self.device), snapshot=True)
400
+
401
+ ds = torch.utils.data.TensorDataset(X, z)
402
+ dl = torch.utils.data.DataLoader(
403
+ ds,
404
+ batch_size=int(batch_size),
405
+ shuffle=True,
406
+ num_workers=int(num_workers),
407
+ pin_memory=True,
408
+ )
409
+
410
+ has_val = False
411
+ if X_val is not None and z_val is not None:
412
+ Xv = _as_tensor(X_val, self.device)
413
+ zv = self._maybe_detach(_as_tensor(z_val, self.device), snapshot=True)
414
+ has_val = True
415
+
416
+ # Only decoder-owned parameters are optimised.
417
+ params = [p for p in self.parameters() if p.requires_grad]
418
+ opt = torch.optim.AdamW(params, lr=lr, weight_decay=weight_decay)
419
+ sched = torch.optim.lr_scheduler.CosineAnnealingLR(
420
+ opt, T_max=int(epochs)
421
+ )
422
+
423
+ best_val = float("inf")
424
+ best_state: Optional[dict[str, torch.Tensor]] = None
425
+ no_improve = 0
426
+
427
+ for ep in range(1, int(epochs) + 1):
428
+ self.train()
429
+ tot = 0.0
430
+ for xb, zb in dl:
431
+ opt.zero_grad()
432
+ loss = self._loss(xb, zb)
433
+ loss.backward()
434
+ nn.utils.clip_grad_norm_(params, 1.0)
435
+ opt.step()
436
+ tot += loss.item() * xb.shape[0]
437
+ sched.step()
438
+
439
+ if verbose:
440
+ msg = (
441
+ f"[fit] epoch {ep:3d}/{epochs} "
442
+ f"| train={tot / len(ds):.4f}"
443
+ )
444
+ if has_val:
445
+ self.eval()
446
+ with torch.no_grad():
447
+ vl = self._loss(Xv, zv).item()
448
+ msg += f" | val={vl:.4f}"
449
+ if vl < best_val - 1e-5:
450
+ best_val, no_improve = vl, 0
451
+ best_state = {
452
+ k: v.detach().cpu().clone()
453
+ for k, v in self.state_dict().items()
454
+ }
455
+ else:
456
+ no_improve += 1
457
+ if no_improve >= int(patience):
458
+ if verbose:
459
+ print(f"[fit] early stop at epoch {ep}")
460
+ break
461
+ print(msg)
462
+
463
+ if best_state is not None:
464
+ self.load_state_dict(best_state)
465
+ if verbose:
466
+ print(f"[fit] restored best val={best_val:.4f}")
467
+ return self
468
+
469
+ # ----------------------------------------------------------------
470
+ # inference
471
+ # ----------------------------------------------------------------
472
+ @torch.no_grad()
473
+ def forward(
474
+ self,
475
+ z: Array,
476
+ *,
477
+ steps: int = 4,
478
+ method: str = "midpoint",
479
+ deterministic: bool = False,
480
+ ) -> torch.Tensor:
481
+ """
482
+ Sample an expression profile from *z* (log1p space).
483
+
484
+ Parameters
485
+ ----------
486
+ z : array, shape ``(B, z_dim)``
487
+ steps : int
488
+ Number of flow-integration steps (4 is usually enough).
489
+ method : ``"midpoint"`` | ``"euler"``
490
+ deterministic : bool
491
+ If ``True``, returns the deterministic NB mean
492
+ (equivalent to ``self.predict(z)``).
493
+ """
494
+ z = self._maybe_detach(_as_tensor(z, self.device))
495
+ if deterministic:
496
+ return self.predict(z)
497
+
498
+ self.eval()
499
+ B = z.shape[0]
500
+ x = torch.randn(B, self.n_genes, device=self.device)
501
+ ts = torch.linspace(0.0, 1.0, int(steps) + 1, device=self.device)
502
+ for i in range(int(steps)):
503
+ t0, t1 = ts[i], ts[i + 1]
504
+ tmid = (t0 + t1) / 2
505
+ dt = (t1 - t0).item()
506
+ v = self._velocity(x, torch.full((B,), t0, device=self.device), z)
507
+ if method == "midpoint":
508
+ xm = x + 0.5 * dt * v
509
+ v = self._velocity(
510
+ xm, torch.full((B,), tmid, device=self.device), z
511
+ )
512
+ x = x + dt * v
513
+ return x
514
+
515
+ @torch.no_grad()
516
+ def predict(self, z: Array) -> torch.Tensor:
517
+ """
518
+ Deterministic reconstruction: the NB mean in log1p space.
519
+
520
+ Convert to pseudo-counts with ``(log_mu.exp() - 1).clamp(min=0)``.
521
+ """
522
+ z = self._maybe_detach(_as_tensor(z, self.device))
523
+ self.eval()
524
+ B = z.shape[0]
525
+ gids = self._gene_ids.unsqueeze(0).expand(B, -1)
526
+ log_mu, _ = self.nb_head(self.backbone(gids, z))
527
+ return log_mu
528
+
529
+ # ----------------------------------------------------------------
530
+ # persistence
531
+ # ----------------------------------------------------------------
532
+ def save(self, path: str) -> None:
533
+ """Save model weights + config to ``path``."""
534
+ torch.save(
535
+ {
536
+ "state": self.state_dict(),
537
+ "cfg": self._cfg,
538
+ "freeze_z": self.freeze_z,
539
+ "n_genes": self.n_genes,
540
+ "z_dim": self.z_dim,
541
+ },
542
+ path,
543
+ )
544
+
545
+ @classmethod
546
+ def load(
547
+ cls,
548
+ path: str,
549
+ *,
550
+ device: str = "auto",
551
+ **init_kwargs,
552
+ ) -> "GeneFlowDecoder":
553
+ """
554
+ Load a saved model.
555
+
556
+ ``n_genes`` / ``z_dim`` / ``freeze_z`` are read from the checkpoint
557
+ unless explicitly overridden in ``init_kwargs``.
558
+ """
559
+ ckpt = torch.load(path, map_location="cpu")
560
+ init_kwargs.setdefault("n_genes", ckpt.get("n_genes"))
561
+ init_kwargs.setdefault("z_dim", ckpt.get("z_dim"))
562
+ init_kwargs.setdefault("freeze_z", ckpt.get("freeze_z", True))
563
+ init_kwargs["device"] = device
564
+ model = cls(**init_kwargs)
565
+ model._cfg = ckpt.get("cfg", model._cfg)
566
+ model.load_state_dict(ckpt["state"])
567
+ return model
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ __version__ = "0.1.0"
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: geneflowdecoder
3
+ Version: 0.1.0
4
+ Summary: Gene Transformer + Conditional Rectified Flow decoder from cell-state z to log1p expression X.
5
+ Author: GeneFlowDecoder Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ZengFLab/geneflowdecoder
8
+ Project-URL: Documentation, https://github.com/ZengFLab/geneflowdecoder#readme
9
+ Project-URL: Repository, https://github.com/ZengFLab/geneflowdecoder
10
+ Project-URL: Issues, https://github.com/ZengFLab/geneflowdecoder/issues
11
+ Keywords: single-cell,transcriptomics,decoder,flow-matching,gene-transformer,scvi,representation-learning
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: torch>=2.1
26
+ Requires-Dist: numpy>=1.24
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Requires-Dist: build; extra == "dev"
30
+ Requires-Dist: twine; extra == "dev"
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest>=7; extra == "test"
33
+ Dynamic: license-file
34
+
35
+ # GeneFlowDecoder
36
+
37
+ `z → X` 的细胞状态解码器:**Gene Transformer + Conditional Rectified Flow (CFM)**,
38
+ 把固定的细胞表征 `z` 解码成 log1p 归一化表达谱 `X`。
39
+
40
+ > 设计目标:接口极简、可复现、单卡可跑 2 万+ 基因面板;`z` 默认冻结,
41
+ > 不参与反传,也不被优化器更新。
42
+
43
+ ```python
44
+ from geneflowdecoder import GeneFlowDecoder
45
+
46
+ model = GeneFlowDecoder(n_genes=2048, z_dim=256, d=512, depth=8)
47
+
48
+ model.fit(X_train, z_train, # X: [N, G] log1p; z: [N, D]
49
+ X_val=X_val, z_val=z_val, # 可选,开启早停
50
+ epochs=200, batch_size=64)
51
+
52
+ X_hat = model(z_test) # 采样(生成式,log1p 空间)
53
+ X_hat = model(z_test, steps=8, deterministic=False)
54
+
55
+ X_rec = model.predict(z_test) # 确定性还原:NB 均值
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 安装
61
+
62
+ ```bash
63
+ # 从源码(开发模式)
64
+ git clone https://github.com/your-org/geneflowdecoder.git
65
+ cd geneflowdecoder
66
+ pip install -e .[dev]
67
+
68
+ # 或构建 wheel 后安装
69
+ python -m build
70
+ pip install dist/geneflowdecoder-0.1.0-py3-none-any.whl
71
+ ```
72
+
73
+ 依赖:`torch >= 2.1`、`numpy >= 1.24`。没有 GPU 也能在 CPU 上跑(只是慢)。
74
+
75
+ ---
76
+
77
+ ## API
78
+
79
+ ### `GeneFlowDecoder(...)`
80
+
81
+ | 参数 | 默认 | 说明 |
82
+ |---|---|---|
83
+ | `n_genes` | — | 基因数,即输出维度 `G` |
84
+ | `z_dim` | — | 细胞状态 `z` 的维度 |
85
+ | `d` | 768 | Transformer 宽度 |
86
+ | `depth` | 16 | Transformer 层数 |
87
+ | `n_heads` | 12 | 注意力头数 |
88
+ | `chunk_size` | 2048 | 基因轴分块,控制显存峰值 |
89
+ | `gradient_checkpoint` | `True` | 激活检查点,省显存 |
90
+ | `cfg` | `FlowConfig()` | flow / loss 超参 |
91
+ | `freeze_z` | `True` | **`z` 全程 `detach`,只读条件** |
92
+ | `device` | `"auto"` | `"auto"` / `"cuda"` / `"cpu"` |
93
+
94
+ ### `model.fit(X, z, *, X_val=None, z_val=None, epochs=200, batch_size=64, lr=2e-4, weight_decay=0.02, patience=20, num_workers=0, verbose=1) -> self`
95
+
96
+ - `X` / `X_val`:`[N, G]` log1p 表达,numpy 或 torch 都行
97
+ - `z` / `z_val`:`[N, z_dim]`;`freeze_z=True` 时绝不会被修改或反传
98
+ - 给了验证集 → 早停 + 自动回滚最优权重
99
+
100
+ ### `model(z, *, steps=4, method="midpoint", deterministic=False) -> Tensor`
101
+
102
+ Flow 采样,`steps=4` 通常够用。
103
+
104
+ ### `model.predict(z) -> Tensor`
105
+
106
+ 确定性输出 `E[X]` 的 log1p(NB mean)。转伪计数:`(log_mu.exp() - 1).clamp(min=0)`。
107
+
108
+ ### `model.save(path)` / `GeneFlowDecoder.load(path, device="auto")`
109
+
110
+ ---
111
+
112
+ ## `FlowConfig`
113
+
114
+ ```python
115
+ from geneflowdecoder import FlowConfig
116
+
117
+ cfg = FlowConfig(
118
+ sigma_min=1e-4, # t 的截断,防 t≈0 数值爆炸
119
+ nb_weight=1.0, # NB 似然辅助损失权重
120
+ corr_weight=0.1, # cell-wise Pearson 相关性正则
121
+ )
122
+ model = GeneFlowDecoder(n_genes=G, z_dim=D, cfg=cfg)
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 发布
128
+
129
+ ```bash
130
+ python -m build # 生成 sdist + wheel 到 dist/
131
+ twine check dist/* # 校验元数据
132
+ twine upload dist/* # 发布到 PyPI(需账号 token)
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 目录结构
138
+
139
+ ```
140
+ geneflowdecoder/
141
+ ├── pyproject.toml
142
+ ├── README.md
143
+ ├── LICENSE
144
+ ├── src/geneflowdecoder/
145
+ │ ├── __init__.py # 公开 API
146
+ │ ├── _core.py # 模型 + fit/forward/predict
147
+ │ └── _version.py
148
+ └── tests/
149
+ └── test_api.py
150
+ ```
151
+
152
+ ## License
153
+
154
+ MIT
@@ -0,0 +1,8 @@
1
+ geneflowdecoder/__init__.py,sha256=-Dys4zK8iISGBhTJ26pblLVkyH2INURzo4d6PZ1VPI8,297
2
+ geneflowdecoder/_core.py,sha256=-k4rhbglkg4tizAupVKbM0UDxe8qk_dhlHQa1wUa5l0,19205
3
+ geneflowdecoder/_version.py,sha256=Ws32h9FRx9NsHSILqcoPUXVrRLtdy-rJtenOSdPkDa0,46
4
+ geneflowdecoder-0.1.0.dist-info/licenses/LICENSE,sha256=rqy_fOD8g0wCk7-2YZbAZBVoap9hAyiDE02Z2vW6vZM,1085
5
+ geneflowdecoder-0.1.0.dist-info/METADATA,sha256=WWWtjh-SXOl8lZo0JiSbj5z4_iho1tS05zKYwt8CF2M,4804
6
+ geneflowdecoder-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
7
+ geneflowdecoder-0.1.0.dist-info/top_level.txt,sha256=LHQ1s5kqyPdRBRkX28Z0rmHvsGGdNMtl3oPK7E4A31E,16
8
+ geneflowdecoder-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GeneFlowDecoder Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ geneflowdecoder