from typing import Tuple
import numpy as np
[docs]
def get_rect_coords(
n_rows: int = 32,
n_cols: int = 32
) -> Tuple[np.ndarray, Tuple[int, int]]:
"""
Generate rectangular grid coordinates with unit spacing.
Constructs a regular rectangular grid suitable for spatial testing
on structured data (e.g., image pixels or tissue samples on a regular lattice).
Parameters
----------
n_rows : int, default 32
Number of rows in the rectangular grid.
n_cols : int, default 32
Number of columns in the rectangular grid.
Returns
-------
coords : np.ndarray
Grid coordinates of shape (N, 2) where N = n_rows × n_cols.
Format: [[y₀, x₀], [y₁, x₁], ...] in row-major order.
grid_dims : tuple of int
(n_rows, n_cols) - the grid dimensions.
Notes
-----
Coordinates use (row, column) indexing convention where row corresponds
to the y-axis and column to the x-axis.
Examples
--------
>>> coords, dims = get_rect_coords(n_rows=10, n_cols=10)
>>> coords.shape
(100, 2)
>>> dims
(10, 10)
"""
y = np.arange(n_rows)
x = np.arange(n_cols)
yy, xx = np.meshgrid(y, x, indexing='ij')
return np.column_stack([yy.ravel(), xx.ravel()]), (n_rows, n_cols)
[docs]
def get_visium_coords(
n_rows: int = 78,
n_cols: int = 64
) -> Tuple[np.ndarray, Tuple[int, int]]:
"""
Generate Visium-like hexagonal grid coordinates.
Constructs coordinates matching the 10x Visium spatial transcriptomics array layout.
Returns integer indices (array_row, array_col) for the physical array layout where
spots are arranged in offset rows (alternating by 1 column).
Parameters
----------
n_rows : int, default 78
Number of rows in the Visium array (e.g., 78 for full slide, 128 for large tissue).
n_cols : int, default 64
Number of spot pairs per row (physical column count is 2 × n_cols or 2 × n_cols + 1).
Returns
-------
coords : np.ndarray
Array indices of shape (N, 2) where N ≤ n_rows × n_cols.
Format: [[row₀, col₀], [row₁, col₁], ...].
Rows are 0-indexed. Columns follow Visium array layout (even/odd offset).
grid_dims : tuple of int
(n_rows, n_cols) - the conceptual grid dimensions.
Notes
-----
Visium layout uses offset hexagonal packing:
- Even rows (0, 2, 4...): column indices 0, 2, 4, 6, ...
- Odd rows (1, 3, 5...): column indices 1, 3, 5, 7, ...
This creates a visually offset honeycomb pattern when plotted.
Use convert_visium_to_physical() to map to physical (y, x) coordinates for distance calculations.
Examples
--------
>>> coords, dims = get_visium_coords(n_rows=78, n_cols=64)
>>> coords.shape[0] # Number of spots
4992
>>> dims
(78, 64)
"""
coords = []
for r in range(n_rows):
# Determine the starting column index based on row parity
# Even rows (0, 2...): cols are 0, 2, 4...
# Odd rows (1, 3...): cols are 1, 3, 5...
start_col = 0 if r % 2 == 0 else 1
for i in range(n_cols):
c = start_col + (i * 2)
coords.append([r, c])
return np.array(coords), (n_rows, n_cols)
[docs]
def convert_visium_to_physical(
coords: np.ndarray
) -> np.ndarray:
"""
Convert integer Visium indices to physical hexagonal coordinates.
Maps array row/column indices to physical (y, x) coordinates in a hexagonal grid
with equilateral triangle geometry. Suitable for distance-based kernel construction
and visualization.
Parameters
----------
coords : np.ndarray
Visium array indices of shape (N, 2) in format [[row, col], ...].
Rows and columns are integers as returned by get_visium_coords().
Returns
-------
phys_coords : np.ndarray
Physical coordinates of shape (N, 2) in format [[y, x], ...].
Notes
-----
Visium uses hexagonal packing with offset rows. The conversion assumes:
- Horizontal neighbor spacing: Δcol = 2 → Δx = 1
- Vertical neighbor spacing: Δrow = 1 → Δy = √3/2 (hexagonal geometry)
Formulas:
$$x = \\text{col} / 2$$
$$y = \\text{row} \\cdot \\frac{\\sqrt{3}}{2}$$
This ensures that nearest neighbors form equilateral triangles in the physical space.
Examples
--------
>>> coords = np.array([[0, 0], [0, 2], [1, 1]]) # Visium indices
>>> phys_coords = convert_visium_to_physical(coords)
>>> phys_coords
array([[0. , 0. ],
[0. , 1. ],
[0.8660254, 0.5 ]])
"""
# Separate row and col
rows = coords[:, 0]
cols = coords[:, 1]
# Calculate physical coordinates
# We use sqrt(3)/2 for Y spacing to ensure equilateral triangles
phys_y = rows * np.sqrt(3) / 2.0
phys_x = cols * 0.5
return np.column_stack([phys_y, phys_x])
[docs]
def compute_torus_distance_matrix(
phys_coords: np.ndarray,
domain_dims: Tuple[float, float]
) -> np.ndarray:
"""
Compute pairwise Euclidean distances on a torus (with periodic boundary conditions).
Calculates distances between all pairs of points assuming the spatial domain
is periodic (wrapping at boundaries). Useful for analyzing spatial patterns in
periodic simulations or tissue with natural periodicity.
Parameters
----------
phys_coords : np.ndarray
Physical coordinates of shape (N, 2) in format [[y, x], ...].
domain_dims : tuple of float
Physical dimensions of the periodic domain: (height, width).
Returns
-------
dist_matrix : np.ndarray
Pairwise distance matrix of shape (N, N) where entry [i, j] is the
shortest Euclidean distance between points i and j on the torus.
Notes
-----
On a torus, the distance between two points is computed as the minimum of:
1. Direct Euclidean distance
2. Euclidean distance after wrapping through periodic boundaries
For each dimension, the wrapped distance is:
$$d_{wrapped} = \\min(|\\Delta p|, D - |\\Delta p|)$$
where D is the domain extent in that dimension.
Examples
--------
>>> coords = np.array([[0.0, 0.0], [1.0, 0.0], [9.9, 0.0]])
>>> domain = (10.0, 10.0)
>>> dists = compute_torus_distance_matrix(coords, domain)
>>> dists[0, 2] # Distance from (0,0) to (9.9,0) on [0,10)×[0,10) torus
0.1 # Wraps around
"""
domain_h, domain_w = domain_dims
# Compute absolute differences (broadcasting)
# Shape: (N, N)
diff_y = np.abs(phys_coords[:, None, 0] - phys_coords[None, :, 0])
diff_x = np.abs(phys_coords[:, None, 1] - phys_coords[None, :, 1])
# Apply Torus wrapping (minimum of direct distance vs wrapped distance)
wrapped_dy = np.minimum(diff_y, domain_h - diff_y)
wrapped_dx = np.minimum(diff_x, domain_w - diff_x)
return np.sqrt(wrapped_dy**2 + wrapped_dx**2)