quadsv.utils
============

.. py:module:: quadsv.utils

.. autoapi-nested-parse::

   General-purpose helpers: grid/coordinate generators, periodic-domain distance
   matrices, Visium I/O, and the Benjamini–Hochberg correction used by every
   detector / comparator.



Attributes
----------

.. autoapisummary::

   quadsv.utils.VISIUM_V1_SPOT_SPACING_UM


Functions
---------

.. autoapisummary::

   quadsv.utils.compute_torus_distance_matrix
   quadsv.utils.convert_visium_to_physical
   quadsv.utils.get_rect_coords
   quadsv.utils.get_visium_coords
   quadsv.utils.load_visium_sample
   quadsv.utils.visium_hex_spacing_um
   quadsv.utils.visium_to_grid


Module Contents
---------------

.. py:function:: compute_torus_distance_matrix(phys_coords, domain_dims)

   Pairwise Euclidean distances on a rectangular torus.

   Each pair's distance is the minimum over direct and wrap-around offsets:
   ``d_wrapped = min(|Δp|, D - |Δp|)`` per axis.

   :param phys_coords: ``(n, 2)`` ``[[y, x], ...]``.
   :type phys_coords: np.ndarray
   :param domain_dims: Periodic domain extent ``(height, width)``.
   :type domain_dims: tuple of float

   :returns: ``(n, n)`` pairwise distances.
   :rtype: np.ndarray

   .. rubric:: Examples

   >>> coords = np.array([[0.0, 0.0], [1.0, 0.0], [9.9, 0.0]])
   >>> dists = compute_torus_distance_matrix(coords, (10.0, 10.0))
   >>> round(dists[0, 2], 2)
   0.1


.. py:function:: convert_visium_to_physical(coords)

   Convert integer Visium ``(row, col)`` indices to physical ``(y, x)``.

   Hexagonal geometry with equilateral triangles: ``Δcol = 2 → Δx = 1`` and
   ``Δrow = 1 → Δy = √3/2``.

   :param coords: ``(n, 2)`` ``[[row, col], ...]`` (integers, from
                  :func:`get_visium_coords` or ``adata.obs[['array_row','array_col']]``).
   :type coords: np.ndarray

   :returns: ``(n, 2)`` ``[[y, x], ...]`` with ``y = row · √3/2``, ``x = col / 2``.
   :rtype: np.ndarray

   .. rubric:: Examples

   >>> coords = np.array([[0, 0], [0, 2], [1, 1]])
   >>> convert_visium_to_physical(coords)
   array([[0.       , 0.       ],
          [0.       , 1.       ],
          [0.8660254, 0.5      ]])


.. py:function:: get_rect_coords(n_rows = 32, n_cols = 32)

   Generate rectangular grid coordinates with unit spacing.

   :param n_rows: Grid dimensions.
   :type n_rows: int
   :param n_cols: Grid dimensions.
   :type n_cols: int

   :returns: * **coords** (*np.ndarray*) -- ``(n, 2)`` row-major ``[[y, x], ...]`` with ``n = n_rows · n_cols``.
             * **grid_dims** (*tuple of int*) -- ``(n_rows, n_cols)``.

   .. rubric:: Examples

   >>> coords, dims = get_rect_coords(n_rows=10, n_cols=10)
   >>> coords.shape
   (100, 2)
   >>> dims
   (10, 10)


.. py:function:: get_visium_coords(n_rows = 78, n_cols = 64)

   Generate Visium-like hexagonal array indices.

   Offset-column layout: even rows use even ``array_col`` indices, odd rows
   use odd indices. Each row holds ``n_cols`` spots.

   :param n_rows: Number of array rows (78 for a full Visium v1 slide).
   :type n_rows: int, default 78
   :param n_cols: Spots per row.
   :type n_cols: int, default 64

   :returns: * **coords** (*np.ndarray*) -- ``(n_rows · n_cols, 2)`` integer ``[[row, col], ...]``.
             * **grid_dims** (*tuple of int*) -- ``(n_rows, n_cols)``.

   .. seealso::

      :obj:`convert_visium_to_physical`
          Map these indices to physical ``(y, x)``.

   .. rubric:: Examples

   >>> coords, dims = get_visium_coords(n_rows=78, n_cols=64)
   >>> coords.shape[0]
   4992


.. py:function:: load_visium_sample(path, matrix_path = None, in_tissue_only = True)

   Load a Visium Space Ranger output directory as :class:`anndata.AnnData`.

   Accepts either the flat layout (``<path>/<sample>_filtered_feature_bc_matrix.h5``
   + ``<path>/spatial/``) or the canonical Space Ranger ``outs/`` layout
   (``<path>/filtered_feature_bc_matrix.h5`` + ``<path>/spatial/``); auto-detects.

   :param path: Directory holding the filtered matrix and ``spatial/`` subfolder.
   :type path: str or Path
   :param matrix_path: Explicit path to the filtered ``.h5`` matrix. Defaults to
                       ``<path>/filtered_feature_bc_matrix.h5`` or ``*_filtered_feature_bc_matrix.h5``.
   :type matrix_path: str or Path, optional
   :param in_tissue_only: Restrict to spots with ``in_tissue == 1``.
   :type in_tissue_only: bool, default True

   :returns: ``adata.obs`` has ``in_tissue``, ``array_row``, ``array_col``,
             ``pxl_row_in_fullres``, ``pxl_col_in_fullres``.
             ``adata.obsm['spatial']`` holds ``(pxl_col, pxl_row)`` full-resolution
             pixel coords. ``adata.uns['spatial']`` stores ``scalefactors_json.json``
             and the source path.
   :rtype: anndata.AnnData

   :raises FileNotFoundError: If the matrix or spatial folder cannot be located.
   :raises ImportError: If :mod:`anndata` / :mod:`scanpy` are not installed.


.. py:function:: visium_hex_spacing_um(spot_spacing_um = VISIUM_V1_SPOT_SPACING_UM, grid = 'dense')

   Physical ``(dy, dx)`` per grid cell for a Visium hex raster.

   :param spot_spacing_um: Center-to-center distance between adjacent spots (100 μm for Visium v1).
   :type spot_spacing_um: float, default 100.0
   :param grid: Rasterization mode — see :func:`visium_to_grid`.
   :type grid: {'dense', 'collapsed'}, default 'dense'

   :returns: ``(dy, dx)`` in micrometres per grid cell.
   :rtype: tuple of float

   :raises ValueError: If ``grid`` is unknown.


.. py:function:: visium_to_grid(adata, genes = None, layer = None, grid = 'dense', fill = 'nearest', spot_spacing_um = VISIUM_V1_SPOT_SPACING_UM, max_row = None, max_col = None)

   Rasterize a Visium ``adata`` onto a regular rectangular grid.

   Rasterization modes:

   - ``grid='dense'`` (default): ``(78, 128)`` array filled from real spots on
     half the cells; remaining cells imputed from their two nearest hex
     neighbours on the same row. Physical spacing ``(100·√3/2, 50)`` μm.
     Exact hex geometry preserved.
   - ``grid='collapsed'``: ``(78, 64)`` array using ``array_col // 2`` as
     column index. Physical spacing ``(100·√3/2, 100)`` μm. Faster, but
     drops the 50 μm horizontal offset between alternating rows
     (≤5 % geometric distortion).

   :param adata: Must carry ``adata.obs['array_row']`` and ``adata.obs['array_col']``
                 (e.g. from :func:`load_visium_sample`).
   :type adata: anndata.AnnData
   :param genes: Gene subset. ``None`` → all ``adata.var_names``.
   :type genes: list of str, optional
   :param layer: ``adata.layers`` key. ``None`` → ``adata.X``.
   :type layer: str, optional
   :param grid:
   :type grid: {'dense', 'collapsed'}, default 'dense'
   :param fill: Empty-cell handling. ``'nearest'`` averages the two row-neighbour spots
                (avoids FFT aliasing); ``'zero'`` leaves cells at 0. Ignored when
                ``grid='collapsed'``.
   :type fill: {'nearest', 'zero'}, default 'nearest'
   :param spot_spacing_um:
   :type spot_spacing_um: float, default 100.0
   :param max_row: Pad output to a common size; defaults to the maxima observed in ``adata``.
   :type max_row: int, optional
   :param max_col: Pad output to a common size; defaults to the maxima observed in ``adata``.
   :type max_col: int, optional

   :returns: * **grid_arr** (*np.ndarray*) -- ``(n_genes, ny, nx)`` float64, ready for
               :func:`quadsv.power_spectrum_2d`.
             * **spacing_um** (*tuple of float*) -- ``(dy, dx)`` in μm.

   :raises KeyError: If ``array_row`` / ``array_col`` are absent from ``adata.obs``.
   :raises ValueError: If ``grid`` / ``fill`` are unknown, or ``layer`` is missing.


.. py:data:: VISIUM_V1_SPOT_SPACING_UM
   :type:  float
   :value: 100.0


