brisc 0.2.1__tar.gz → 0.2.2__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.
- brisc-0.2.2/.git_archival.txt +2 -0
- brisc-0.2.2/.gitattributes +1 -0
- brisc-0.2.2/.gitignore +21 -0
- {brisc-0.2.1 → brisc-0.2.2}/MANIFEST.in +2 -0
- {brisc-0.2.1/brisc.egg-info → brisc-0.2.2}/PKG-INFO +1 -1
- {brisc-0.2.1 → brisc-0.2.2}/brisc/hvg.pyx +101 -125
- {brisc-0.2.1 → brisc-0.2.2}/brisc/knn.pyx +93 -80
- {brisc-0.2.1 → brisc-0.2.2}/brisc/single_cell.py +14 -1
- {brisc-0.2.1 → brisc-0.2.2/brisc.egg-info}/PKG-INFO +1 -1
- {brisc-0.2.1 → brisc-0.2.2}/brisc.egg-info/SOURCES.txt +7 -0
- brisc-0.2.2/brisc.egg-info/scm_file_list.json +110 -0
- brisc-0.2.2/brisc.egg-info/scm_version.json +8 -0
- {brisc-0.2.1 → brisc-0.2.2}/pyproject.toml +4 -2
- brisc-0.2.2/tests/conftest.py +30 -0
- brisc-0.2.2/tests/make_test_data.py +105 -0
- {brisc-0.2.1 → brisc-0.2.2}/LICENSE +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/README.md +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/__init__.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/colorspacious.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/concatenate.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/cysparse.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/cyutils.pxd +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/cyutils.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/de.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/doublets.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/embed.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/harmonize.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/hdf5.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/kmeans.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/labels.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/leiden.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/loess.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/norm_factors.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/normalize.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/pca.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/pseudobulk.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/pseudobulk_and_markers.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/qc.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/snn.pyx +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/sparse.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/threadpoolctl.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/type_aliases.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/utils.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc/validated_dict.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc.egg-info/dependency_links.txt +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc.egg-info/requires.txt +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/brisc.egg-info/top_level.txt +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/setup.cfg +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/setup.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_cluster.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_de.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_embedding.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_hvg.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_integration.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_markers.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_neighbors.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_normalize.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_pca.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_pseudobulk.py +0 -0
- {brisc-0.2.1 → brisc-0.2.2}/tests/test_qc.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.git_archival.txt export-subst
|
brisc-0.2.2/.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Python and build artifacts
|
|
2
|
+
brisc/__pycache__/
|
|
3
|
+
build/
|
|
4
|
+
dist/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
wheelhouse/
|
|
7
|
+
conda-bld/
|
|
8
|
+
brisc/_v2/
|
|
9
|
+
brisc/_v3/
|
|
10
|
+
brisc/_v4/
|
|
11
|
+
brisc/_sve/
|
|
12
|
+
|
|
13
|
+
# Cython artifacts (generated C/C++ files and compiled libraries)
|
|
14
|
+
*.c
|
|
15
|
+
*.cpp
|
|
16
|
+
*.so
|
|
17
|
+
*.pyd
|
|
18
|
+
|
|
19
|
+
# Miscellaneous
|
|
20
|
+
*.txt
|
|
21
|
+
!.git_archival.txt
|
|
@@ -12,20 +12,24 @@ def gene_mean_and_variance_csr(const numeric[::1] data,
|
|
|
12
12
|
const long long[::1] cell_indices,
|
|
13
13
|
const unsigned long long num_cells,
|
|
14
14
|
const unsigned num_dataset_genes,
|
|
15
|
+
unsigned long long[:, :, ::1] thread_sums,
|
|
16
|
+
unsigned long long[::1] counts_sum,
|
|
17
|
+
unsigned long long[::1] squared_counts_sum,
|
|
15
18
|
float[::1] mean,
|
|
16
19
|
float[::1] var,
|
|
17
20
|
unsigned[::1] nonzero_count,
|
|
18
21
|
unsigned num_threads):
|
|
22
|
+
# `thread_sums[thread_index, gene]` holds the sum, squared sum and number
|
|
23
|
+
# of non-zero counts of each gene across the elements processed by each
|
|
24
|
+
# thread, interleaved so that each element updates a single cache line.
|
|
25
|
+
# It is allocated once by the caller and reused across batches.
|
|
19
26
|
|
|
20
27
|
cdef unsigned gene, cell, thread_index
|
|
21
28
|
cdef unsigned long long num_elements, i, j, value, chunk_size, start, \
|
|
22
29
|
end, total_sum, total_sum_of_squares, total_nonzero_count
|
|
23
30
|
cdef float inv_num_cells = <float> 1 / num_cells
|
|
24
31
|
cdef double inv_num_pairs_of_cells = 1.0 / (num_cells * (num_cells - 1))
|
|
25
|
-
cdef
|
|
26
|
-
cdef vector[vector[unsigned long long]] thread_sum, thread_sum_of_squares
|
|
27
|
-
cdef vector[vector[unsigned]] thread_nonzero_counts
|
|
28
|
-
cdef unsigned long long[::1] sum, sum_of_squares
|
|
32
|
+
cdef unsigned long long[:, ::1] sums
|
|
29
33
|
|
|
30
34
|
if cell_indices.shape[0] == 0:
|
|
31
35
|
num_elements = indices.shape[0]
|
|
@@ -34,21 +38,20 @@ def gene_mean_and_variance_csr(const numeric[::1] data,
|
|
|
34
38
|
num_threads = min(num_threads, num_cells)
|
|
35
39
|
|
|
36
40
|
if num_threads <= 1:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
nonzero_count[:] = 0
|
|
41
|
+
sums = thread_sums[0]
|
|
42
|
+
for gene in range(num_dataset_genes):
|
|
43
|
+
sums[gene, 0] = 0
|
|
44
|
+
sums[gene, 1] = 0
|
|
45
|
+
sums[gene, 2] = 0
|
|
43
46
|
if cell_indices.shape[0] == 0:
|
|
44
47
|
# Iterate over all elements of the count matrix, ignoring which
|
|
45
48
|
# cell they're from
|
|
46
49
|
for i in range(num_elements):
|
|
47
50
|
gene = indices[i]
|
|
48
51
|
value = <unsigned long long> data[i]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
sums[gene, 0] += value
|
|
53
|
+
sums[gene, 1] += value * value
|
|
54
|
+
sums[gene, 2] += 1
|
|
52
55
|
else:
|
|
53
56
|
# Only iterate over the elements from cells in `cell_indices` (i.e.
|
|
54
57
|
# cells in this batch, and/or passing QC)
|
|
@@ -58,20 +61,20 @@ def gene_mean_and_variance_csr(const numeric[::1] data,
|
|
|
58
61
|
<unsigned long long> indptr[cell + 1]):
|
|
59
62
|
gene = indices[i]
|
|
60
63
|
value = <unsigned long long> data[i]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
sums[gene, 0] += value
|
|
65
|
+
sums[gene, 1] += value * value
|
|
66
|
+
sums[gene, 2] += 1
|
|
64
67
|
|
|
65
68
|
# Calculate means and variances from the sums and squared sums
|
|
66
69
|
for gene in range(num_dataset_genes):
|
|
67
|
-
|
|
70
|
+
counts_sum[gene] = sums[gene, 0]
|
|
71
|
+
squared_counts_sum[gene] = sums[gene, 1]
|
|
72
|
+
nonzero_count[gene] = <unsigned> sums[gene, 2]
|
|
73
|
+
mean[gene] = counts_sum[gene] * inv_num_cells
|
|
68
74
|
var[gene] = <float> (inv_num_pairs_of_cells * (
|
|
69
|
-
<double> num_cells * <double>
|
|
70
|
-
<double>
|
|
75
|
+
<double> num_cells * <double> squared_counts_sum[gene] -
|
|
76
|
+
<double> counts_sum[gene] * <double> counts_sum[gene]))
|
|
71
77
|
else:
|
|
72
|
-
thread_sum.resize(num_threads)
|
|
73
|
-
thread_sum_of_squares.resize(num_threads)
|
|
74
|
-
thread_nonzero_counts.resize(num_threads)
|
|
75
78
|
with nogil:
|
|
76
79
|
if cell_indices.shape[0] == 0:
|
|
77
80
|
# Partition the work by elements, not cells, for better
|
|
@@ -80,31 +83,28 @@ def gene_mean_and_variance_csr(const numeric[::1] data,
|
|
|
80
83
|
chunk_size = (num_elements + num_threads - 1) / num_threads
|
|
81
84
|
with parallel(num_threads=num_threads):
|
|
82
85
|
thread_index = threadid()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
num_dataset_genes)
|
|
86
|
+
for gene in range(num_dataset_genes):
|
|
87
|
+
thread_sums[thread_index, gene, 0] = 0
|
|
88
|
+
thread_sums[thread_index, gene, 1] = 0
|
|
89
|
+
thread_sums[thread_index, gene, 2] = 0
|
|
88
90
|
|
|
89
91
|
start = thread_index * chunk_size
|
|
90
92
|
end = min(start + chunk_size, num_elements)
|
|
91
93
|
for i in range(start, end):
|
|
92
94
|
gene = indices[i]
|
|
93
95
|
value = <unsigned long long> data[i]
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
thread_nonzero_counts[thread_index][gene] += 1
|
|
96
|
+
thread_sums[thread_index, gene, 0] += value
|
|
97
|
+
thread_sums[thread_index, gene, 1] += value * value
|
|
98
|
+
thread_sums[thread_index, gene, 2] += 1
|
|
98
99
|
else:
|
|
99
100
|
# Partition the work by cells
|
|
100
101
|
chunk_size = (num_cells + num_threads - 1) / num_threads
|
|
101
102
|
with parallel(num_threads=num_threads):
|
|
102
103
|
thread_index = threadid()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
num_dataset_genes)
|
|
104
|
+
for gene in range(num_dataset_genes):
|
|
105
|
+
thread_sums[thread_index, gene, 0] = 0
|
|
106
|
+
thread_sums[thread_index, gene, 1] = 0
|
|
107
|
+
thread_sums[thread_index, gene, 2] = 0
|
|
108
108
|
|
|
109
109
|
start = thread_index * chunk_size
|
|
110
110
|
end = min(start + chunk_size, num_cells)
|
|
@@ -114,10 +114,9 @@ def gene_mean_and_variance_csr(const numeric[::1] data,
|
|
|
114
114
|
<unsigned long long> indptr[cell + 1]):
|
|
115
115
|
gene = indices[i]
|
|
116
116
|
value = <unsigned long long> data[i]
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
thread_nonzero_counts[thread_index][gene] += 1
|
|
117
|
+
thread_sums[thread_index, gene, 0] += value
|
|
118
|
+
thread_sums[thread_index, gene, 1] += value * value
|
|
119
|
+
thread_sums[thread_index, gene, 2] += 1
|
|
121
120
|
|
|
122
121
|
# Calculate means and variances by aggregating the sums and squared
|
|
123
122
|
# sums across threads
|
|
@@ -126,17 +125,18 @@ def gene_mean_and_variance_csr(const numeric[::1] data,
|
|
|
126
125
|
total_sum_of_squares = 0
|
|
127
126
|
total_nonzero_count = 0
|
|
128
127
|
for thread_index in range(num_threads):
|
|
129
|
-
total_sum = total_sum +
|
|
130
|
-
thread_sum[thread_index][gene]
|
|
128
|
+
total_sum = total_sum + thread_sums[thread_index, gene, 0]
|
|
131
129
|
total_sum_of_squares = total_sum_of_squares + \
|
|
132
|
-
|
|
130
|
+
thread_sums[thread_index, gene, 1]
|
|
133
131
|
total_nonzero_count = total_nonzero_count + \
|
|
134
|
-
|
|
132
|
+
thread_sums[thread_index, gene, 2]
|
|
133
|
+
counts_sum[gene] = total_sum
|
|
134
|
+
squared_counts_sum[gene] = total_sum_of_squares
|
|
135
|
+
nonzero_count[gene] = <unsigned> total_nonzero_count
|
|
135
136
|
mean[gene] = total_sum * inv_num_cells
|
|
136
137
|
var[gene] = <float> (inv_num_pairs_of_cells * (
|
|
137
138
|
<double> num_cells * <double> total_sum_of_squares -
|
|
138
139
|
<double> total_sum * <double> total_sum))
|
|
139
|
-
nonzero_count[gene] = total_nonzero_count
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
def gene_mean_and_variance_csc(const numeric[::1] data,
|
|
@@ -258,38 +258,33 @@ def clipped_sum_csr(const numeric[::1] data,
|
|
|
258
258
|
const unsigned num_dataset_genes,
|
|
259
259
|
const long long[::1] cell_indices,
|
|
260
260
|
const float[::1] clip_val,
|
|
261
|
+
unsigned long long[:, :, ::1] thread_sums,
|
|
262
|
+
const unsigned long long[::1] counts_sum,
|
|
263
|
+
const unsigned long long[::1] squared_counts_sum,
|
|
261
264
|
float[::1] batch_counts_sum,
|
|
262
265
|
float[::1] squared_batch_counts_sum,
|
|
263
266
|
unsigned num_threads):
|
|
264
|
-
cdef unsigned cell, gene, thread_index
|
|
265
|
-
cdef unsigned long long i, j, value,
|
|
266
|
-
|
|
267
|
-
cdef vector[unsigned long long] batch_counts_sum_int_buffer, \
|
|
268
|
-
squared_batch_counts_sum_int_buffer, num_out_of_range_buffer
|
|
269
|
-
cdef vector[vector[unsigned long long]] thread_batch_counts_sum_int, \
|
|
270
|
-
thread_squared_batch_counts_sum_int, thread_num_out_of_range
|
|
267
|
+
cdef unsigned cell, gene, thread_index
|
|
268
|
+
cdef unsigned long long i, j, value, chunk_size, start, end, \
|
|
269
|
+
out_of_range_sum, squared_out_of_range_sum, num_out_of_range
|
|
271
270
|
cdef pair[unsigned, unsigned] row_range
|
|
272
|
-
cdef unsigned long long[::1]
|
|
273
|
-
squared_batch_counts_sum_int, num_out_of_range
|
|
271
|
+
cdef unsigned long long[:, ::1] sums
|
|
274
272
|
|
|
275
273
|
# Key insight: the things we're summing are integers except when
|
|
276
274
|
# `value > clip_val[gene]`, in which case we are adding a (floating-point)
|
|
277
275
|
# constant, so keep track of this case separately and add it at the end, to
|
|
278
|
-
# minimize floating-point error.
|
|
279
|
-
# for these
|
|
280
|
-
#
|
|
276
|
+
# minimize floating-point error. This case is rare, so only update
|
|
277
|
+
# `thread_sums` for these elements, and subtract them from the unclipped
|
|
278
|
+
# sums calculated by `gene_mean_and_variance_csr()`. Also, use an integer
|
|
279
|
+
# version of `clip_val` for these comparisons, to avoid any potential
|
|
280
|
+
# inconsistency in floating-point roundoff between code paths.
|
|
281
281
|
num_threads = min(num_threads, num_cells)
|
|
282
282
|
if num_threads <= 1:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
<unsigned long long[:num_dataset_genes]> \
|
|
289
|
-
squared_batch_counts_sum_int_buffer.data()
|
|
290
|
-
num_out_of_range_buffer.resize(num_dataset_genes)
|
|
291
|
-
num_out_of_range = <unsigned long long[:num_dataset_genes]> \
|
|
292
|
-
num_out_of_range_buffer.data()
|
|
283
|
+
sums = thread_sums[0]
|
|
284
|
+
for gene in range(num_dataset_genes):
|
|
285
|
+
sums[gene, 0] = 0
|
|
286
|
+
sums[gene, 1] = 0
|
|
287
|
+
sums[gene, 2] = 0
|
|
293
288
|
if cell_indices.shape[0] == 0:
|
|
294
289
|
for cell in range(num_cells):
|
|
295
290
|
for i in range(<unsigned long long> indptr[cell],
|
|
@@ -297,10 +292,9 @@ def clipped_sum_csr(const numeric[::1] data,
|
|
|
297
292
|
gene = indices[i]
|
|
298
293
|
value = <unsigned long long> data[i]
|
|
299
294
|
if value > clip_val[gene]:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
squared_batch_counts_sum_int[gene] += value * value
|
|
295
|
+
sums[gene, 0] += value
|
|
296
|
+
sums[gene, 1] += value * value
|
|
297
|
+
sums[gene, 2] += 1
|
|
304
298
|
else:
|
|
305
299
|
for j in range(<unsigned long long> cell_indices.shape[0]):
|
|
306
300
|
cell = cell_indices[j]
|
|
@@ -309,31 +303,25 @@ def clipped_sum_csr(const numeric[::1] data,
|
|
|
309
303
|
gene = indices[i]
|
|
310
304
|
value = <unsigned long long> data[i]
|
|
311
305
|
if value > clip_val[gene]:
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
squared_batch_counts_sum_int[gene] += value * value
|
|
306
|
+
sums[gene, 0] += value
|
|
307
|
+
sums[gene, 1] += value * value
|
|
308
|
+
sums[gene, 2] += 1
|
|
316
309
|
for gene in range(num_dataset_genes):
|
|
317
310
|
batch_counts_sum[gene] = \
|
|
318
|
-
|
|
319
|
-
|
|
311
|
+
(counts_sum[gene] - sums[gene, 0]) + \
|
|
312
|
+
sums[gene, 2] * clip_val[gene]
|
|
320
313
|
squared_batch_counts_sum[gene] = \
|
|
321
|
-
|
|
322
|
-
|
|
314
|
+
(squared_counts_sum[gene] - sums[gene, 1]) + \
|
|
315
|
+
sums[gene, 2] * clip_val[gene] * clip_val[gene]
|
|
323
316
|
else:
|
|
324
|
-
thread_batch_counts_sum_int.resize(num_threads)
|
|
325
|
-
thread_squared_batch_counts_sum_int.resize(num_threads)
|
|
326
|
-
thread_num_out_of_range.resize(num_threads)
|
|
327
317
|
with nogil:
|
|
328
318
|
if cell_indices.shape[0] == 0:
|
|
329
319
|
with parallel(num_threads=num_threads):
|
|
330
320
|
thread_index = threadid()
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
thread_num_out_of_range[thread_index].resize(
|
|
336
|
-
num_dataset_genes)
|
|
321
|
+
for gene in range(num_dataset_genes):
|
|
322
|
+
thread_sums[thread_index, gene, 0] = 0
|
|
323
|
+
thread_sums[thread_index, gene, 1] = 0
|
|
324
|
+
thread_sums[thread_index, gene, 2] = 0
|
|
337
325
|
|
|
338
326
|
row_range = get_thread_offset(indptr, thread_index,
|
|
339
327
|
num_threads)
|
|
@@ -343,23 +331,18 @@ def clipped_sum_csr(const numeric[::1] data,
|
|
|
343
331
|
gene = indices[i]
|
|
344
332
|
value = <unsigned long long> data[i]
|
|
345
333
|
if value > clip_val[gene]:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
thread_index][gene] += value
|
|
351
|
-
thread_squared_batch_counts_sum_int[
|
|
352
|
-
thread_index][gene] += value * value
|
|
334
|
+
thread_sums[thread_index, gene, 0] += value
|
|
335
|
+
thread_sums[thread_index, gene, 1] += \
|
|
336
|
+
value * value
|
|
337
|
+
thread_sums[thread_index, gene, 2] += 1
|
|
353
338
|
else:
|
|
354
339
|
chunk_size = (num_cells + num_threads - 1) / num_threads
|
|
355
340
|
with parallel(num_threads=num_threads):
|
|
356
341
|
thread_index = threadid()
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
thread_num_out_of_range[thread_index].resize(
|
|
362
|
-
num_dataset_genes)
|
|
342
|
+
for gene in range(num_dataset_genes):
|
|
343
|
+
thread_sums[thread_index, gene, 0] = 0
|
|
344
|
+
thread_sums[thread_index, gene, 1] = 0
|
|
345
|
+
thread_sums[thread_index, gene, 2] = 0
|
|
363
346
|
|
|
364
347
|
start = thread_index * chunk_size
|
|
365
348
|
end = min(start + chunk_size, num_cells)
|
|
@@ -370,36 +353,29 @@ def clipped_sum_csr(const numeric[::1] data,
|
|
|
370
353
|
gene = indices[i]
|
|
371
354
|
value = <unsigned long long> data[i]
|
|
372
355
|
if value > clip_val[gene]:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
thread_index][gene] += value
|
|
378
|
-
thread_squared_batch_counts_sum_int[
|
|
379
|
-
thread_index][gene] += value * value
|
|
356
|
+
thread_sums[thread_index, gene, 0] += value
|
|
357
|
+
thread_sums[thread_index, gene, 1] += \
|
|
358
|
+
value * value
|
|
359
|
+
thread_sums[thread_index, gene, 2] += 1
|
|
380
360
|
|
|
381
361
|
for gene in prange(num_dataset_genes,
|
|
382
362
|
num_threads=num_threads):
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
363
|
+
out_of_range_sum = 0
|
|
364
|
+
squared_out_of_range_sum = 0
|
|
365
|
+
num_out_of_range = 0
|
|
386
366
|
for thread_index in range(num_threads):
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
total_num_out_of_range = \
|
|
394
|
-
total_num_out_of_range + \
|
|
395
|
-
thread_num_out_of_range[thread_index][gene]
|
|
367
|
+
out_of_range_sum = out_of_range_sum + \
|
|
368
|
+
thread_sums[thread_index, gene, 0]
|
|
369
|
+
squared_out_of_range_sum = squared_out_of_range_sum + \
|
|
370
|
+
thread_sums[thread_index, gene, 1]
|
|
371
|
+
num_out_of_range = num_out_of_range + \
|
|
372
|
+
thread_sums[thread_index, gene, 2]
|
|
396
373
|
batch_counts_sum[gene] = \
|
|
397
|
-
|
|
398
|
-
|
|
374
|
+
(counts_sum[gene] - out_of_range_sum) + \
|
|
375
|
+
num_out_of_range * clip_val[gene]
|
|
399
376
|
squared_batch_counts_sum[gene] = \
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
clip_val[gene] * clip_val[gene]
|
|
377
|
+
(squared_counts_sum[gene] - squared_out_of_range_sum) + \
|
|
378
|
+
num_out_of_range * clip_val[gene] * clip_val[gene]
|
|
403
379
|
|
|
404
380
|
|
|
405
381
|
def clipped_sum_csc(const numeric[::1] data,
|
|
@@ -37,12 +37,14 @@ def knn_self(const float[:, ::1] X,
|
|
|
37
37
|
num_cells_in_block, query_index_in_block, query_sorted, num_pairs, \
|
|
38
38
|
pair_index, next_pair_index, query_in_cluster_index, \
|
|
39
39
|
num_queries_in_cluster, max_cluster_size, cluster_start, \
|
|
40
|
-
cluster_size, neighbor_offset,
|
|
40
|
+
cluster_size, neighbor_offset, block_offset, block_end, \
|
|
41
|
+
num_dimensions = X.shape[1], \
|
|
41
42
|
num_clusters = centroids.shape[0], \
|
|
42
43
|
num_chunks = (num_X + chunk_size_kmeans - 1) / chunk_size_kmeans, \
|
|
43
44
|
num_blocks = (num_X + chunk_size_search - 1) / chunk_size_search
|
|
44
45
|
cdef float norm, worst_distance, distance, query_norm, true_distance, \
|
|
45
|
-
|
|
46
|
+
block_min_distance
|
|
47
|
+
cdef const float* row
|
|
46
48
|
cdef uninitialized_vector[unsigned] cluster_offsets_buffer
|
|
47
49
|
cdef uninitialized_vector[float] centroid_norms_buffer, \
|
|
48
50
|
temp_distances_buffer, query_X_buffer, block_distances_buffer
|
|
@@ -242,34 +244,25 @@ def knn_self(const float[:, ::1] X,
|
|
|
242
244
|
query_sorted = block_start + query_index_in_block
|
|
243
245
|
query_norm = cell_norms[query_sorted]
|
|
244
246
|
worst_distance = distances[query_sorted, 0]
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
num_neighbors)
|
|
261
|
-
worst_distance = distances[query_sorted, 0]
|
|
262
|
-
else:
|
|
263
|
-
# Same cluster the query cell is in; check for
|
|
264
|
-
# self-neighbors
|
|
265
|
-
for neighbor_offset in range(cluster_size):
|
|
247
|
+
row = &block_distances[
|
|
248
|
+
query_in_cluster_index * cluster_size]
|
|
249
|
+
# Skip blocks of 64 cells where even the nearest one
|
|
250
|
+
# does not beat the current worst neighbor
|
|
251
|
+
for block_offset in range(0, cluster_size, 64):
|
|
252
|
+
block_end = min(block_offset + 64, cluster_size)
|
|
253
|
+
block_min_distance = row[block_offset]
|
|
254
|
+
for neighbor_offset in range(block_offset + 1,
|
|
255
|
+
block_end):
|
|
256
|
+
block_min_distance = min(block_min_distance,
|
|
257
|
+
row[neighbor_offset])
|
|
258
|
+
if query_norm + block_min_distance >= worst_distance:
|
|
259
|
+
continue
|
|
260
|
+
for neighbor_offset in range(block_offset,
|
|
261
|
+
block_end):
|
|
266
262
|
neighbor = cluster_start + neighbor_offset
|
|
267
263
|
if query_sorted == neighbor:
|
|
268
264
|
continue # skip self-neighbors
|
|
269
|
-
|
|
270
|
-
query_in_cluster_index * cluster_size +
|
|
271
|
-
neighbor_offset]
|
|
272
|
-
true_distance = query_norm + partial_distance
|
|
265
|
+
true_distance = query_norm + row[neighbor_offset]
|
|
273
266
|
if true_distance < worst_distance:
|
|
274
267
|
max_heap_replace_top(
|
|
275
268
|
&neighbors[query_sorted, 0],
|
|
@@ -458,37 +451,28 @@ def knn_self(const float[:, ::1] X,
|
|
|
458
451
|
query_sorted = block_start + query_index_in_block
|
|
459
452
|
query_norm = cell_norms[query_sorted]
|
|
460
453
|
worst_distance = distances[query_sorted, 0]
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
worst_distance = \
|
|
479
|
-
distances[query_sorted, 0]
|
|
480
|
-
else:
|
|
481
|
-
# Same cluster the query cell is in; check for
|
|
482
|
-
# self-neighbors
|
|
483
|
-
for neighbor_offset in range(cluster_size):
|
|
454
|
+
row = \
|
|
455
|
+
thread_block_distances[thread_index].data() + \
|
|
456
|
+
query_in_cluster_index * cluster_size
|
|
457
|
+
for block_offset in range(0, cluster_size, 64):
|
|
458
|
+
block_end = \
|
|
459
|
+
min(block_offset + 64, cluster_size)
|
|
460
|
+
block_min_distance = row[block_offset]
|
|
461
|
+
for neighbor_offset in range(
|
|
462
|
+
block_offset + 1, block_end):
|
|
463
|
+
block_min_distance = min(
|
|
464
|
+
block_min_distance,
|
|
465
|
+
row[neighbor_offset])
|
|
466
|
+
if query_norm + block_min_distance >= \
|
|
467
|
+
worst_distance:
|
|
468
|
+
continue
|
|
469
|
+
for neighbor_offset in range(block_offset,
|
|
470
|
+
block_end):
|
|
484
471
|
neighbor = cluster_start + neighbor_offset
|
|
485
472
|
if query_sorted == neighbor:
|
|
486
473
|
continue # skip self-neighbors
|
|
487
|
-
partial_distance = thread_block_distances[
|
|
488
|
-
thread_index][query_in_cluster_index *
|
|
489
|
-
cluster_size + neighbor_offset]
|
|
490
474
|
true_distance = \
|
|
491
|
-
query_norm +
|
|
475
|
+
query_norm + row[neighbor_offset]
|
|
492
476
|
if true_distance < worst_distance:
|
|
493
477
|
max_heap_replace_top(
|
|
494
478
|
&neighbors[query_sorted, 0],
|
|
@@ -544,14 +528,16 @@ def knn_cross(const float[:, ::1] Y,
|
|
|
544
528
|
num_cells_in_block, query_index_in_block, query_sorted, num_pairs, \
|
|
545
529
|
pair_index, next_pair_index, query_in_cluster_index, \
|
|
546
530
|
num_queries_in_cluster, max_cluster_size, cluster_start, \
|
|
547
|
-
cluster_size, neighbor_offset,
|
|
531
|
+
cluster_size, neighbor_offset, block_offset, block_end, \
|
|
532
|
+
num_dimensions = X.shape[1], \
|
|
548
533
|
num_clusters = centroids.shape[0], \
|
|
549
534
|
num_chunks = (num_Y + chunk_size_kmeans - 1) / \
|
|
550
535
|
chunk_size_kmeans, \
|
|
551
536
|
num_blocks = (num_Y + chunk_size_search - 1) / \
|
|
552
537
|
chunk_size_search
|
|
553
538
|
cdef float norm, worst_distance, distance, query_norm, true_distance, \
|
|
554
|
-
|
|
539
|
+
block_min_distance
|
|
540
|
+
cdef const float* row
|
|
555
541
|
cdef uninitialized_vector[unsigned] cluster_offsets_buffer
|
|
556
542
|
cdef uninitialized_vector[float] centroid_norms_buffer, \
|
|
557
543
|
temp_distances_buffer, query_Y_buffer, block_distances_buffer
|
|
@@ -743,18 +729,29 @@ def knn_cross(const float[:, ::1] Y,
|
|
|
743
729
|
query_sorted = block_start + query_index_in_block
|
|
744
730
|
query_norm = query_norms[query_sorted]
|
|
745
731
|
worst_distance = distances[query_sorted, 0]
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
732
|
+
row = &block_distances[
|
|
733
|
+
query_in_cluster_index * cluster_size]
|
|
734
|
+
# Skip blocks of 64 cells where even the nearest one
|
|
735
|
+
# does not beat the current worst neighbor
|
|
736
|
+
for block_offset in range(0, cluster_size, 64):
|
|
737
|
+
block_end = min(block_offset + 64, cluster_size)
|
|
738
|
+
block_min_distance = row[block_offset]
|
|
739
|
+
for neighbor_offset in range(block_offset + 1,
|
|
740
|
+
block_end):
|
|
741
|
+
block_min_distance = min(block_min_distance,
|
|
742
|
+
row[neighbor_offset])
|
|
743
|
+
if query_norm + block_min_distance >= worst_distance:
|
|
744
|
+
continue
|
|
745
|
+
for neighbor_offset in range(block_offset,
|
|
746
|
+
block_end):
|
|
747
|
+
neighbor = cluster_start + neighbor_offset
|
|
748
|
+
true_distance = query_norm + row[neighbor_offset]
|
|
749
|
+
if true_distance < worst_distance:
|
|
750
|
+
max_heap_replace_top(
|
|
751
|
+
&neighbors[query_sorted, 0],
|
|
752
|
+
&distances[query_sorted, 0], neighbor,
|
|
753
|
+
true_distance, num_neighbors)
|
|
754
|
+
worst_distance = distances[query_sorted, 0]
|
|
758
755
|
|
|
759
756
|
pair_index = next_pair_index
|
|
760
757
|
|
|
@@ -944,18 +941,34 @@ def knn_cross(const float[:, ::1] Y,
|
|
|
944
941
|
query_sorted = block_start + query_index_in_block
|
|
945
942
|
query_norm = query_norms[query_sorted]
|
|
946
943
|
worst_distance = distances[query_sorted, 0]
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
944
|
+
row = \
|
|
945
|
+
thread_block_distances[thread_index].data() + \
|
|
946
|
+
query_in_cluster_index * cluster_size
|
|
947
|
+
for block_offset in range(0, cluster_size, 64):
|
|
948
|
+
block_end = \
|
|
949
|
+
min(block_offset + 64, cluster_size)
|
|
950
|
+
block_min_distance = row[block_offset]
|
|
951
|
+
for neighbor_offset in range(
|
|
952
|
+
block_offset + 1, block_end):
|
|
953
|
+
block_min_distance = min(
|
|
954
|
+
block_min_distance,
|
|
955
|
+
row[neighbor_offset])
|
|
956
|
+
if query_norm + block_min_distance >= \
|
|
957
|
+
worst_distance:
|
|
958
|
+
continue
|
|
959
|
+
for neighbor_offset in range(block_offset,
|
|
960
|
+
block_end):
|
|
961
|
+
neighbor = cluster_start + neighbor_offset
|
|
962
|
+
true_distance = \
|
|
963
|
+
query_norm + row[neighbor_offset]
|
|
964
|
+
if true_distance < worst_distance:
|
|
965
|
+
max_heap_replace_top(
|
|
966
|
+
&neighbors[query_sorted, 0],
|
|
967
|
+
&distances[query_sorted, 0],
|
|
968
|
+
neighbor, true_distance,
|
|
969
|
+
num_neighbors)
|
|
970
|
+
worst_distance = \
|
|
971
|
+
distances[query_sorted, 0]
|
|
959
972
|
|
|
960
973
|
pair_index = next_pair_index
|
|
961
974
|
|
|
@@ -11385,6 +11385,12 @@ class SingleCell:
|
|
|
11385
11385
|
norm_gene_vars = []
|
|
11386
11386
|
total_nonzero_count = np.zeros(len(genes_in_all_datasets),
|
|
11387
11387
|
dtype=np.uint64)
|
|
11388
|
+
# For CSR, allocate the per-thread sums once and reuse them across
|
|
11389
|
+
# batches, rather than reallocating them for every batch
|
|
11390
|
+
if any(isinstance(dataset._X, csr_array) for dataset in datasets):
|
|
11391
|
+
thread_sums = np.empty((num_threads, max(
|
|
11392
|
+
dataset._X.shape[1] for dataset in datasets), 3),
|
|
11393
|
+
dtype=np.uint64)
|
|
11388
11394
|
for X, cell_mask, gene_indices in batches:
|
|
11389
11395
|
num_dataset_genes = X.shape[1]
|
|
11390
11396
|
mean = np.empty(num_dataset_genes, dtype=np.float32)
|
|
@@ -11400,10 +11406,15 @@ class SingleCell:
|
|
|
11400
11406
|
else:
|
|
11401
11407
|
cell_indices = cell_mask
|
|
11402
11408
|
num_cells = len(cell_indices)
|
|
11409
|
+
counts_sum = np.empty(num_dataset_genes, dtype=np.uint64)
|
|
11410
|
+
squared_counts_sum = np.empty(num_dataset_genes,
|
|
11411
|
+
dtype=np.uint64)
|
|
11403
11412
|
gene_mean_and_variance_csr(
|
|
11404
11413
|
data=X.data, indices=X.indices, indptr=X.indptr,
|
|
11405
11414
|
cell_indices=cell_indices, num_cells=num_cells,
|
|
11406
|
-
num_dataset_genes=num_dataset_genes,
|
|
11415
|
+
num_dataset_genes=num_dataset_genes,
|
|
11416
|
+
thread_sums=thread_sums, counts_sum=counts_sum,
|
|
11417
|
+
squared_counts_sum=squared_counts_sum, mean=mean, var=var,
|
|
11407
11418
|
nonzero_count=nonzero_count, num_threads=num_threads)
|
|
11408
11419
|
else:
|
|
11409
11420
|
if cell_mask is None:
|
|
@@ -11452,6 +11463,8 @@ class SingleCell:
|
|
|
11452
11463
|
data=X.data, indices=X.indices, indptr=X.indptr,
|
|
11453
11464
|
num_cells=num_cells, num_dataset_genes=num_dataset_genes,
|
|
11454
11465
|
cell_indices=cell_indices, clip_val=clip_val,
|
|
11466
|
+
thread_sums=thread_sums, counts_sum=counts_sum,
|
|
11467
|
+
squared_counts_sum=squared_counts_sum,
|
|
11455
11468
|
batch_counts_sum=batch_counts_sum,
|
|
11456
11469
|
squared_batch_counts_sum=squared_batch_counts_sum,
|
|
11457
11470
|
num_threads=num_threads)
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
.git_archival.txt
|
|
2
|
+
.gitattributes
|
|
3
|
+
.gitignore
|
|
1
4
|
LICENSE
|
|
2
5
|
MANIFEST.in
|
|
3
6
|
README.md
|
|
@@ -37,7 +40,11 @@ brisc.egg-info/PKG-INFO
|
|
|
37
40
|
brisc.egg-info/SOURCES.txt
|
|
38
41
|
brisc.egg-info/dependency_links.txt
|
|
39
42
|
brisc.egg-info/requires.txt
|
|
43
|
+
brisc.egg-info/scm_file_list.json
|
|
44
|
+
brisc.egg-info/scm_version.json
|
|
40
45
|
brisc.egg-info/top_level.txt
|
|
46
|
+
tests/conftest.py
|
|
47
|
+
tests/make_test_data.py
|
|
41
48
|
tests/test_cluster.py
|
|
42
49
|
tests/test_de.py
|
|
43
50
|
tests/test_embedding.py
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".git_archival.txt",
|
|
4
|
+
".gitattributes",
|
|
5
|
+
".github/workflows/build-and-publish.yml",
|
|
6
|
+
".github/workflows/docs.yml",
|
|
7
|
+
".gitignore",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"MANIFEST.in",
|
|
10
|
+
"README.md",
|
|
11
|
+
"brisc/__init__.py",
|
|
12
|
+
"brisc/colorspacious.py",
|
|
13
|
+
"brisc/concatenate.py",
|
|
14
|
+
"brisc/cysparse.pyx",
|
|
15
|
+
"brisc/cyutils.pxd",
|
|
16
|
+
"brisc/cyutils.pyx",
|
|
17
|
+
"brisc/de.py",
|
|
18
|
+
"brisc/doublets.pyx",
|
|
19
|
+
"brisc/embed.pyx",
|
|
20
|
+
"brisc/harmonize.pyx",
|
|
21
|
+
"brisc/hdf5.pyx",
|
|
22
|
+
"brisc/hvg.pyx",
|
|
23
|
+
"brisc/kmeans.pyx",
|
|
24
|
+
"brisc/knn.pyx",
|
|
25
|
+
"brisc/labels.pyx",
|
|
26
|
+
"brisc/leiden.pyx",
|
|
27
|
+
"brisc/loess.pyx",
|
|
28
|
+
"brisc/norm_factors.pyx",
|
|
29
|
+
"brisc/normalize.pyx",
|
|
30
|
+
"brisc/pca.pyx",
|
|
31
|
+
"brisc/pseudobulk.py",
|
|
32
|
+
"brisc/pseudobulk_and_markers.pyx",
|
|
33
|
+
"brisc/qc.pyx",
|
|
34
|
+
"brisc/single_cell.py",
|
|
35
|
+
"brisc/snn.pyx",
|
|
36
|
+
"brisc/sparse.py",
|
|
37
|
+
"brisc/threadpoolctl.py",
|
|
38
|
+
"brisc/type_aliases.py",
|
|
39
|
+
"brisc/utils.py",
|
|
40
|
+
"brisc/validated_dict.py",
|
|
41
|
+
"docs/.gitignore",
|
|
42
|
+
"docs/Makefile",
|
|
43
|
+
"docs/deploy.sh",
|
|
44
|
+
"docs/deploy/_worker.js",
|
|
45
|
+
"docs/deploy/robots.txt",
|
|
46
|
+
"docs/make_tutorial_data.py",
|
|
47
|
+
"docs/source/_pygments_ristretto.py",
|
|
48
|
+
"docs/source/_static/css/custom.css",
|
|
49
|
+
"docs/source/_static/fonts/DejaVuSansMono-subset.woff2",
|
|
50
|
+
"docs/source/_static/images/favicon.ico",
|
|
51
|
+
"docs/source/_static/images/runner_logo_dark.svg",
|
|
52
|
+
"docs/source/_static/images/runner_logo_light.svg",
|
|
53
|
+
"docs/source/_static/images/runner_title_wide_dark.svg",
|
|
54
|
+
"docs/source/_static/images/runner_title_wide_light.svg",
|
|
55
|
+
"docs/source/_static/js/api-scrollspy.js",
|
|
56
|
+
"docs/source/_static/js/benchmark-data.js",
|
|
57
|
+
"docs/source/_static/js/benchmark.js",
|
|
58
|
+
"docs/source/_static/js/details-anchor.js",
|
|
59
|
+
"docs/source/_static/js/header-reveal.js",
|
|
60
|
+
"docs/source/_static/js/theme-fix.js",
|
|
61
|
+
"docs/source/_templates/guide-nav.html",
|
|
62
|
+
"docs/source/api/index.md",
|
|
63
|
+
"docs/source/conf.py",
|
|
64
|
+
"docs/source/de/analysis.rst",
|
|
65
|
+
"docs/source/de/index.rst",
|
|
66
|
+
"docs/source/de/io.rst",
|
|
67
|
+
"docs/source/de/properties.rst",
|
|
68
|
+
"docs/source/index.md",
|
|
69
|
+
"docs/source/installation.md",
|
|
70
|
+
"docs/source/pseudobulk/analysis.rst",
|
|
71
|
+
"docs/source/pseudobulk/data_access.rst",
|
|
72
|
+
"docs/source/pseudobulk/dictionary_interface.rst",
|
|
73
|
+
"docs/source/pseudobulk/index.rst",
|
|
74
|
+
"docs/source/pseudobulk/io.rst",
|
|
75
|
+
"docs/source/pseudobulk/manipulation.rst",
|
|
76
|
+
"docs/source/pseudobulk/properties.rst",
|
|
77
|
+
"docs/source/singlecell/analysis.rst",
|
|
78
|
+
"docs/source/singlecell/data_access.rst",
|
|
79
|
+
"docs/source/singlecell/index.rst",
|
|
80
|
+
"docs/source/singlecell/io.rst",
|
|
81
|
+
"docs/source/singlecell/manipulation.rst",
|
|
82
|
+
"docs/source/singlecell/properties.rst",
|
|
83
|
+
"docs/source/singlecell/typedefs.rst",
|
|
84
|
+
"docs/source/tutorials/basic_workflow.md",
|
|
85
|
+
"docs/source/tutorials/data_manipulation.md",
|
|
86
|
+
"docs/source/tutorials/differential_expression.md",
|
|
87
|
+
"docs/source/tutorials/images/label_transfer.png",
|
|
88
|
+
"docs/source/tutorials/images/markers.png",
|
|
89
|
+
"docs/source/tutorials/images/umap.png",
|
|
90
|
+
"docs/source/tutorials/images/volcano.png",
|
|
91
|
+
"docs/source/tutorials/index.md",
|
|
92
|
+
"docs/source/tutorials/integration_and_label_transfer.md",
|
|
93
|
+
"docs/source/tutorials/interoperability.md",
|
|
94
|
+
"pyproject.toml",
|
|
95
|
+
"setup.py",
|
|
96
|
+
"tests/conftest.py",
|
|
97
|
+
"tests/make_test_data.py",
|
|
98
|
+
"tests/test_cluster.py",
|
|
99
|
+
"tests/test_de.py",
|
|
100
|
+
"tests/test_embedding.py",
|
|
101
|
+
"tests/test_hvg.py",
|
|
102
|
+
"tests/test_integration.py",
|
|
103
|
+
"tests/test_markers.py",
|
|
104
|
+
"tests/test_neighbors.py",
|
|
105
|
+
"tests/test_normalize.py",
|
|
106
|
+
"tests/test_pca.py",
|
|
107
|
+
"tests/test_pseudobulk.py",
|
|
108
|
+
"tests/test_qc.py"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=64", "setuptools-scm>=8", "wheel", "Cython>=3.0", "numpy>=1.17.0", "scipy>=0.16.0"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "brisc"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "Fast single-cell toolkit"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -39,6 +39,8 @@ include = ["brisc", "brisc.*"]
|
|
|
39
39
|
[tool.setuptools.exclude-package-data]
|
|
40
40
|
"*" = ["*.cpp", "*.c"]
|
|
41
41
|
|
|
42
|
+
[tool.setuptools_scm]
|
|
43
|
+
|
|
42
44
|
[tool.cibuildwheel]
|
|
43
45
|
build = "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-* cp315-*"
|
|
44
46
|
skip = "*musllinux* *i686* *ppc64le* *s390x* cp314t-* cp315t-*"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Shared fixtures for the brisc test suite."""
|
|
2
|
+
import shutil
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
_FIXTURE = "Parse_10K_PBMC_cytokines.h5ad"
|
|
6
|
+
_HF_REPO = "dashingcell/Parse_10K_PBMC_cytokines"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(scope="session")
|
|
10
|
+
def data_path():
|
|
11
|
+
# Pull the ~0.15 GB fixture from the public HF dataset (cached under
|
|
12
|
+
# ~/.cache/huggingface).
|
|
13
|
+
from huggingface_hub import hf_hub_download
|
|
14
|
+
return hf_hub_download(_HF_REPO, _FIXTURE, repo_type="dataset")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(scope="session")
|
|
18
|
+
def sc_orig(data_path):
|
|
19
|
+
from brisc import SingleCell
|
|
20
|
+
return SingleCell(data_path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture(scope="session")
|
|
24
|
+
def require_r():
|
|
25
|
+
# R is installed in CI, so a broken ryp/limma should fail loudly, not skip.
|
|
26
|
+
if shutil.which("R") is None and shutil.which("Rscript") is None:
|
|
27
|
+
pytest.skip("R not installed")
|
|
28
|
+
import ryp
|
|
29
|
+
ryp.r("suppressMessages(library(limma))")
|
|
30
|
+
return True
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build the brisc test fixture and publish it to the HF dataset CI pulls from.
|
|
3
|
+
Kept for provenance -- CI never runs this; conftest fetches the finished file.
|
|
4
|
+
|
|
5
|
+
PBS vs IFN-gamma, balanced 50/50, subsampled to 10,000 cells x full genes:
|
|
6
|
+
24 pseudobulk samples (2 x 12 donors) instead of 1,092, so DE by
|
|
7
|
+
(sample, cell_type) stays well-populated.
|
|
8
|
+
|
|
9
|
+
Run where the 10M source (~227 GB) lives; skips the download if it's present.
|
|
10
|
+
Needs huggingface_hub and a write token (HF_TOKEN or `huggingface-cli login`).
|
|
11
|
+
"""
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import polars as pl
|
|
15
|
+
from brisc import SingleCell
|
|
16
|
+
from huggingface_hub import HfApi
|
|
17
|
+
|
|
18
|
+
REPO_ID = 'dashingcell/Parse_10K_PBMC_cytokines'
|
|
19
|
+
N_CELLS = 10_000
|
|
20
|
+
CONDITIONS = ['PBS', 'IFN-gamma']
|
|
21
|
+
OBS_COLUMNS = ['sample', 'donor', 'cell_type', 'treatment', 'cytokine']
|
|
22
|
+
|
|
23
|
+
data_dir = Path.home() / 'single-cell' / 'Parse'
|
|
24
|
+
src = data_dir / 'Parse_10M_PBMC_cytokines.h5ad'
|
|
25
|
+
out = data_dir / f'Parse_{N_CELLS // 1000}K_PBMC_cytokines.h5ad'
|
|
26
|
+
|
|
27
|
+
CARD = """\
|
|
28
|
+
---
|
|
29
|
+
license: other
|
|
30
|
+
license_name: parse-biosciences-data-terms
|
|
31
|
+
license_link: https://www.parsebiosciences.com/datasets/
|
|
32
|
+
pretty_name: Parse 10K PBMC Cytokines (IFN-γ vs PBS)
|
|
33
|
+
tags:
|
|
34
|
+
- single-cell
|
|
35
|
+
- scRNA-seq
|
|
36
|
+
- PBMC
|
|
37
|
+
- immunology
|
|
38
|
+
- cytokines
|
|
39
|
+
- brisc
|
|
40
|
+
size_categories:
|
|
41
|
+
- 1K<n<10K
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# Parse 10K PBMC Cytokines (IFN-γ vs PBS)
|
|
45
|
+
|
|
46
|
+
Balanced 10,000-cell subsample of the ~10 million cell cytokine stimulation
|
|
47
|
+
dataset from Parse Biosciences.
|
|
48
|
+
|
|
49
|
+
In the original experiment, peripheral blood mononuclear cells (PBMCs) from
|
|
50
|
+
twelve healthy donors were treated with either one of 90 different cytokines or
|
|
51
|
+
a phosphate-buffered saline (PBS) control for 24 hours, yielding
|
|
52
|
+
(90 + 1) × 12 = 1,092 experimental conditions.
|
|
53
|
+
|
|
54
|
+
For the brisc test suite, the data is restricted to the interferon-gamma
|
|
55
|
+
(IFN-γ) versus PBS contrast and subsampled to 5,000 cells per condition,
|
|
56
|
+
stratified by cell type. The result is 10,000 cells × 40,352 genes of raw
|
|
57
|
+
counts across 24 samples (12 donors × 2 conditions) and 18 annotated cell
|
|
58
|
+
types, with `obs` columns `sample`, `donor`, `cell_type`, `treatment`, and
|
|
59
|
+
`cytokine`.
|
|
60
|
+
|
|
61
|
+
## Source
|
|
62
|
+
|
|
63
|
+
- https://www.parsebiosciences.com/datasets/10-million-human-pbmcs-in-a-single-experiment/
|
|
64
|
+
- https://www.biorxiv.org/content/10.64898/2025.12.12.693897v1
|
|
65
|
+
|
|
66
|
+
All credit to Parse Biosciences.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def remove_unused_categories(s: pl.Series) -> pl.Expr:
|
|
70
|
+
"""polars-Enum version of pandas' remove_unused_categories: rebuild each
|
|
71
|
+
Enum from the values present."""
|
|
72
|
+
present = set(s.unique().to_list())
|
|
73
|
+
kept = [c for c in s.dtype.categories.to_list() if c in present]
|
|
74
|
+
return pl.col(s.name).cast(pl.Enum(kept))
|
|
75
|
+
|
|
76
|
+
# Fetch the 10M source once
|
|
77
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
if not src.exists():
|
|
79
|
+
url = 'https://parse-wget.s3.us-west-2.amazonaws.com/10m/' + src.name
|
|
80
|
+
subprocess.run(['wget', '-O', str(src), url], check=True)
|
|
81
|
+
|
|
82
|
+
# Build the balanced 10K fixture
|
|
83
|
+
sc = SingleCell(src, obs_columns=OBS_COLUMNS)\
|
|
84
|
+
.filter_obs(pl.col('cytokine').is_in(CONDITIONS))
|
|
85
|
+
half = N_CELLS // 2
|
|
86
|
+
pbs = sc.filter_obs(pl.col('cytokine') == 'PBS')\
|
|
87
|
+
.subsample_obs(n=half, by_column='cell_type', QC_column=None, seed=0)
|
|
88
|
+
ifn = sc.filter_obs(pl.col('cytokine') == 'IFN-gamma')\
|
|
89
|
+
.subsample_obs(n=half, by_column='cell_type', QC_column=None, seed=0)
|
|
90
|
+
fixture = pbs.concat_obs(ifn)
|
|
91
|
+
|
|
92
|
+
# Drop unused Enum categories (sample 1,092 -> 24, cytokine 91 -> 2)
|
|
93
|
+
enum_cols = [name for name, dt in fixture.obs.schema.items() if dt == pl.Enum]
|
|
94
|
+
fixture = fixture.with_columns_obs(
|
|
95
|
+
[remove_unused_categories(fixture.obs[c]) for c in enum_cols])
|
|
96
|
+
fixture.save(out, overwrite=True)
|
|
97
|
+
|
|
98
|
+
# Publish data + card to the HF dataset repo (no-op if unchanged)
|
|
99
|
+
api = HfApi()
|
|
100
|
+
api.create_repo(REPO_ID, repo_type='dataset', exist_ok=True)
|
|
101
|
+
api.upload_file(path_or_fileobj=str(out), path_in_repo=out.name,
|
|
102
|
+
repo_id=REPO_ID, repo_type='dataset')
|
|
103
|
+
api.upload_file(path_or_fileobj=CARD.encode(), path_in_repo='README.md',
|
|
104
|
+
repo_id=REPO_ID, repo_type='dataset')
|
|
105
|
+
print(f'Published to https://huggingface.co/datasets/{REPO_ID}')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|