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 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.
@@ -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 @@
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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from ._inference import scUnveil
2
+
3
+ __all__ = ["scUnveil"]
@@ -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,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