vima-spatial 0.2.6__tar.gz → 0.2.8__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.
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/PKG-INFO +1 -1
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/setup.cfg +1 -1
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/_settings.py +37 -9
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/ingest/dimreduce.py +108 -49
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/ingest/ingest.py +1 -1
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/PKG-INFO +1 -1
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/README.md +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/pyproject.toml +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/__init__.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/cc.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/data/__init__.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/data/download.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/data/patchcollection.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/data/samples.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/fingerprints.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/ingest/__init__.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/ingest/nonst.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/ingest/st.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/ingest/util.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/models/__init__.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/models/resnet_vae.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/models/resnetlight_decoder.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/models/resnetlight_encoder.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/models/simple_vae.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/models/vae.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/patchfeatures.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/train/__init__.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/train/logging.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/train/training.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/vis/__init__.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/vis/features.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/vis/patches.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/vis/patchexamples.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/vis/spatial.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima/vis/umaps.py +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/SOURCES.txt +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/dependency_links.txt +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/requires.txt +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/top_level.txt +0 -0
- {vima_spatial-0.2.6 → vima_spatial-0.2.8}/tests/test_ra_regression.py +0 -0
|
@@ -98,6 +98,32 @@ _LOGGING_LEVELS = {
|
|
|
98
98
|
|
|
99
99
|
logger = logging.getLogger("vima")
|
|
100
100
|
|
|
101
|
+
# matplotlib backends that render to a file and cannot display anything; a
|
|
102
|
+
# terminal with no display falls back to "agg". Names are matched lowercase.
|
|
103
|
+
# Fallback for the query below, and what matplotlib has reported for years.
|
|
104
|
+
_FILE_ONLY_BACKENDS = {"agg", "cairo", "pdf", "pgf", "ps", "svg", "template"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _file_only_backends():
|
|
108
|
+
"""Names of the backends that can only write to a file.
|
|
109
|
+
|
|
110
|
+
Asked of matplotlib where it exposes the list (3.9+), so a backend added
|
|
111
|
+
later is picked up, with :data:`_FILE_ONLY_BACKENDS` as the fallback.
|
|
112
|
+
|
|
113
|
+
Deliberately phrased as "which backends are file-only" rather than "which
|
|
114
|
+
are interactive", even though matplotlib offers both: it classifies
|
|
115
|
+
Jupyter's inline backend as *non*-interactive, since it drives no GUI event
|
|
116
|
+
loop -- yet inline plots do appear in front of the user, which is the only
|
|
117
|
+
thing we need to know. ``resolve_backend`` would answer that question, but
|
|
118
|
+
it imports the backend module and raises on unknown names, so we avoid it.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
from matplotlib.backends.registry import backend_registry, BackendFilter
|
|
122
|
+
|
|
123
|
+
return set(backend_registry.list_builtin(BackendFilter.NON_INTERACTIVE))
|
|
124
|
+
except Exception:
|
|
125
|
+
return _FILE_ONLY_BACKENDS
|
|
126
|
+
|
|
101
127
|
# ANSI colors: debug messages are grayed out, results are green.
|
|
102
128
|
_GRAY, _GREEN, _RESET = "\033[90m", "\033[32m", "\033[0m"
|
|
103
129
|
|
|
@@ -200,19 +226,21 @@ class Settings:
|
|
|
200
226
|
def _display_available(self):
|
|
201
227
|
"""Whether the active matplotlib backend can actually show a figure.
|
|
202
228
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
A backend is a display unless it is one of the file-only ones (see
|
|
230
|
+
:func:`_file_only_backends`). Deciding it that way around means
|
|
231
|
+
anything unrecognized -- a third-party or future backend, or a
|
|
232
|
+
``module://`` backend from an embedding host such as Jupyter's
|
|
233
|
+
``matplotlib_inline`` or ``ipympl`` -- counts as a display, so the
|
|
234
|
+
worst case is plain ``plt.show()``, exactly as before this setting
|
|
235
|
+
existed.
|
|
209
236
|
"""
|
|
210
237
|
import matplotlib
|
|
211
238
|
|
|
212
|
-
|
|
213
|
-
|
|
239
|
+
try:
|
|
240
|
+
backend = matplotlib.get_backend()
|
|
241
|
+
except Exception: # backend resolution can fail in odd environments
|
|
214
242
|
return True
|
|
215
|
-
return backend.lower() in
|
|
243
|
+
return backend.lower() not in _file_only_backends()
|
|
216
244
|
|
|
217
245
|
def _saving_plots(self):
|
|
218
246
|
"""Whether :meth:`show` should save rather than display."""
|
|
@@ -16,10 +16,10 @@ def metapixels_allsamples(normedpixelsdir, masksdir, sids, total_n_metapixels):
|
|
|
16
16
|
"""
|
|
17
17
|
Pool metapixels across all samples for a more robust PCA fit.
|
|
18
18
|
|
|
19
|
-
Loads each sample's normalized pixels,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
Loads each sample's normalized pixels, builds roughly
|
|
20
|
+
``total_n_metapixels // len(sids)`` randomly chosen metapixels from it, and
|
|
21
|
+
standardizes them with the stored per-marker means/stds. Warns if a sample's
|
|
22
|
+
markers differ from the first sample's.
|
|
23
23
|
|
|
24
24
|
Parameters
|
|
25
25
|
----------
|
|
@@ -55,7 +55,7 @@ def metapixels_allsamples(normedpixelsdir, masksdir, sids, total_n_metapixels):
|
|
|
55
55
|
for i, sid in enumerate(settings.progress(sids, name='creating metapixels')):
|
|
56
56
|
da = xr.open_dataarray(f'{normedpixelsdir}/{sid}.nc')
|
|
57
57
|
mask_da = xr.open_dataarray(f'{masksdir}/{sid}.nc')
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
# ensure same markers in same order in all files
|
|
60
60
|
markers = list(da.marker.values)
|
|
61
61
|
if ref_markers is None:
|
|
@@ -68,18 +68,21 @@ def metapixels_allsamples(normedpixelsdir, masksdir, sids, total_n_metapixels):
|
|
|
68
68
|
f'{len(missing)} missing, {len(extra)} extra vs {ref_sid}')
|
|
69
69
|
logger.warning(f'{sid} has different markers ({len(markers)}) '
|
|
70
70
|
f'than {ref_sid} ({len(ref_markers)}): {detail}')
|
|
71
|
-
|
|
72
|
-
means = xr.DataArray(da.attrs['means'], dims='marker')
|
|
73
|
-
stds = xr.DataArray(da.attrs['stds'], dims='marker')
|
|
74
|
-
da = ((da - means) / stds).where(mask_da, 0)
|
|
75
71
|
|
|
76
|
-
|
|
72
|
+
means, stds = da.attrs['means'], da.attrs['stds']
|
|
73
|
+
|
|
74
|
+
# metapixels are built from the un-standardized pixels and standardized
|
|
75
|
+
# afterward: averaging over a window and the affine (x-mean)/std commute,
|
|
76
|
+
# so this is identical to standardizing the full array first but avoids
|
|
77
|
+
# materializing several (y, x, marker)-sized temporaries.
|
|
78
|
+
mp, all_npixels[sid] = metapixels(da, mask_da, n_metapixels=nmp_per_sample)
|
|
77
79
|
da.close(); mask_da.close()
|
|
78
80
|
del da, mask_da
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
|
|
82
|
+
mp -= means
|
|
83
|
+
mp /= stds
|
|
84
|
+
all_metapixels[sid] = pd.DataFrame(data=mp, columns=markers)
|
|
85
|
+
del mp
|
|
83
86
|
|
|
84
87
|
# visualize distribution of num non-empty pixels per metapixel in this sample
|
|
85
88
|
if settings.show_plots():
|
|
@@ -95,33 +98,61 @@ def metapixels_allsamples(normedpixelsdir, masksdir, sids, total_n_metapixels):
|
|
|
95
98
|
|
|
96
99
|
return all_metapixels, all_npixels
|
|
97
100
|
|
|
98
|
-
def metapixels(s, mask, npixels_thresh=0):
|
|
101
|
+
def metapixels(s, mask, npixels_thresh=0, n_metapixels=None, window=5):
|
|
99
102
|
"""
|
|
100
103
|
Pool each pixel with its neighbors into a metapixel.
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
Averages each marker over a ``window``-by-``window`` window centered on a
|
|
106
|
+
pixel, using only the non-empty (masked) pixels in that window, so each
|
|
104
107
|
metapixel is the average over its non-empty neighbors. Metapixels with at
|
|
105
108
|
most ``npixels_thresh`` contributing pixels are dropped.
|
|
106
109
|
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
n_metapixels
|
|
113
|
+
If given, uniformly sample at most this many metapixel centers and
|
|
114
|
+
compute only those. Since the caller typically keeps a small random
|
|
115
|
+
subset anyway, this avoids convolving the whole (y, x, marker) array,
|
|
116
|
+
which dominates the cost for large marker panels.
|
|
117
|
+
|
|
107
118
|
Returns
|
|
108
119
|
-------
|
|
109
120
|
tuple
|
|
110
|
-
``(
|
|
111
|
-
|
|
121
|
+
``(metapixels, npixels)``: a ``(metapixel, marker)`` float32 array and
|
|
122
|
+
the per-metapixel count of contributing non-empty pixels.
|
|
112
123
|
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
npixels = convolve(mask.
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
|
|
124
|
+
mask = mask.data
|
|
125
|
+
H, W = mask.shape
|
|
126
|
+
|
|
127
|
+
# how many non-empty pixels contribute to each candidate metapixel (cheap: 2D only)
|
|
128
|
+
kernel = np.ones((window, window), np.float32)
|
|
129
|
+
npixels = convolve(mask.astype(np.float32), kernel, mode="constant")
|
|
130
|
+
|
|
131
|
+
# pick the metapixel centers, sampling before doing any work over markers
|
|
132
|
+
centers = np.flatnonzero(npixels.ravel() > npixels_thresh)
|
|
133
|
+
if n_metapixels is not None and len(centers) > n_metapixels:
|
|
134
|
+
centers = centers[np.random.choice(len(centers), n_metapixels, replace=False)]
|
|
135
|
+
npixels = npixels.ravel()[centers]
|
|
136
|
+
|
|
137
|
+
# sum each window by gathering its non-empty pixels, one neighbor offset at a time
|
|
138
|
+
data = s.data.reshape(H * W, -1)
|
|
139
|
+
mask = mask.ravel()
|
|
140
|
+
r, c = np.divmod(centers, W)
|
|
141
|
+
mp = np.zeros((len(centers), data.shape[1]), np.float32)
|
|
142
|
+
rad = window // 2
|
|
143
|
+
for dr in range(-rad, rad + 1):
|
|
144
|
+
rr = r + dr
|
|
145
|
+
for dc in range(-rad, rad + 1):
|
|
146
|
+
cc = c + dc
|
|
147
|
+
neighbor = rr * W + cc
|
|
148
|
+
contributes = (rr >= 0) & (rr < H) & (cc >= 0) & (cc < W)
|
|
149
|
+
contributes &= mask[np.where(contributes, neighbor, 0)]
|
|
150
|
+
i = np.flatnonzero(contributes)
|
|
151
|
+
mp[i] += data[neighbor[i]]
|
|
122
152
|
|
|
123
153
|
# divide each metapixel by the # of non-empty pixels that contributed to it and return
|
|
124
|
-
|
|
154
|
+
mp /= npixels[:, None]
|
|
155
|
+
return mp, npixels
|
|
125
156
|
|
|
126
157
|
# mps should be an array of dataframes containing metapixels
|
|
127
158
|
def pca_metapixels(mps, k):
|
|
@@ -142,17 +173,33 @@ def pca_metapixels(mps, k):
|
|
|
142
173
|
Returns
|
|
143
174
|
-------
|
|
144
175
|
tuple
|
|
145
|
-
``(loadings,
|
|
146
|
-
|
|
176
|
+
``(loadings, allmp)``: the gene-by-component loading matrix and the
|
|
177
|
+
standardized metapixel AnnData.
|
|
147
178
|
"""
|
|
148
179
|
logger.info('merging and standardizing metapixels')
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
180
|
+
mps = list(mps)
|
|
181
|
+
markers = mps[0].columns
|
|
182
|
+
n = sum(len(mp) for mp in mps)
|
|
183
|
+
|
|
184
|
+
# merge into one preallocated float32 matrix and standardize it in place,
|
|
185
|
+
# accumulating the moments in float64. Doing this in pandas instead upcasts
|
|
186
|
+
# the whole matrix to float64 and copies it once per operation, which at
|
|
187
|
+
# these sizes costs more than the PCA itself.
|
|
188
|
+
allmp = np.empty((n, len(markers)), np.float32)
|
|
189
|
+
i = 0
|
|
190
|
+
for mp in mps:
|
|
191
|
+
allmp[i:i+len(mp)] = mp.to_numpy(np.float32, copy=False)
|
|
192
|
+
i += len(mp)
|
|
193
|
+
del mps
|
|
194
|
+
|
|
195
|
+
allmp -= (allmp.sum(axis=0, dtype=np.float64) / n).astype(np.float32)
|
|
196
|
+
stds = np.sqrt(np.einsum('ij,ij->j', allmp, allmp, dtype=np.float64) / n)
|
|
197
|
+
stds[stds == 0] = 1 # constant features stay exactly 0, as the old fillna(0) left them
|
|
198
|
+
allmp /= stds.astype(np.float32)
|
|
199
|
+
|
|
200
|
+
allmp = ad.AnnData(X=allmp,
|
|
201
|
+
obs=pd.DataFrame(index=np.arange(n).astype(str)),
|
|
202
|
+
var=pd.DataFrame(index=markers))
|
|
156
203
|
logger.info(f'Metapixel matrix: {allmp.shape[0]:,} pixels × {allmp.shape[1]} features')
|
|
157
204
|
|
|
158
205
|
logger.info('performing PCA...')
|
|
@@ -176,7 +223,7 @@ def pca_metapixels(mps, k):
|
|
|
176
223
|
plt.xticks(range(len(loadings.columns)), loadings.columns, rotation=90)
|
|
177
224
|
settings.show('pc_loadings')
|
|
178
225
|
|
|
179
|
-
return loadings,
|
|
226
|
+
return loadings, allmp
|
|
180
227
|
|
|
181
228
|
def pca_pixels(normedpixelsdir, masksdir, pcloadings, sids):
|
|
182
229
|
"""
|
|
@@ -193,16 +240,18 @@ def pca_pixels(normedpixelsdir, masksdir, pcloadings, sids):
|
|
|
193
240
|
column giving the source sample.
|
|
194
241
|
"""
|
|
195
242
|
pcs = []
|
|
196
|
-
|
|
243
|
+
sid_codes = []
|
|
244
|
+
# project in float32: a DataFrame (or float64) right-hand side silently
|
|
245
|
+
# promotes the result, doubling both the projection cost and the size of
|
|
246
|
+
# the returned table, which has a row per pixel in the whole dataset
|
|
247
|
+
loadings = np.ascontiguousarray(np.asarray(pcloadings), dtype=np.float32)
|
|
197
248
|
|
|
198
249
|
logger.info('Applying PCA projection to each sample')
|
|
199
|
-
for sid in settings.progress(sids, name='pixels -> PCA space'):
|
|
250
|
+
for code, sid in enumerate(settings.progress(sids, name='pixels -> PCA space')):
|
|
200
251
|
da = xr.open_dataarray(f'{normedpixelsdir}/{sid}.nc')
|
|
201
252
|
mask_da = xr.open_dataarray(f'{masksdir}/{sid}.nc')
|
|
202
253
|
|
|
203
|
-
means =
|
|
204
|
-
stds = xr.DataArray(da.attrs['stds'], dims='marker')
|
|
205
|
-
da = ((da - means) / stds).where(mask_da, 0)
|
|
254
|
+
means, stds = da.attrs['means'], da.attrs['stds']
|
|
206
255
|
|
|
207
256
|
# load raw arrays and close before dtype conversion so we never hold
|
|
208
257
|
# two full (H × W × n_genes) copies simultaneously
|
|
@@ -213,19 +262,29 @@ def pca_pixels(normedpixelsdir, masksdir, pcloadings, sids):
|
|
|
213
262
|
pl = data.astype(np.float32, copy=False)[mask]
|
|
214
263
|
del data, mask; gc.collect()
|
|
215
264
|
|
|
216
|
-
|
|
265
|
+
# standardize the non-empty pixels only, rather than the full
|
|
266
|
+
# (y, x, marker) array; empty pixels are dropped by the mask anyway
|
|
267
|
+
pl -= means
|
|
268
|
+
pl /= stds
|
|
269
|
+
|
|
270
|
+
pl_pca = pl.dot(loadings)
|
|
217
271
|
pcs.append(pl_pca)
|
|
218
|
-
|
|
272
|
+
sid_codes.append(np.full(pl_pca.shape[0], code, dtype=np.int32))
|
|
219
273
|
del pl; gc.collect()
|
|
220
274
|
|
|
221
275
|
# concatenate
|
|
222
276
|
pcs = np.vstack(pcs)
|
|
223
|
-
|
|
277
|
+
sid_codes = np.concatenate(sid_codes)
|
|
224
278
|
|
|
225
279
|
allpixels_pca = pd.DataFrame(
|
|
226
280
|
pcs,
|
|
227
|
-
columns=[f'PC{i}' for i in range(1,
|
|
281
|
+
columns=[f'PC{i}' for i in range(1, loadings.shape[1] + 1)]
|
|
228
282
|
)
|
|
229
|
-
|
|
283
|
+
# categorical rather than an object column: one code per pixel instead of
|
|
284
|
+
# one pointer, over tens of millions of rows
|
|
285
|
+
# drop categories for samples that contributed no pixels, so downstream
|
|
286
|
+
# get_dummies (Harmony) never sees an all-zero batch column
|
|
287
|
+
allpixels_pca['sid'] = pd.Categorical.from_codes(
|
|
288
|
+
sid_codes, categories=list(sids)).remove_unused_categories()
|
|
230
289
|
|
|
231
290
|
return allpixels_pca
|
|
@@ -149,7 +149,7 @@ def pca_pixels(outdir, repname, nmetamarkers=10, npixels_to_plot=50000,
|
|
|
149
149
|
total_n_metapixels=total_n_metapixels)
|
|
150
150
|
|
|
151
151
|
# PCA the metapixels
|
|
152
|
-
loadings,
|
|
152
|
+
loadings, allmp = dimreduce.pca_metapixels(metapixels.values(), nmetamarkers)
|
|
153
153
|
loadings.to_feather(f'{processeddir}/_pcloadings.feather')
|
|
154
154
|
del metapixels, allmp; gc.collect()
|
|
155
155
|
|
|
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
|