scunveil 0.1.0__tar.gz
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.
- scunveil-0.1.0/LICENSE +21 -0
- scunveil-0.1.0/PKG-INFO +22 -0
- scunveil-0.1.0/README.md +1 -0
- scunveil-0.1.0/pyproject.toml +33 -0
- scunveil-0.1.0/setup.cfg +4 -0
- scunveil-0.1.0/src/scunveil/__init__.py +3 -0
- scunveil-0.1.0/src/scunveil/_data_operations.py +100 -0
- scunveil-0.1.0/src/scunveil/_inference.py +235 -0
- scunveil-0.1.0/src/scunveil/_layers.py +35 -0
- scunveil-0.1.0/src/scunveil/_model.py +71 -0
- scunveil-0.1.0/src/scunveil.egg-info/PKG-INFO +22 -0
- scunveil-0.1.0/src/scunveil.egg-info/SOURCES.txt +13 -0
- scunveil-0.1.0/src/scunveil.egg-info/dependency_links.txt +1 -0
- scunveil-0.1.0/src/scunveil.egg-info/requires.txt +13 -0
- scunveil-0.1.0/src/scunveil.egg-info/top_level.txt +1 -0
scunveil-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thonzyk
|
|
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.
|
scunveil-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scunveil
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Inference package for scUnveil single-cell embeddings and gene-expression prediction.
|
|
5
|
+
Author: Tomáš Honzík
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: <3.12,>=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: tensorflow==2.15.*
|
|
11
|
+
Requires-Dist: numpy<2,>=1.23.5
|
|
12
|
+
Requires-Dist: pandas<2.3,>=2.0
|
|
13
|
+
Requires-Dist: scipy<1.14,>=1.10
|
|
14
|
+
Requires-Dist: h5py<4,>=3.8
|
|
15
|
+
Requires-Dist: anndata<0.12,>=0.10
|
|
16
|
+
Requires-Dist: tqdm<5,>=4.66
|
|
17
|
+
Requires-Dist: huggingface_hub>=0.23
|
|
18
|
+
Provides-Extra: cuda
|
|
19
|
+
Requires-Dist: tensorflow[and-cuda]==2.15.*; (platform_system == "Linux" and platform_machine == "x86_64") and extra == "cuda"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# scUnveil
|
scunveil-0.1.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# scUnveil
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scunveil"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Inference package for scUnveil single-cell embeddings and gene-expression prediction."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9,<3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Tomáš Honzík" }
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"tensorflow==2.15.*",
|
|
18
|
+
"numpy>=1.23.5,<2",
|
|
19
|
+
"pandas>=2.0,<2.3",
|
|
20
|
+
"scipy>=1.10,<1.14",
|
|
21
|
+
"h5py>=3.8,<4",
|
|
22
|
+
"anndata>=0.10,<0.12",
|
|
23
|
+
"tqdm>=4.66,<5",
|
|
24
|
+
"huggingface_hub>=0.23"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
cuda = [
|
|
29
|
+
"tensorflow[and-cuda]==2.15.* ; platform_system == 'Linux' and platform_machine == 'x86_64'"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
scunveil-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import tensorflow as tf
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def logits_to_CPM(pred, add_oder_of_magnitude=6.0):
|
|
6
|
+
pred = tf.constant(pred)
|
|
7
|
+
pred = tf.cast(pred, 'float32')
|
|
8
|
+
pred = tf.nn.softmax(pred)
|
|
9
|
+
log10_pred = tf.math.log(pred) / tf.math.log(10.0)
|
|
10
|
+
pred = log10_pred + add_oder_of_magnitude
|
|
11
|
+
|
|
12
|
+
pred = pred.numpy()
|
|
13
|
+
|
|
14
|
+
return pred
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def simple_scipy_norm_x(x):
|
|
18
|
+
x = x.tocoo()
|
|
19
|
+
idx = np.stack([x.row, x.col], axis=1).astype(np.int64)
|
|
20
|
+
# counts as float32 (required by stateless_binomial)
|
|
21
|
+
shape = np.array(x.shape, dtype=np.int64)
|
|
22
|
+
|
|
23
|
+
vals = tf.constant(x.data.astype(np.float16))
|
|
24
|
+
vals = tf.math.log1p(vals)
|
|
25
|
+
|
|
26
|
+
sp = tf.sparse.SparseTensor(idx, vals, shape)
|
|
27
|
+
sp = tf.sparse.reorder(sp)
|
|
28
|
+
|
|
29
|
+
x_tf_dense = tf.sparse.to_dense(sp)
|
|
30
|
+
return x_tf_dense
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def pretrain_batch_from_x_tf(x_batch, variable_dilution=False):
|
|
34
|
+
# scipy sparse -> TF SparseTensor (COO)
|
|
35
|
+
x = x_batch.tocoo()
|
|
36
|
+
idx = np.stack([x.row, x.col], axis=1).astype(np.int64)
|
|
37
|
+
vals = x.data.astype(np.float32) # counts as float32 (required by stateless_binomial)
|
|
38
|
+
shape = np.array(x.shape, dtype=np.int64)
|
|
39
|
+
|
|
40
|
+
sp = tf.sparse.SparseTensor(idx, vals, shape)
|
|
41
|
+
sp = tf.sparse.reorder(sp)
|
|
42
|
+
|
|
43
|
+
B = tf.cast(sp.dense_shape[0], tf.int32)
|
|
44
|
+
|
|
45
|
+
# dilution and keep-prob
|
|
46
|
+
if variable_dilution:
|
|
47
|
+
dilution = tf.random.uniform([B, 1], minval=0.1, maxval=1.0, dtype=tf.float32)
|
|
48
|
+
p = 1.0 - tf.squeeze(dilution, axis=1) # [B]
|
|
49
|
+
|
|
50
|
+
# binomial thinning ONLY on nnz
|
|
51
|
+
row = tf.cast(sp.indices[:, 0], tf.int32)
|
|
52
|
+
p_vals = tf.gather(p, row)
|
|
53
|
+
else:
|
|
54
|
+
p_vals = 0.5
|
|
55
|
+
|
|
56
|
+
seed = tf.random.uniform([2], maxval=2**31 - 1, dtype=tf.int32)
|
|
57
|
+
diluted_i = tf.random.stateless_binomial(
|
|
58
|
+
shape=tf.shape(sp.values),
|
|
59
|
+
seed=seed,
|
|
60
|
+
counts=sp.values, # float32
|
|
61
|
+
probs=p_vals, # float32
|
|
62
|
+
output_dtype=tf.int32
|
|
63
|
+
)
|
|
64
|
+
diluted = tf.cast(diluted_i, tf.float32)
|
|
65
|
+
|
|
66
|
+
# x_diluted sparse (drop zeros)
|
|
67
|
+
keep = diluted > 0.0
|
|
68
|
+
x_dil_sp = tf.sparse.reorder(tf.sparse.SparseTensor(
|
|
69
|
+
indices=tf.boolean_mask(sp.indices, keep),
|
|
70
|
+
values=tf.boolean_mask(diluted, keep),
|
|
71
|
+
dense_shape=sp.dense_shape
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
# complement on original support
|
|
75
|
+
comp_vals = sp.values - diluted
|
|
76
|
+
comp_sp = tf.sparse.SparseTensor(sp.indices, comp_vals, sp.dense_shape)
|
|
77
|
+
|
|
78
|
+
# y = complement / row-sum (sparse -> dense)
|
|
79
|
+
row2 = tf.cast(comp_sp.indices[:, 0], tf.int32)
|
|
80
|
+
y_sum = tf.math.unsorted_segment_sum(comp_sp.values, row2, num_segments=B) # [B]
|
|
81
|
+
valid_y = y_sum > 0.0
|
|
82
|
+
y_vals = tf.math.divide_no_nan(comp_sp.values, tf.gather(y_sum, row2))
|
|
83
|
+
y_sp = tf.sparse.reorder(tf.sparse.SparseTensor(comp_sp.indices, y_vals, comp_sp.dense_shape))
|
|
84
|
+
|
|
85
|
+
# log1p norm
|
|
86
|
+
x_dil_sp = tf.SparseTensor(
|
|
87
|
+
indices=x_dil_sp.indices,
|
|
88
|
+
values=tf.cast(tf.math.log1p(x_dil_sp.values), tf.float16),
|
|
89
|
+
dense_shape=x_dil_sp.dense_shape
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
x_diluted = tf.sparse.to_dense(x_dil_sp)
|
|
93
|
+
y = tf.sparse.to_dense(y_sp)
|
|
94
|
+
|
|
95
|
+
# Replace zero-target rows with uniform labels.
|
|
96
|
+
G = tf.cast(tf.shape(y)[1], y.dtype)
|
|
97
|
+
uniform_value = tf.cast(1.0, y.dtype) / G
|
|
98
|
+
y = y + tf.cast(~valid_y[:, None], y.dtype) * uniform_value
|
|
99
|
+
|
|
100
|
+
return x_diluted, y
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
# from src.scunveil.model import RNABagModel
|
|
5
|
+
# from src.scunveil.layers import PCAProjection
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import tensorflow as tf
|
|
8
|
+
import numpy as np
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
from scipy.sparse import csc_matrix
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from huggingface_hub import snapshot_download
|
|
13
|
+
|
|
14
|
+
from ._model import RNABagModel
|
|
15
|
+
from ._layers import PCAProjection
|
|
16
|
+
from ._data_operations import simple_scipy_norm_x, logits_to_CPM
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SCUNVEIL_MODEL_REPO = "thonzik/sc-unveil"
|
|
20
|
+
|
|
21
|
+
NO_INPUT_ANNDATA_TEXT = 'No Input AnnData found, please run first "set_input_anndata(...)".'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@tf.function
|
|
25
|
+
def run_tf_model_pred(tf_model, x_input):
|
|
26
|
+
return tf_model(x_input, training=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class scUnveil:
|
|
30
|
+
def __init__(self):
|
|
31
|
+
"""
|
|
32
|
+
reference_var_path: is used for maping model gene permutation onto input samples gene permutation
|
|
33
|
+
"""
|
|
34
|
+
self.input_anndata = None
|
|
35
|
+
|
|
36
|
+
checkpoint_path = snapshot_download(
|
|
37
|
+
repo_id=SCUNVEIL_MODEL_REPO,
|
|
38
|
+
repo_type="model",
|
|
39
|
+
allow_patterns=[
|
|
40
|
+
"var_sorted.csv",
|
|
41
|
+
"config.json",
|
|
42
|
+
"weights.h5",
|
|
43
|
+
"pca_mean.npy",
|
|
44
|
+
"pca_mat.npy",
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
checkpoint_path = Path(checkpoint_path)
|
|
48
|
+
self.checkpoint_path = checkpoint_path
|
|
49
|
+
|
|
50
|
+
self.reference_var = pd.read_csv(checkpoint_path / 'var_sorted.csv')
|
|
51
|
+
|
|
52
|
+
with open(checkpoint_path / 'config.json', 'r', encoding='utf-8') as fr:
|
|
53
|
+
CONFIG = json.load(fr)
|
|
54
|
+
CF = SimpleNamespace(**CONFIG)
|
|
55
|
+
|
|
56
|
+
self.config = CF
|
|
57
|
+
|
|
58
|
+
print('Model Initialization...')
|
|
59
|
+
m = RNABagModel(n_vars=CF.n_genes, n_layers=CF.n_layers, emb_dim=CF.emb_dim)
|
|
60
|
+
print('Loading Weights...')
|
|
61
|
+
m.model.load_weights(checkpoint_path / 'weights.h5')
|
|
62
|
+
|
|
63
|
+
self.raw_embedder = tf.keras.Model(
|
|
64
|
+
inputs=m.model.input,
|
|
65
|
+
outputs=m.model.layers[-2].output
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.pca_mean = np.load(checkpoint_path / 'pca_mean.npy').astype(np.float32)[None, :]
|
|
69
|
+
self.pca_mat = np.load(checkpoint_path / f'pca_mat.npy').astype(np.float32)
|
|
70
|
+
|
|
71
|
+
self.pca_projector = tf.keras.Sequential([PCAProjection()])
|
|
72
|
+
self.pca_projector.build((None, CF.emb_dim))
|
|
73
|
+
self.pca_projector.set_weights([self.pca_mean, self.pca_mat])
|
|
74
|
+
|
|
75
|
+
self.expression_predictor = tf.keras.Sequential([m.model.layers[-1]])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def set_input_anndata(self, input_anndata, batch_size=32):
|
|
79
|
+
self.input_anndata = input_anndata
|
|
80
|
+
self._process_anndata(batch_size=batch_size)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _process_anndata(self, batch_size):
|
|
84
|
+
self._calculate_gene_sort()
|
|
85
|
+
self._calculate_embeddings(batch_size)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _detect_gene_column(self):
|
|
89
|
+
"""Scan .var index + columns to find which holds gene names or Ensembl IDs."""
|
|
90
|
+
ref_names = set(list(self.reference_var['feature_name'])[:100])
|
|
91
|
+
ref_ids = set(list(self.reference_var['feature_id'])[:100])
|
|
92
|
+
|
|
93
|
+
candidates = {"__index__": self.input_anndata.var.index.astype(str)}
|
|
94
|
+
for col in self.input_anndata.var.columns:
|
|
95
|
+
candidates[col] = self.input_anndata.var[col].astype(str)
|
|
96
|
+
|
|
97
|
+
best_col, best_score, best_type = None, 0, None
|
|
98
|
+
for name, values in candidates.items():
|
|
99
|
+
vals = set(values)
|
|
100
|
+
for ref_set, id_type in [(ref_names, "feature_name"), (ref_ids, "feature_id")]:
|
|
101
|
+
overlap = len(vals & ref_set)
|
|
102
|
+
if overlap > best_score:
|
|
103
|
+
best_col, best_score, best_type = name, overlap, id_type
|
|
104
|
+
|
|
105
|
+
if best_score < 10:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Could not find gene identifiers in input .var "
|
|
108
|
+
f"(best match: column='{best_col}', overlap={best_score}/100). "
|
|
109
|
+
f"Ensure .var.index or a .var column contains gene symbols "
|
|
110
|
+
f"(e.g. TP53, CD3D) or Ensembl IDs (e.g. ENSG00000141510)."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
vals = self.input_anndata.var.index.astype(str) if best_col == "__index__" else self.input_anndata.var[best_col].astype(str)
|
|
114
|
+
ref_col = self.reference_var[best_type]
|
|
115
|
+
return list(vals), {name: i for i, name in enumerate(ref_col[:self.config.n_genes])}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _calculate_gene_sort(self):
|
|
119
|
+
if self.input_anndata is None:
|
|
120
|
+
print(NO_INPUT_ANNDATA_TEXT)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
print('Mapping gene permutation...')
|
|
124
|
+
|
|
125
|
+
this_ad_vars, ref_var_lookup = self._detect_gene_column()
|
|
126
|
+
|
|
127
|
+
n_this_ad_vars = len(this_ad_vars)
|
|
128
|
+
|
|
129
|
+
this_indices = []
|
|
130
|
+
reference_indices = []
|
|
131
|
+
|
|
132
|
+
for this_var_i, this_var_name in enumerate(this_ad_vars):
|
|
133
|
+
if this_var_name not in ref_var_lookup:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
ref_var_i = ref_var_lookup[this_var_name]
|
|
137
|
+
|
|
138
|
+
reference_indices.append(ref_var_i)
|
|
139
|
+
this_indices.append(this_var_i)
|
|
140
|
+
|
|
141
|
+
this_indices = np.asarray(this_indices, dtype=np.int64)
|
|
142
|
+
reference_indices = np.asarray(reference_indices, dtype=np.int64)
|
|
143
|
+
|
|
144
|
+
arr_of_ones = np.ones(len(this_indices), dtype=np.float32)
|
|
145
|
+
self.var_map_matrix = csc_matrix((arr_of_ones, (this_indices, reference_indices)),
|
|
146
|
+
shape=(n_this_ad_vars, self.config.n_genes))
|
|
147
|
+
|
|
148
|
+
assert self.var_map_matrix.max() < 1.5, "Duplicate gene mapping detected in var_map_matrix"
|
|
149
|
+
|
|
150
|
+
def _calculate_embeddings(self, batch_size):
|
|
151
|
+
print('Processing cells...')
|
|
152
|
+
|
|
153
|
+
n_cells_to_process = self.input_anndata.X.shape[0]
|
|
154
|
+
|
|
155
|
+
self.raw_embeddings = np.zeros((n_cells_to_process, self.config.emb_dim), dtype=np.float16)
|
|
156
|
+
self.pca_embeddings = np.zeros((n_cells_to_process, self.config.emb_dim), dtype=np.float16)
|
|
157
|
+
|
|
158
|
+
pbar = tqdm(total=n_cells_to_process)
|
|
159
|
+
|
|
160
|
+
for i in range(0, n_cells_to_process, batch_size):
|
|
161
|
+
x = self.input_anndata.X[i:i+batch_size].copy()
|
|
162
|
+
|
|
163
|
+
x = x @ self.var_map_matrix # still sparse, shape (n_rows, n_genes)
|
|
164
|
+
|
|
165
|
+
x = simple_scipy_norm_x(x)
|
|
166
|
+
|
|
167
|
+
n_batch = x.shape[0]
|
|
168
|
+
|
|
169
|
+
this_raw_emb = run_tf_model_pred(self.raw_embedder, x)
|
|
170
|
+
self.raw_embeddings[i:i+n_batch] = this_raw_emb.numpy()
|
|
171
|
+
|
|
172
|
+
this_pca_emb = run_tf_model_pred(self.pca_projector, this_raw_emb)
|
|
173
|
+
self.pca_embeddings[i:i+n_batch] = this_pca_emb.numpy()
|
|
174
|
+
|
|
175
|
+
pbar.update(n_batch)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_raw_embeddings(self):
|
|
179
|
+
return self.raw_embeddings.copy()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_embeddings(self, n_features=512):
|
|
183
|
+
"""
|
|
184
|
+
if n_features is None return full raw embeddings, if integer, top_n_features from PCA projection is returned instead
|
|
185
|
+
"""
|
|
186
|
+
if self.input_anndata is None:
|
|
187
|
+
print(NO_INPUT_ANNDATA_TEXT)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
if n_features is None:
|
|
191
|
+
return self.pca_embeddings.copy()
|
|
192
|
+
|
|
193
|
+
assert n_features <= self.config.emb_dim
|
|
194
|
+
|
|
195
|
+
return self.pca_embeddings[:, :n_features].copy()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_all_genes_prediction(self, batch_size=128):
|
|
199
|
+
if self.input_anndata is None:
|
|
200
|
+
print(NO_INPUT_ANNDATA_TEXT)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
n_cells_to_process = self.raw_embeddings.shape[0]
|
|
204
|
+
gene_expressions = np.zeros((n_cells_to_process, self.config.n_genes), dtype=np.float16)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
pbar = tqdm(total=n_cells_to_process)
|
|
208
|
+
|
|
209
|
+
for i in range(0, n_cells_to_process, batch_size):
|
|
210
|
+
this_raw_emb = self.raw_embeddings[i:i+batch_size].copy()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
this_prediction = run_tf_model_pred(self.expression_predictor, this_raw_emb)
|
|
214
|
+
|
|
215
|
+
this_prediction = logits_to_CPM(this_prediction)
|
|
216
|
+
|
|
217
|
+
gene_expressions[i:i+batch_size] = this_prediction
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
pbar.update(this_raw_emb.shape[0])
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
return gene_expressions
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_specific_genes_prediction(self, list_of_gene_names):
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_genes_embeddings(self):
|
|
231
|
+
if self.input_anndata is None:
|
|
232
|
+
print(NO_INPUT_ANNDATA_TEXT)
|
|
233
|
+
return
|
|
234
|
+
pass
|
|
235
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import tensorflow as tf
|
|
2
|
+
|
|
3
|
+
class GatedSkipAdd(tf.keras.layers.Layer):
|
|
4
|
+
def __init__(self, alpha_init=0.0, eps=1e-6, **kwargs):
|
|
5
|
+
super().__init__(**kwargs)
|
|
6
|
+
self.alpha_init = alpha_init
|
|
7
|
+
self.eps = eps
|
|
8
|
+
|
|
9
|
+
def build(self, input_shape):
|
|
10
|
+
self.alpha = self.add_weight(
|
|
11
|
+
name="alpha",
|
|
12
|
+
shape=(),
|
|
13
|
+
initializer=tf.keras.initializers.Constant(self.alpha_init),
|
|
14
|
+
trainable=True,
|
|
15
|
+
)
|
|
16
|
+
super().build(input_shape)
|
|
17
|
+
|
|
18
|
+
def call(self, inputs):
|
|
19
|
+
x, skip = inputs
|
|
20
|
+
a = self.alpha
|
|
21
|
+
|
|
22
|
+
y = x + a * skip
|
|
23
|
+
|
|
24
|
+
denom = tf.sqrt(1.0 + tf.square(a) + self.eps)
|
|
25
|
+
return y / denom
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PCAProjection(tf.keras.layers.Layer):
|
|
29
|
+
def build(self, input_shape):
|
|
30
|
+
n = input_shape[-1]
|
|
31
|
+
self.mean = self.add_weight("mean", (1, n), trainable=False)
|
|
32
|
+
self.pca = self.add_weight("pca", (n, n), trainable=False)
|
|
33
|
+
|
|
34
|
+
def call(self, x):
|
|
35
|
+
return (x - self.mean) @ self.pca
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import tensorflow as tf
|
|
2
|
+
from tensorflow.keras.layers import Dense, Input, LayerNormalization, Add
|
|
3
|
+
from tensorflow.keras.models import Model
|
|
4
|
+
from tensorflow.keras.optimizers import AdamW
|
|
5
|
+
from tensorflow.keras.losses import categorical_crossentropy
|
|
6
|
+
|
|
7
|
+
# from src.scunveil.layers import GatedSkipAdd
|
|
8
|
+
from ._layers import GatedSkipAdd
|
|
9
|
+
|
|
10
|
+
def c_xent(y_true, y_pred):
|
|
11
|
+
losss = categorical_crossentropy(tf.cast(y_true, 'float32'), tf.cast(y_pred, 'float32'), from_logits=True)
|
|
12
|
+
return tf.reduce_mean(losss)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RNABagModel:
|
|
16
|
+
def __init__(self, n_vars, n_layers, emb_dim, ff_dim=None, activation='relu'):
|
|
17
|
+
assert n_layers >= 2
|
|
18
|
+
if ff_dim is None:
|
|
19
|
+
ff_dim = emb_dim * 4
|
|
20
|
+
self.n_vars = n_vars
|
|
21
|
+
self.n_layers = n_layers
|
|
22
|
+
self.emb_dim = emb_dim
|
|
23
|
+
|
|
24
|
+
input_x = Input(shape=(n_vars,), name="x")
|
|
25
|
+
|
|
26
|
+
emb_x = Dense(emb_dim, use_bias=True, name="emb_0")(input_x)
|
|
27
|
+
|
|
28
|
+
x = emb_x
|
|
29
|
+
u_net_residuals = []
|
|
30
|
+
|
|
31
|
+
half = n_layers // 2
|
|
32
|
+
|
|
33
|
+
for layer_i in range(n_layers):
|
|
34
|
+
if layer_i < half:
|
|
35
|
+
u_net_residuals.append(x)
|
|
36
|
+
|
|
37
|
+
if layer_i >= n_layers - half:
|
|
38
|
+
skip = u_net_residuals.pop()
|
|
39
|
+
x = GatedSkipAdd()([x, skip])
|
|
40
|
+
|
|
41
|
+
x_add = LayerNormalization(epsilon=1e-6)(x)
|
|
42
|
+
x_add = Dense(ff_dim, activation=activation)(x_add)
|
|
43
|
+
x_add = Dense(emb_dim)(x_add)
|
|
44
|
+
|
|
45
|
+
x = Add(name=f"emb_{layer_i+1}")([x, x_add])
|
|
46
|
+
|
|
47
|
+
output_layer = Dense(n_vars, name="out")(x)
|
|
48
|
+
|
|
49
|
+
inputs = input_x
|
|
50
|
+
|
|
51
|
+
self.model = Model(
|
|
52
|
+
inputs=inputs,
|
|
53
|
+
outputs=output_layer
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def compile_pretrain(self, lr=1e-3, wd=0.0, clipnorm=None):
|
|
58
|
+
optimizer = AdamW(
|
|
59
|
+
learning_rate=lr,
|
|
60
|
+
weight_decay=wd,
|
|
61
|
+
clipnorm=clipnorm,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self.model.compile(
|
|
65
|
+
optimizer=optimizer,
|
|
66
|
+
loss=c_xent,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
print(f'Number of parameters: {self.model.count_params():,}')
|
|
70
|
+
|
|
71
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scunveil
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Inference package for scUnveil single-cell embeddings and gene-expression prediction.
|
|
5
|
+
Author: Tomáš Honzík
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: <3.12,>=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: tensorflow==2.15.*
|
|
11
|
+
Requires-Dist: numpy<2,>=1.23.5
|
|
12
|
+
Requires-Dist: pandas<2.3,>=2.0
|
|
13
|
+
Requires-Dist: scipy<1.14,>=1.10
|
|
14
|
+
Requires-Dist: h5py<4,>=3.8
|
|
15
|
+
Requires-Dist: anndata<0.12,>=0.10
|
|
16
|
+
Requires-Dist: tqdm<5,>=4.66
|
|
17
|
+
Requires-Dist: huggingface_hub>=0.23
|
|
18
|
+
Provides-Extra: cuda
|
|
19
|
+
Requires-Dist: tensorflow[and-cuda]==2.15.*; (platform_system == "Linux" and platform_machine == "x86_64") and extra == "cuda"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# scUnveil
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/scunveil/__init__.py
|
|
5
|
+
src/scunveil/_data_operations.py
|
|
6
|
+
src/scunveil/_inference.py
|
|
7
|
+
src/scunveil/_layers.py
|
|
8
|
+
src/scunveil/_model.py
|
|
9
|
+
src/scunveil.egg-info/PKG-INFO
|
|
10
|
+
src/scunveil.egg-info/SOURCES.txt
|
|
11
|
+
src/scunveil.egg-info/dependency_links.txt
|
|
12
|
+
src/scunveil.egg-info/requires.txt
|
|
13
|
+
src/scunveil.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
tensorflow==2.15.*
|
|
2
|
+
numpy<2,>=1.23.5
|
|
3
|
+
pandas<2.3,>=2.0
|
|
4
|
+
scipy<1.14,>=1.10
|
|
5
|
+
h5py<4,>=3.8
|
|
6
|
+
anndata<0.12,>=0.10
|
|
7
|
+
tqdm<5,>=4.66
|
|
8
|
+
huggingface_hub>=0.23
|
|
9
|
+
|
|
10
|
+
[cuda]
|
|
11
|
+
|
|
12
|
+
[cuda:platform_system == "Linux" and platform_machine == "x86_64"]
|
|
13
|
+
tensorflow[and-cuda]==2.15.*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scunveil
|