def spatial_neighbors( adata: AnnData, spatial_key: str = Key.obsm.spatial, coord_type: Optional[Union[str, CoordType]] = None, n_rings: int = 1, n_neigh: int = 6, delaunay: bool = False, radius: Optional[float] = None, transform: Optional[Union[str, Transform]] = None, key_added: Optional[str] = None, ) -> None: """ Create a graph from spatial coordinates. Parameters ---------- %(adata)s %(spatial_key)s coord_type Type of coordinate system. Valid options are: - `{c.VISIUM!r}` - Visium coordinates. - `{c.GENERIC!r}` - generic coordinates. If `None`, use `{c.VISIUM!r}` if ``spatial_key`` is present in :attr:`anndata.AnnData.obsm`, otherwise use `{c.GENERIC!r}`. n_rings Number of rings of neighbors for Visium data. n_neigh Number of neighborhoods to consider for non-Visium data. delaunay Whether to compute the graph from Delaunay triangulation. radius Radius of neighbors for non-Visium data. transform Type of adjacency matrix transform. Valid options are: - `{t.SPECTRAL.s!r}` - spectral transformation of the adjacency matrix. - `{t.COSINE.s!r}` - cosine transformation of the adjacency matrix. - `{t.NONE.v}` - no transformation of the adjacency matrix. key_added Key which controls where the results are saved. Returns ------- Modifies the ``adata`` with the following keys: - :attr:`anndata.AnnData.obsp` ``['{{key_added}}_connectivities']`` - spatial connectivity matrix. - :attr:`anndata.AnnData.obsp` ``['{{key_added}}_distances']`` - spatial distances matrix. - :attr:`anndata.AnnData.uns` ``['{{key_added}}']`` - spatial neighbors dictionary. """ _assert_positive(n_rings, name="n_rings") _assert_positive(n_neigh, name="n_neigh") _assert_spatial_basis(adata, spatial_key) transform = Transform.NONE if transform is None else Transform(transform) if coord_type is None: coord_type = CoordType.VISIUM if Key.uns.spatial in adata.uns else CoordType.GENERIC else: coord_type = CoordType(coord_type) start = logg.info( f"Creating graph using `{coord_type}` coordinates and `{transform}` transform" ) coords = adata.obsm[spatial_key] if coord_type == CoordType.VISIUM: if n_rings > 1: Adj: csr_matrix = _build_connectivity(coords, 6, neigh_correct=True, set_diag=True, delaunay=delaunay, return_distance=False) Res = Adj Walk = Adj for i in range(n_rings - 1): Walk = Walk @ Adj with warnings.catch_warnings(): warnings.simplefilter("ignore", SparseEfficiencyWarning) Walk[Res.nonzero()] = 0.0 Walk.eliminate_zeros() Walk.data[:] = i + 2.0 Res = Res + Walk Adj = Res Adj.setdiag(0.0) Adj.eliminate_zeros() Dst = Adj.copy() Adj.data[:] = 1.0 else: Adj = _build_connectivity(coords, 6, neigh_correct=True, delaunay=delaunay) Dst = None elif coord_type == CoordType.GENERIC: Adj, Dst = _build_connectivity(coords, n_neigh, radius, delaunay=delaunay, return_distance=True) else: raise NotImplementedError(coord_type) # check transform if transform == Transform.SPECTRAL: Adj = _transform_a_spectral(Adj) elif transform == Transform.COSINE: Adj = _transform_a_cosine(Adj) elif transform == Transform.NONE: pass else: raise NotImplementedError( f"Transform `{transform}` is not yet implemented.") neighs_key = Key.uns.spatial_neighs(key_added) conns_key = Key.obsp.spatial_conn(key_added) dists_key = Key.obsp.spatial_dist(key_added) neighbors_dict = { "connectivities_key": conns_key, "params": { "n_neighbors": n_neigh, "coord_type": coord_type.v, "radius": radius, "transform": transform.v }, "distances_key": dists_key, } _save_data(adata, attr="obsp", key=conns_key, data=Adj) if Dst is not None: _save_data(adata, attr="obsp", key=dists_key, data=Dst, prefix=False) _save_data(adata, attr="uns", key=neighs_key, data=neighbors_dict, prefix=False, time=start)
def co_occurrence( adata: AnnData, cluster_key: str, spatial_key: str = Key.obsm.spatial, n_steps: int = 50, copy: bool = False, n_splits: Optional[int] = None, n_jobs: Optional[int] = None, backend: str = "loky", show_progress_bar: bool = True, ) -> Optional[Tuple[np.ndarray, np.ndarray]]: """ Compute co-occurrence probability of clusters. The co-occurrence is computed across ``n_steps`` distance thresholds in spatial dimensions. Parameters ---------- %(adata)s %(cluster_key)s %(spatial_key)s n_steps Number of distance thresholds at which co-occurrence is computed. %(copy)s n_splits Number of splits in which to divide the spatial coordinates in :attr:`anndata.AnnData.obsm` ``['{spatial_key}']``. %(parallelize)s Returns ------- If ``copy = True``, returns the co-occurrence probability and the distance thresholds intervals. Otherwise, modifies the ``adata`` with the following keys: - :attr:`anndata.AnnData.uns` ``['{cluster_key}_co_occurrence']['occ']`` - the co-occurrence probabilities across interval thresholds. - :attr:`anndata.AnnData.uns` ``['{cluster_key}_co_occurrence']['interval']`` - the distance thresholds computed at ``n_steps``. """ _assert_categorical_obs(adata, key=cluster_key) _assert_spatial_basis(adata, key=spatial_key) spatial = adata.obsm[spatial_key].astype(fp) original_clust = adata.obs[cluster_key] # find minimum, second minimum and maximum for thresholding thres_min, thres_max = _find_min_max(spatial) # annotate cluster idx clust_map = { v: i for i, v in enumerate(original_clust.cat.categories.values) } labs = np.array([clust_map[c] for c in original_clust], dtype=ip) labs_unique = np.array(list(clust_map.values()), dtype=ip) # create intervals thresholds interval = np.linspace(thres_min, thres_max, num=n_steps, dtype=fp) n_obs = spatial.shape[0] if n_splits is None: size_arr = (n_obs**2 * 4) / 1024 / 1024 # calc expected mem usage if size_arr > 2_000: s = 1 while 2_048 < (n_obs / s): s += 1 n_splits = s logg.warning( f"`n_splits` was automatically set to: {n_splits}\n" f"preventing a NxN with N={n_obs} distance matrix to be created" ) else: n_splits = 1
def ripley_k( adata: AnnData, cluster_key: str, spatial_key: str = Key.obsm.spatial, mode: str = "ripley", support: int = 100, copy: bool = False, ) -> Optional[pd.DataFrame]: r""" Calculate `Ripley's K <https://en.wikipedia.org/wiki/Spatial_descriptive_statistics#Ripley's_K_and_L_functions>`_ statistics for each cluster in the tissue coordinates. Parameters ---------- %(adata)s %(cluster_key)s %(spatial_key)s mode Keyword which indicates the method for edge effects correction. See :class:`astropy.stats.RipleysKEstimator` for valid options. support Number of points where Ripley's K is evaluated between a fixed radii with :math:`min=0`, :math:`max=\sqrt{{area \over 2}}`. %(copy)s Returns ------- If ``copy = True``, returns a :class:`pandas.DataFrame` with the following keys: - `'ripley_k'` - the Ripley's K statistic. - `'distance'` - set of distances where the estimator was evaluated. Otherwise, modifies the ``adata`` with the following key: - :attr:`anndata.AnnData.uns` ``['{{cluster_key}}_ripley_k']`` - the above mentioned dataframe. """ # noqa: D205, D400 try: # from pointpats import ripley, hull from astropy.stats import RipleysKEstimator except ImportError: raise ImportError( "Please install `astropy` as `pip install astropy`.") from None _assert_spatial_basis(adata, key=spatial_key) coord = adata.obsm[spatial_key] # set coordinates y_min = int(coord[:, 1].min()) y_max = int(coord[:, 1].max()) x_min = int(coord[:, 0].min()) x_max = int(coord[:, 0].max()) area = int((x_max - x_min) * (y_max - y_min)) r = np.linspace(0, (area / 2)**0.5, support) # set estimator Kest = RipleysKEstimator(area=area, x_max=x_max, y_max=y_max, x_min=x_min, y_min=y_min) df_lst = [] # TODO: how long does this take (i.e. does it make sense to measure the elapse time?) logg.info("Calculating Ripley's K") for c in adata.obs[cluster_key].unique(): idx = adata.obs[cluster_key].values == c coord_sub = coord[idx, :] est = Kest(data=coord_sub, radii=r, mode=mode) df_est = pd.DataFrame(np.stack([est, r], axis=1)) df_est.columns = ["ripley_k", "distance"] df_est[cluster_key] = c df_lst.append(df_est) df = pd.concat(df_lst, axis=0) # filter by min max dist minmax_dist = df.groupby(cluster_key)["ripley_k"].max().min() df = df[df.ripley_k < minmax_dist].copy() if copy: return df adata.uns[f"ripley_k_{cluster_key}"] = df _save_data(adata, attr="uns", key=Key.uns.ripley_k(cluster_key), data=df)
def generate_spot_crops( self, adata: AnnData, library_id: Optional[str] = None, spatial_key: str = Key.obsm.spatial, spot_scale: float = 1.0, obs_names: Optional[Iterable[Any]] = None, as_array: Union[str, bool] = False, return_obs: bool = False, **kwargs: Any, ) -> Union[Iterator["ImageContainer"], Iterator[np.ndarray], Iterator[Tuple[np.ndarray, ...]], Iterator[Dict[str, np.ndarray]], ]: """ Iterate over :attr:`adata.obs_names` and extract crops. Implemented for 10X spatial datasets. Parameters ---------- %(adata)s library_id Key in :attr:`anndata.AnnData.uns` ``['{spatial_key}']`` used to get the spot diameter. %(spatial_key)s spot_scale Scaling factor for the spot diameter. Larger values mean more context. obs_names Observations from :attr:`adata.obs_names` for which to generate the crops. If `None`, all names are used. %(as_array)s return_obs Whether to also yield names from ``obs_names``. kwargs Keyword arguments for :meth:`crop_center`. Yields ------ If ``return_obs = True``, yields a :class:`tuple` ``(crop, obs_name)``. Otherwise, yields just the crops. The type of the crops depends on ``as_array``. """ self._assert_not_empty() _assert_positive(spot_scale, name="scale") _assert_spatial_basis(adata, spatial_key) library_id = Key.uns.library_id(adata, spatial_key=spatial_key, library_id=library_id) if obs_names is None: obs_names = adata.obs_names obs_names = _assert_non_empty_sequence(obs_names, name="observations") adata = adata[obs_names, :] spatial = adata.obsm[spatial_key][:, :2] diameter = adata.uns[spatial_key][library_id]["scalefactors"][ "spot_diameter_fullres"] radius = int(round(diameter // 2 * spot_scale)) for i, obs in enumerate(adata.obs_names): crop = self.crop_center(y=spatial[i][1], x=spatial[i][0], radius=radius, **kwargs) crop.data.attrs[Key.img.obs] = obs crop = crop._maybe_as_array(as_array) yield (crop, obs) if return_obs else crop