vima-spatial 0.2.7__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.
Files changed (40) hide show
  1. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/PKG-INFO +1 -1
  2. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/setup.cfg +1 -1
  3. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/ingest/dimreduce.py +108 -49
  4. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/ingest/ingest.py +1 -1
  5. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/PKG-INFO +1 -1
  6. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/README.md +0 -0
  7. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/pyproject.toml +0 -0
  8. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/__init__.py +0 -0
  9. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/_settings.py +0 -0
  10. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/cc.py +0 -0
  11. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/data/__init__.py +0 -0
  12. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/data/download.py +0 -0
  13. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/data/patchcollection.py +0 -0
  14. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/data/samples.py +0 -0
  15. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/fingerprints.py +0 -0
  16. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/ingest/__init__.py +0 -0
  17. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/ingest/nonst.py +0 -0
  18. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/ingest/st.py +0 -0
  19. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/ingest/util.py +0 -0
  20. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/models/__init__.py +0 -0
  21. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/models/resnet_vae.py +0 -0
  22. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/models/resnetlight_decoder.py +0 -0
  23. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/models/resnetlight_encoder.py +0 -0
  24. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/models/simple_vae.py +0 -0
  25. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/models/vae.py +0 -0
  26. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/patchfeatures.py +0 -0
  27. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/train/__init__.py +0 -0
  28. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/train/logging.py +0 -0
  29. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/train/training.py +0 -0
  30. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/vis/__init__.py +0 -0
  31. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/vis/features.py +0 -0
  32. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/vis/patches.py +0 -0
  33. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/vis/patchexamples.py +0 -0
  34. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/vis/spatial.py +0 -0
  35. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima/vis/umaps.py +0 -0
  36. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/SOURCES.txt +0 -0
  37. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/dependency_links.txt +0 -0
  38. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/requires.txt +0 -0
  39. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/src/vima_spatial.egg-info/top_level.txt +0 -0
  40. {vima_spatial-0.2.7 → vima_spatial-0.2.8}/tests/test_ra_regression.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vima-spatial
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: variational inference-based microniche analysis
5
5
  Home-page: https://github.com/yakirr/vima
6
6
  Author: Yakir Reshef
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = vima-spatial
3
- version = 0.2.7
3
+ version = 0.2.8
4
4
  author = Yakir Reshef
5
5
  author_email = yreshef@broadinstitute.org
6
6
  description = variational inference-based microniche analysis
@@ -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, standardizes them with the stored
20
- per-marker means/stds, builds metapixels, and randomly downsamples each
21
- sample to roughly ``total_n_metapixels // len(sids)`` metapixels. Warns if a
22
- sample's markers differ from the first sample's.
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
- all_metapixels[sid], all_npixels[sid] = metapixels(da, mask_da)
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
- if len(all_metapixels[sid]) > nmp_per_sample:
80
- ix = np.random.choice(len(all_metapixels[sid]), nmp_per_sample, replace=False)
81
- all_metapixels[sid] = all_metapixels[sid].iloc[ix]
82
- all_npixels[sid] = all_npixels[sid][ix]
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
- Sums each marker over a 5x5 window centered on every pixel and divides by
103
- the number of non-empty (masked) pixels contributing to that window, so each
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
- ``(metapixel_df, npixels)``: a marker-columned DataFrame of retained
111
- metapixels and the per-metapixel count of contributing non-empty pixels.
121
+ ``(metapixels, npixels)``: a ``(metapixel, marker)`` float32 array and
122
+ the per-metapixel count of contributing non-empty pixels.
112
123
  """
113
- markers = s.marker.values
114
-
115
- # make metapixels and compute how many non-empty pixels and transcripts are in each metapixel
116
- kernel = np.ones((5, 5), np.float32)
117
- mp = convolve(s.data, kernel[:, :, None], mode="constant")
118
- npixels = convolve(mask.data.astype('float32'), kernel, mode="constant")
119
-
120
- # filter out metapixels with few non-empty pixels
121
- metapixels_mask = npixels > npixels_thresh
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
- return pd.DataFrame(data=mp[metapixels_mask] / npixels[metapixels_mask][:,None], columns=markers), npixels[metapixels_mask]
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, C, allmp)``: the gene-by-component loading matrix, the
146
- feature correlation matrix, and the standardized metapixel AnnData.
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
- allmp = pd.concat(mps)
150
- allmp -= allmp.values.mean(axis=0, dtype=np.float64)
151
- allmp /= allmp.values.std(axis=0, dtype=np.float64)
152
- allmp = allmp.fillna(0)
153
- allmp.index = np.arange(len(allmp)).astype(str)
154
- allmp = ad.AnnData(X=allmp)
155
- C = np.corrcoef(allmp.X[::max(1,(len(allmp)//50000))].T)
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, C, allmp
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
- sid_labels = []
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 = xr.DataArray(da.attrs['means'], dims='marker')
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
- pl_pca = pl.dot(pcloadings)
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
- sid_labels.append(np.full(pl_pca.shape[0], sid, dtype=object))
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
- sid_labels = np.concatenate(sid_labels)
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, pcloadings.shape[1] + 1)]
281
+ columns=[f'PC{i}' for i in range(1, loadings.shape[1] + 1)]
228
282
  )
229
- allpixels_pca['sid'] = sid_labels
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, C, allmp = dimreduce.pca_metapixels(metapixels.values(), nmetamarkers)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vima-spatial
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: variational inference-based microniche analysis
5
5
  Home-page: https://github.com/yakirr/vima
6
6
  Author: Yakir Reshef
File without changes