bowside 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.
- bowside/__init__.py +6 -0
- bowside/dataset.py +535 -0
- bowside/mixture.py +408 -0
- bowside/mixture_claude.py +378 -0
- bowside/model.py +275 -0
- bowside/network.py +448 -0
- bowside/sampling.py +257 -0
- bowside/sensitivity.py +289 -0
- bowside/training.py +986 -0
- bowside/trajectory.py +327 -0
- bowside/utils.py +75 -0
- bowside-0.1.0.dist-info/METADATA +92 -0
- bowside-0.1.0.dist-info/RECORD +15 -0
- bowside-0.1.0.dist-info/WHEEL +4 -0
- bowside-0.1.0.dist-info/licenses/LICENSE +674 -0
bowside/__init__.py
ADDED
bowside/dataset.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"""Module for datasets to be used to train an ODE GRN model."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from math import floor
|
|
5
|
+
from typing import Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
import anndata as ad
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import torch
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
from scipy import sparse
|
|
13
|
+
from torch.utils.data import Dataset
|
|
14
|
+
|
|
15
|
+
from bowside.model import BifurcatingSDE
|
|
16
|
+
from bowside.sampling import MarkovSampler
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MarkovChainDataset(Dataset):
|
|
20
|
+
"""
|
|
21
|
+
A pytorch dataset to generate Markov chain cell sequences, ready for
|
|
22
|
+
training an ODE or SDE model.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
adata : ad.AnnData
|
|
27
|
+
AnnData object.
|
|
28
|
+
layer : Optional[str], optional
|
|
29
|
+
Layer of the AnnData object to use as the gene expression values.
|
|
30
|
+
If `None`, use adata.X. Default: None.
|
|
31
|
+
mask_var : Optional[Union[NDArray[np.bool_], str]], optional
|
|
32
|
+
Only use a subset of genes given by a boolean array or a string
|
|
33
|
+
referring to a column in `adata.var`. Default: "highly_variable".
|
|
34
|
+
When set to None, use all genes.
|
|
35
|
+
tpm_key : str, optional
|
|
36
|
+
Key of the transition probability matrix in `adata.obsp` or
|
|
37
|
+
`adata.uns`. Default: "T_forward".
|
|
38
|
+
recompute_matrix : bool, optional
|
|
39
|
+
Whether the matrix should be recomputed. Default: False.
|
|
40
|
+
self_transitions : bool, optional
|
|
41
|
+
Whether the matrix should be computed with self-transitions
|
|
42
|
+
enabled. Default: False.
|
|
43
|
+
num_chains : int, optional
|
|
44
|
+
Number of Markov chains to simulate. Default: 1,000.
|
|
45
|
+
max_iter : int, optional
|
|
46
|
+
Maximum number of iterations for Markov chains. Default: 1,000.
|
|
47
|
+
rtol : float, optional
|
|
48
|
+
Relative tolerance for convergence. Default: 1e-5.
|
|
49
|
+
atol : float, optional
|
|
50
|
+
Absolute tolerance for convergence. Default: 1e-5.
|
|
51
|
+
n_jobs : int, optional
|
|
52
|
+
Number of threads used for Markov sampling when running on CPU.
|
|
53
|
+
Default: -1 (all available threads).
|
|
54
|
+
seed : int, optional
|
|
55
|
+
Random seed. Default: 42.
|
|
56
|
+
copy : bool, optional
|
|
57
|
+
Whether the AnnData object should be copied or modified inplace.
|
|
58
|
+
Default: False (modify inplace).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
adata: ad.AnnData
|
|
62
|
+
"""AnnData object for sampling cell sequences and getting gene expression."""
|
|
63
|
+
X: torch.Tensor
|
|
64
|
+
"""Gene expression tensor of shape (n_cells, n_genes)."""
|
|
65
|
+
sampler: MarkovSampler
|
|
66
|
+
"""Instance of Markov chain sampler for sampling cell sequences."""
|
|
67
|
+
markov_chains: torch.Tensor
|
|
68
|
+
"""Markov chains of cell sequences, represented by their index."""
|
|
69
|
+
gene_names: pd.Index
|
|
70
|
+
"""Gene names index of shape (n_genes,)."""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
adata: ad.AnnData,
|
|
75
|
+
layer: Optional[str] = None,
|
|
76
|
+
mask_var: Optional[Union[NDArray[np.bool_], str]] = "highly_variable",
|
|
77
|
+
tpm_key: str = "T_forward",
|
|
78
|
+
recompute_matrix: bool = False,
|
|
79
|
+
self_transitions: bool = False,
|
|
80
|
+
num_chains: int = 1_000,
|
|
81
|
+
max_iter: int = 1_000,
|
|
82
|
+
rtol: float = 1e-5,
|
|
83
|
+
atol: float = 1e-5,
|
|
84
|
+
n_jobs: int = -1,
|
|
85
|
+
seed: int = 42,
|
|
86
|
+
copy: bool = False,
|
|
87
|
+
):
|
|
88
|
+
# Prepare expression data
|
|
89
|
+
self.adata = adata.copy() if copy else adata
|
|
90
|
+
X = self.adata.X if layer is None else self.adata.layers[layer]
|
|
91
|
+
if X is None:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
"Invalid gene expression matrix. Make sure that adata.X is present."
|
|
94
|
+
)
|
|
95
|
+
self.gene_names = self.adata.var_names
|
|
96
|
+
if mask_var is not None:
|
|
97
|
+
if isinstance(mask_var, str):
|
|
98
|
+
if adata.var[mask_var].dtype != bool:
|
|
99
|
+
raise TypeError(
|
|
100
|
+
f"Expected adata.var[mask_var] to be of type bool, but got type {adata.var[mask_var].dtype}"
|
|
101
|
+
)
|
|
102
|
+
mask_var = self.adata.var[mask_var].to_numpy()
|
|
103
|
+
X = X[:, mask_var]
|
|
104
|
+
self.gene_names = self.gene_names[mask_var]
|
|
105
|
+
if isinstance(
|
|
106
|
+
X,
|
|
107
|
+
(sparse.csc_array, sparse.csr_array, sparse.csc_matrix, sparse.csr_matrix),
|
|
108
|
+
):
|
|
109
|
+
X = X.toarray()
|
|
110
|
+
self.X = torch.from_numpy(X)
|
|
111
|
+
|
|
112
|
+
# Sample Markov chains
|
|
113
|
+
self.sampler = MarkovSampler(
|
|
114
|
+
adata=adata,
|
|
115
|
+
tpm_key=tpm_key,
|
|
116
|
+
recompute_matrix=recompute_matrix,
|
|
117
|
+
self_transitions=self_transitions,
|
|
118
|
+
copy=False,
|
|
119
|
+
)
|
|
120
|
+
self.sampler.sample_markov_chains(
|
|
121
|
+
num_chains=num_chains,
|
|
122
|
+
max_iter=max_iter,
|
|
123
|
+
rtol=rtol,
|
|
124
|
+
atol=atol,
|
|
125
|
+
n_jobs=n_jobs,
|
|
126
|
+
seed=seed,
|
|
127
|
+
)
|
|
128
|
+
self.markov_chains = torch.from_numpy(
|
|
129
|
+
self.adata.uns["markov_sampling"]["cell_sequences"]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def __len__(self) -> int:
|
|
133
|
+
return self.markov_chains.shape[0]
|
|
134
|
+
|
|
135
|
+
def __getitem__(self, idx) -> torch.Tensor:
|
|
136
|
+
return self.X[self.markov_chains[idx, :], :]
|
|
137
|
+
|
|
138
|
+
def n_genes(self) -> int:
|
|
139
|
+
"""
|
|
140
|
+
Get the number of genes in the dataset.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
int
|
|
145
|
+
Number of genes.
|
|
146
|
+
"""
|
|
147
|
+
return len(self.gene_names)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class PseudotimeDataset(Dataset):
|
|
151
|
+
"""
|
|
152
|
+
A pytorch dataset to sample cells along (pseudo) time, ready for
|
|
153
|
+
training an ODE or SDE model with a distributional loss.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
adata : ad.AnnData
|
|
158
|
+
AnnData object.
|
|
159
|
+
layer : Optional[str], optional
|
|
160
|
+
Layer of the AnnData object to use as the gene expression values.
|
|
161
|
+
If `None`, use adata.X. Default: None.
|
|
162
|
+
mask_var : Optional[Union[NDArray[np.bool_], str]], optional
|
|
163
|
+
Only use a subset of genes given by a boolean array or a string
|
|
164
|
+
referring to a column in `adata.var`. Default: "highly_variable".
|
|
165
|
+
When set to None, use all genes.
|
|
166
|
+
pseudotime_key : str, optional
|
|
167
|
+
Key of the (pseudo) time annotation in `adata.obs`. Default:
|
|
168
|
+
"pseudotime".
|
|
169
|
+
num_cells : int, optional
|
|
170
|
+
Number of cells to sample for each time point. Default: 1,000.
|
|
171
|
+
num_quantiles : int, optional
|
|
172
|
+
Number of quantiles for generating for sampling cells along
|
|
173
|
+
(pseudo) time. Default: 20.
|
|
174
|
+
time_points : Optional[Union[NDArray[np.bool_], str]], option
|
|
175
|
+
Explicit time points to use for sampling cells. Overrides
|
|
176
|
+
`num_quantiles`. Default: None (use quantiles along pseudottime).
|
|
177
|
+
seed : int, optional
|
|
178
|
+
Random seed. Default: 42.
|
|
179
|
+
copy : bool, optional
|
|
180
|
+
Whether the AnnData object should be copied or modified inplace.
|
|
181
|
+
Default: False (modify inplace).
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
adata: ad.AnnData
|
|
185
|
+
"""AnnData object for sampling cell sequences and getting gene expression."""
|
|
186
|
+
X: torch.Tensor
|
|
187
|
+
"""Gene expression tensor of shape (n_cells, n_genes)."""
|
|
188
|
+
sampled_cells: torch.Tensor
|
|
189
|
+
"""Sampled cell indices of shape (num_cells, len(self.time_points))."""
|
|
190
|
+
gene_names: pd.Index
|
|
191
|
+
"""Gene names index of shape (n_genes,)."""
|
|
192
|
+
pseudotime: torch.Tensor
|
|
193
|
+
"""Tensor of shape (n_cells,) with pseudotime of each cell in `adata`."""
|
|
194
|
+
time_points: torch.Tensor
|
|
195
|
+
"""Tensor of shape (num_quantiles,) with the quantile means."""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
adata: ad.AnnData,
|
|
200
|
+
layer: Optional[str] = None,
|
|
201
|
+
mask_var: Optional[Union[NDArray[np.bool_], str]] = "highly_variable",
|
|
202
|
+
pseudotime_key: str = "pseudotime",
|
|
203
|
+
num_cells: int = 1_000,
|
|
204
|
+
num_quantiles: int = 20,
|
|
205
|
+
time_points: Optional[Sequence[Union[int, float]]] = None,
|
|
206
|
+
seed: int = 42,
|
|
207
|
+
copy: bool = False,
|
|
208
|
+
):
|
|
209
|
+
if pseudotime_key not in adata.obs:
|
|
210
|
+
raise KeyError("Pseudotime key not found in adata.obs.")
|
|
211
|
+
# Prepare expression data
|
|
212
|
+
self.adata = adata.copy() if copy else adata
|
|
213
|
+
X = self.adata.X if layer is None else self.adata.layers[layer]
|
|
214
|
+
if X is None:
|
|
215
|
+
raise ValueError(
|
|
216
|
+
"Invalid gene expression matrix. Make sure that adata.X is present."
|
|
217
|
+
)
|
|
218
|
+
self.gene_names = self.adata.var_names
|
|
219
|
+
if mask_var is not None:
|
|
220
|
+
if isinstance(mask_var, str):
|
|
221
|
+
if adata.var[mask_var].dtype != bool:
|
|
222
|
+
raise TypeError(
|
|
223
|
+
f"Expected adata.var[mask_var] to be of type bool, but got type {adata.var[mask_var].dtype}"
|
|
224
|
+
)
|
|
225
|
+
mask_var = self.adata.var[mask_var].to_numpy()
|
|
226
|
+
X = X[:, mask_var]
|
|
227
|
+
self.gene_names = self.gene_names[mask_var]
|
|
228
|
+
if isinstance(
|
|
229
|
+
X,
|
|
230
|
+
(sparse.csc_array, sparse.csr_array, sparse.csc_matrix, sparse.csr_matrix),
|
|
231
|
+
):
|
|
232
|
+
X = X.toarray()
|
|
233
|
+
self.X = torch.from_numpy(X)
|
|
234
|
+
|
|
235
|
+
# Sample cells at time points
|
|
236
|
+
pseudotime: pd.Series = adata.obs[pseudotime_key]
|
|
237
|
+
if time_points is None:
|
|
238
|
+
# Use quantile center as bin value
|
|
239
|
+
pseudotime = pd.qcut(pseudotime, num_quantiles).apply(
|
|
240
|
+
lambda x: (x.left + x.right) / 2
|
|
241
|
+
)
|
|
242
|
+
time_points = sorted(pseudotime.cat.categories)
|
|
243
|
+
self.pseudotime = torch.from_numpy(pseudotime.to_numpy())
|
|
244
|
+
self.time_points = torch.from_numpy(np.asarray(time_points))
|
|
245
|
+
self.sampled_cells = torch.zeros(
|
|
246
|
+
num_cells, len(self.time_points), dtype=torch.int
|
|
247
|
+
)
|
|
248
|
+
integer_idx = torch.arange(len(self.adata))
|
|
249
|
+
generator = torch.Generator()
|
|
250
|
+
generator.manual_seed(seed)
|
|
251
|
+
for i, tp in enumerate(time_points):
|
|
252
|
+
tp_idx = integer_idx[self.pseudotime == tp]
|
|
253
|
+
self.sampled_cells[:, i] = tp_idx[
|
|
254
|
+
torch.randint(len(tp_idx), (num_cells,), generator=generator)
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
def __len__(self) -> int:
|
|
258
|
+
return self.sampled_cells.shape[0]
|
|
259
|
+
|
|
260
|
+
def __getitem__(self, idx) -> torch.Tensor:
|
|
261
|
+
return self.X[self.sampled_cells[idx, :], :]
|
|
262
|
+
|
|
263
|
+
def n_genes(self) -> int:
|
|
264
|
+
"""
|
|
265
|
+
Get the number of genes in the dataset.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
int
|
|
270
|
+
Number of genes.
|
|
271
|
+
"""
|
|
272
|
+
return len(self.gene_names)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class VelocityDataset(Dataset):
|
|
276
|
+
"""
|
|
277
|
+
A pytorch dataset containing spliced expression and RNA velocity,
|
|
278
|
+
ready for training an ODE model with gradient matching.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
adata : ad.AnnData
|
|
283
|
+
AnnData object.
|
|
284
|
+
layer : str, optional
|
|
285
|
+
Layer of the AnnData object to use as spliced expression values.
|
|
286
|
+
By default "Ms".
|
|
287
|
+
mask_var : Optional[Union[NDArray[np.bool_], str]], optional
|
|
288
|
+
Only use a subset of genes given by a boolean array or a string
|
|
289
|
+
referring to a column in `adata.var`. Default: "highly_variable".
|
|
290
|
+
When set to None, use all genes.
|
|
291
|
+
vkey : str, optional
|
|
292
|
+
Name of velocity estimates to be used. By default "velocity".
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
adata: ad.AnnData
|
|
296
|
+
"""AnnData object for sampling cell sequences and getting gene expression."""
|
|
297
|
+
X: torch.Tensor
|
|
298
|
+
"""Spliced expression tensor of shape (n_cells, n_genes)."""
|
|
299
|
+
V: torch.Tensor
|
|
300
|
+
"""RNA velocity tensor of shape (n_cells, n_genes)"""
|
|
301
|
+
gene_names: pd.Index
|
|
302
|
+
"""Gene names index of shape (n_genes,)."""
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
adata: ad.AnnData,
|
|
307
|
+
layer: Optional[str] = "Ms",
|
|
308
|
+
mask_var: Optional[Union[NDArray[np.bool_], str]] = "highly_variable",
|
|
309
|
+
vkey: str = "velocity",
|
|
310
|
+
):
|
|
311
|
+
# Prepare expression data
|
|
312
|
+
self.adata = adata
|
|
313
|
+
X = self.adata.X if layer is None else self.adata.layers[layer]
|
|
314
|
+
if X is None:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
"Invalid gene expression matrix. Make sure that adata.X is present."
|
|
317
|
+
)
|
|
318
|
+
V = self.adata.layers[vkey]
|
|
319
|
+
self.gene_names = adata.var_names
|
|
320
|
+
if mask_var is not None:
|
|
321
|
+
if isinstance(mask_var, str):
|
|
322
|
+
if adata.var[mask_var].dtype != bool:
|
|
323
|
+
raise TypeError(
|
|
324
|
+
f"Expected adata.var[mask_var] to be of type bool, but got type {adata.var[mask_var].dtype}"
|
|
325
|
+
)
|
|
326
|
+
mask_var = self.adata.var[mask_var].to_numpy()
|
|
327
|
+
X = X[:, mask_var]
|
|
328
|
+
V = V[:, mask_var]
|
|
329
|
+
self.gene_names = self.gene_names[mask_var]
|
|
330
|
+
if isinstance(
|
|
331
|
+
X,
|
|
332
|
+
(sparse.csc_array, sparse.csr_array, sparse.csc_matrix, sparse.csr_matrix),
|
|
333
|
+
):
|
|
334
|
+
X = X.toarray()
|
|
335
|
+
self.X = torch.from_numpy(X).to(torch.float32)
|
|
336
|
+
self.V = torch.from_numpy(V).to(torch.float32)
|
|
337
|
+
|
|
338
|
+
def __len__(self) -> int:
|
|
339
|
+
return self.X.shape[0]
|
|
340
|
+
|
|
341
|
+
def __getitem__(self, idx) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
342
|
+
return self.X[idx, :], self.V[idx, :]
|
|
343
|
+
|
|
344
|
+
def n_genes(self) -> int:
|
|
345
|
+
"""
|
|
346
|
+
Get the number of genes in the dataset.
|
|
347
|
+
|
|
348
|
+
Returns
|
|
349
|
+
-------
|
|
350
|
+
int
|
|
351
|
+
Number of genes.
|
|
352
|
+
"""
|
|
353
|
+
return len(self.gene_names)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class BifurcatingMarkovChainDataset(MarkovChainDataset):
|
|
357
|
+
"""
|
|
358
|
+
Dummy Markov chain dataset simulating an SDE with a defined
|
|
359
|
+
adjacency matrix instead of using a single-cell dataset.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
def __init__(self, y0: torch.Tensor):
|
|
363
|
+
from bowside.training import integrate_model
|
|
364
|
+
|
|
365
|
+
self.sde = BifurcatingSDE()
|
|
366
|
+
n_steps = 50
|
|
367
|
+
self.time_points = torch.linspace(
|
|
368
|
+
0, n_steps - 1, n_steps, dtype=torch.float32, device=y0.device
|
|
369
|
+
)
|
|
370
|
+
simulations = integrate_model(
|
|
371
|
+
self.sde, y0, time_points=self.time_points, dt=0.1
|
|
372
|
+
)
|
|
373
|
+
self.X = simulations.reshape(-1, simulations.shape[-1])
|
|
374
|
+
self.markov_chains = (
|
|
375
|
+
torch.arange(self.X.shape[0])
|
|
376
|
+
.reshape(simulations.shape[0], simulations.shape[1])
|
|
377
|
+
.permute(1, 0)
|
|
378
|
+
.contiguous()
|
|
379
|
+
)
|
|
380
|
+
self.gene_names = pd.Index([f"gene_{i}" for i in range(simulations.size(2))])
|
|
381
|
+
self.adata = ad.AnnData(
|
|
382
|
+
X=self.X.detach().cpu().numpy(), var=pd.DataFrame(index=self.gene_names)
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class BifurcatingPseudotimeDataset(PseudotimeDataset):
|
|
387
|
+
"""
|
|
388
|
+
Dummy pseudotime dataset simulating an SDE with a defined adjacency
|
|
389
|
+
matrix instead of using a single-cell dataset.
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
def __init__(self, y0: torch.Tensor):
|
|
393
|
+
from bowside.training import integrate_model
|
|
394
|
+
|
|
395
|
+
self.sde = BifurcatingSDE()
|
|
396
|
+
n_steps = 50
|
|
397
|
+
self.time_points = torch.linspace(
|
|
398
|
+
0, n_steps - 1, n_steps, dtype=torch.float32, device=y0.device
|
|
399
|
+
)
|
|
400
|
+
simulations = integrate_model(
|
|
401
|
+
self.sde, y0, time_points=self.time_points, dt=0.1
|
|
402
|
+
)
|
|
403
|
+
self.X = simulations.reshape(-1, simulations.shape[-1])
|
|
404
|
+
self.pseudotime = self.time_points.repeat_interleave(simulations.shape[1])
|
|
405
|
+
self.sampled_cells = (
|
|
406
|
+
torch.arange(self.X.shape[0])
|
|
407
|
+
.reshape(simulations.shape[0], simulations.shape[1])
|
|
408
|
+
.permute(1, 0)
|
|
409
|
+
.contiguous()
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
self.gene_names = pd.Index([f"gene_{i}" for i in range(simulations.size(2))])
|
|
413
|
+
self.adata = ad.AnnData(
|
|
414
|
+
X=self.X.detach().cpu().numpy(), var=pd.DataFrame(index=self.gene_names)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def split_by_time_point(
|
|
419
|
+
dataset: PseudotimeDataset,
|
|
420
|
+
train_proportion: float = 0.8,
|
|
421
|
+
validation_proportion: float = 0.2,
|
|
422
|
+
test_proportion: float = 0.0,
|
|
423
|
+
) -> Union[
|
|
424
|
+
Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor, torch.Tensor, torch.Tensor]
|
|
425
|
+
]:
|
|
426
|
+
"""
|
|
427
|
+
Split the dataset by time points. The validation time points are
|
|
428
|
+
taken from the end, the test time points from the middle of the
|
|
429
|
+
remaining time points. By default, only training and validation
|
|
430
|
+
indices are generated.
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
dataset : PseudotimeDataset
|
|
435
|
+
Dataset that should be split.
|
|
436
|
+
train_proportion : float, optional
|
|
437
|
+
Proportion of timepoints for training. Default: 0.8.
|
|
438
|
+
validation_proportion : float, optional
|
|
439
|
+
Proportion of timepoints for validation. Default: 0.2.
|
|
440
|
+
test_proportion : float, optional
|
|
441
|
+
Proportion of timepoints for testing. Default: 0.0.
|
|
442
|
+
|
|
443
|
+
Returns
|
|
444
|
+
-------
|
|
445
|
+
Union[ Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor, torch.Tensor, torch.Tensor] ]
|
|
446
|
+
Indices of the training, validation, and (optionally) test
|
|
447
|
+
timepoints.
|
|
448
|
+
|
|
449
|
+
Raises
|
|
450
|
+
------
|
|
451
|
+
ValueError
|
|
452
|
+
An error is raised if one of the proportions is not in the
|
|
453
|
+
appropriate range, or if they do not sum to 1.
|
|
454
|
+
"""
|
|
455
|
+
if not np.isclose(train_proportion + validation_proportion + test_proportion, 1.0):
|
|
456
|
+
raise ValueError("Proportions must sum up to 1.")
|
|
457
|
+
|
|
458
|
+
if train_proportion <= 0.0 or train_proportion >= 1.0:
|
|
459
|
+
raise ValueError(
|
|
460
|
+
f"Training proportion must be in (0, 1), but was {train_proportion}."
|
|
461
|
+
)
|
|
462
|
+
if validation_proportion <= 0.0 or validation_proportion >= 1.0:
|
|
463
|
+
raise ValueError(
|
|
464
|
+
f"Validation proportion must be in (0, 1), but was {validation_proportion}."
|
|
465
|
+
)
|
|
466
|
+
if test_proportion < 0.0 or test_proportion >= 1.0:
|
|
467
|
+
raise ValueError(
|
|
468
|
+
f"Test proportion must be in [0, 1), but was {test_proportion}."
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
n_time_points = len(dataset.time_points)
|
|
472
|
+
all_indices = torch.arange(n_time_points)
|
|
473
|
+
|
|
474
|
+
# Take validation time points from the end
|
|
475
|
+
n_validation = floor(validation_proportion * n_time_points)
|
|
476
|
+
validation_indices = all_indices[-n_validation:]
|
|
477
|
+
|
|
478
|
+
train_indices = all_indices[:-n_validation]
|
|
479
|
+
|
|
480
|
+
# To test model performance based on interpolation, take time points
|
|
481
|
+
# from the middle of the remaining time points
|
|
482
|
+
if test_proportion > 0:
|
|
483
|
+
n_test = floor(test_proportion * n_time_points)
|
|
484
|
+
test_start_idx = ((n_time_points - n_validation) // 2) - (n_test // 2)
|
|
485
|
+
test_indices = all_indices[test_start_idx : test_start_idx + n_test]
|
|
486
|
+
train_indices = torch.tensor(
|
|
487
|
+
[idx for idx in train_indices if idx not in test_indices]
|
|
488
|
+
)
|
|
489
|
+
return train_indices, validation_indices, test_indices
|
|
490
|
+
return train_indices, validation_indices
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class PseudotimeTimepointWrapper(Dataset):
|
|
494
|
+
"""
|
|
495
|
+
Wrapper around the :class:`PseudotimeDataset` to select a subset of
|
|
496
|
+
the original time points, e.g., for train/validation/test split.
|
|
497
|
+
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
dataset : PseudotimeDataset
|
|
501
|
+
Original dataset.
|
|
502
|
+
timepoint_indices : torch.Tensor
|
|
503
|
+
Subset of time points.
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
def __init__(self, dataset: PseudotimeDataset, timepoint_indices: torch.Tensor):
|
|
507
|
+
self.dataset = dataset
|
|
508
|
+
self.time_point_indices = timepoint_indices
|
|
509
|
+
self.time_points = dataset.time_points[self.time_point_indices]
|
|
510
|
+
|
|
511
|
+
def __len__(self):
|
|
512
|
+
return len(self.dataset)
|
|
513
|
+
|
|
514
|
+
def __getitem__(self, idx):
|
|
515
|
+
sample = self.dataset[idx]
|
|
516
|
+
if sample.dim() == 2:
|
|
517
|
+
sliced_sample = sample[self.time_point_indices, :]
|
|
518
|
+
elif sample.dim() == 3:
|
|
519
|
+
sliced_sample = sample[:, self.time_point_indices, :]
|
|
520
|
+
else:
|
|
521
|
+
raise ValueError(
|
|
522
|
+
f"Unexpected tensor shape from underlying dataset: {sample.shape}"
|
|
523
|
+
)
|
|
524
|
+
return sliced_sample
|
|
525
|
+
|
|
526
|
+
def n_genes(self) -> int:
|
|
527
|
+
"""
|
|
528
|
+
Get the number of genes in the dataset.
|
|
529
|
+
|
|
530
|
+
Returns
|
|
531
|
+
-------
|
|
532
|
+
int
|
|
533
|
+
Number of genes.
|
|
534
|
+
"""
|
|
535
|
+
return self.dataset.n_genes()
|