Skip to content

Alignment API

Everything you need to bring two spatial modalities into one coordinate system, importable from smint.alignment. The functions are grouped by the task they serve, in the order you normally use them:

Task Use it when Start with
Pre-registration The datasets differ in scale, rotation or mirroring build_pretransform
Centroid registration and refinement Both modalities come from the same section (post-staining) register_centroid_files
ST–SM registration The modalities come from sequential sections register_sm_to_st
Coordinate columns You want to know which columns SMINT will read detect_coordinate_columns

To run a registration out of process, locally or on SLURM, see the Jobs API. For the files these functions read and write, see Data formats.

from smint.alignment import register_centroid_files

table, result = register_centroid_files(
    "nuclei_centroids.csv", "xenium_cells.csv",
    source_cols=("x_centroid", "y_centroid"),
    output_path="nuclei_registered.csv",
)
print(result["tre_initial"][0], "->", result["tre_validation"][0])
print(table["registration_status"].value_counts())

Coordinate conventions

Everything here is plain (x, y), except inside the STalign wrappers: STalign works in row–column (y, x) order, and load_landmarks swaps the pair on load. transform_points returns (x, y) in the frame you choose with orientation.

Pre-registration

A coarse scale, rotation and flip, held as one 3×3 affine so it can be inspected, saved with save_transform and replayed. Run it before landmark picking or LDDMM when the two datasets start in different frames. Operations are applied flip → rotate → scale about the source centroid, then the source centroid is moved onto the reference centroid.

import pandas as pd
from smint.alignment import build_pretransform, apply_pretransform, describe_pretransform

sm_xy = pd.read_csv("sm_matrix.csv", usecols=["x", "y"]).to_numpy(float)
xenium_xy = pd.read_csv("xenium_cells.csv", usecols=["x_centroid", "y_centroid"]).to_numpy(float)

matrix = build_pretransform(sm_xy, xenium_xy, rotation=90)
sm_pre = apply_pretransform(sm_xy, matrix)
print(describe_pretransform(matrix))   # scale_x, scale_y, rotation_deg, reflects, ...

smint.alignment.build_pretransform

build_pretransform(source_xy, reference_xy=None, scale=None, scale_mode='extent', preserve_aspect=True, rotation=0.0, flip_x=False, flip_y=False, align_centroids=True, center=None)

Build the coarse pre-registration affine for a source dataset.

Applies flip, then rotation, then scale, all about center (the source centroid by default), and finally an optional translation putting the source centroid on the reference centroid.

Parameters:

Name Type Description Default
source_xy array - like

(N, 2) source coordinates.

required
reference_xy array - like

(M, 2) reference coordinates. Required for fitted scaling or centroid alignment.

None
scale tuple of float

Explicit (sx, sy). Overrides fitting against the reference.

None
scale_mode (extent, max, none)

Fitting mode when scale is not given. See fit_scale_to_reference.

'extent'
preserve_aspect bool

Isotropic scaling when fitting.

True
rotation float

Counter-clockwise degrees.

0.0
flip_x bool

Mirror horizontally / vertically.

False
flip_y bool

Mirror horizontally / vertically.

False
align_centroids bool

Translate so the source centroid lands on the reference centroid.

True
center sequence of float

Centre for flip/rotate/scale; defaults to the source centroid.

None

Returns:

Type Description
ndarray

3x3 homogeneous affine, applicable with apply_affine.

smint.alignment.apply_pretransform

apply_pretransform(coords, matrix)

Apply a 3x3 affine to (N, 2) (x, y) coordinates.

Thin wrapper over apply_affine so pre-registration reads naturally on its own.

smint.alignment.fit_scale_to_reference

fit_scale_to_reference(source_xy, reference_xy, mode='extent', preserve_aspect=True)

Scale factors bringing a source dataset onto a reference's coordinate system.

Parameters:

Name Type Description Default
source_xy array - like

(N, 2) / (M, 2) coordinate arrays.

required
reference_xy array - like

(N, 2) / (M, 2) coordinate arrays.

required
mode (extent, max, none)

'extent' (default) matches the bounding-box span of each axis, which is robust to the two datasets having different origins. 'max' matches the maximum coordinate directly -- literal, but wrong whenever either dataset does not start near zero. 'none' returns (1.0, 1.0).

'extent'
preserve_aspect bool

If True (default), use a single isotropic factor -- the smaller of the two -- so the tissue is not distorted. Anisotropic scaling can force bounding boxes to agree while making the shapes match worse, which then misleads the landmark step.

True

Returns:

Name Type Description
sx float

Scale factor along x.

sy float

Scale factor along y (equal to sx when preserve_aspect).

Notes

Bounding boxes are sensitive to outliers: a handful of stray points inflates the span and shrinks the fitted factor. Check the result on a plot, and drop obvious debris before fitting.

smint.alignment.overlap_score

overlap_score(source_xy, reference_xy, pixel_size=50.0)

Fraction of occupied reference bins that the source also occupies.

A cheap, correspondence-free way to tell whether a coarse pre-registration is getting better or worse -- useful for driving an interactive rotate/flip control, where a full registration per adjustment would be far too slow. Ranges 0 to 1; higher is better.

Parameters:

Name Type Description Default
source_xy array - like

(N, 2) coordinate sets in a shared frame.

required
reference_xy array - like

(N, 2) coordinate sets in a shared frame.

required
pixel_size float

Bin size. Too small and nothing overlaps; too large and everything does. Roughly the tissue feature scale works well.

50.0

smint.alignment.describe_pretransform

describe_pretransform(matrix)

Decompose an affine into human-readable scale, rotation and shear.

Useful for sanity-checking a transform before committing to it -- a negative determinant means the transform includes a reflection, which is easy to introduce accidentally by combining a flip with a rotation.

Returns:

Type Description
dict

scale_x, scale_y, rotation_deg, shear_deg, translation, determinant, reflects.

smint.alignment.save_transform

save_transform(matrix, path, **metadata)

Save an affine transform as JSON, with its decomposition alongside.

A coarse transform that cannot be reproduced is of little use when revisiting a registration months later, so the matrix is stored together with the human-readable parameters it corresponds to and any extra metadata worth recording (source files, rotation/flip settings).

Parameters:

Name Type Description Default
matrix ndarray

3x3 homogeneous affine.

required
path str

Destination .json.

required
**metadata

Additional JSON-serialisable fields to record.

{}

Returns:

Type Description
Path

The path written.

smint.alignment.load_transform

load_transform(path)

Load a 3x3 affine saved by save_transform.

Returns:

Type Description
ndarray

The 3x3 matrix. Read the file directly if you also want the metadata.

Matrix building blocks

smint.alignment.compose

compose(*matrices)

Compose 3x3 affine matrices, applied left to right.

compose(A, B) returns a matrix equivalent to applying A then B. Note this is B @ A in matrix terms -- the argument order follows the order operations happen, which is the one that matches how people describe them.

smint.alignment.rotation_matrix

rotation_matrix(degrees, center=(0.0, 0.0))

3x3 affine rotating counter-clockwise by degrees about center.

Counter-clockwise in standard xy orientation. Note that images displayed with an inverted y axis will appear to turn the other way.

smint.alignment.flip_matrix

flip_matrix(flip_x=False, flip_y=False, center=(0.0, 0.0))

3x3 affine mirroring about center.

flip_x mirrors left-right (negates x), flip_y mirrors top-bottom. A flip is a scale by -1, so it is handled by the same machinery.

smint.alignment.scale_matrix

scale_matrix(sx, sy=None, center=(0.0, 0.0))

3x3 affine scaling about center; sy defaults to sx.

smint.alignment.translation_matrix

translation_matrix(dx, dy)

3x3 affine for a translation.

Centroid registration and refinement

For a second modality acquired on the same section, where nuclei and cells correspond one-to-one. register_centroids fits the transform (multiround displacement voting by default) and reports target registration error on held-out points; refine_by_assignment then snaps confidently matched nuclei exactly onto their cells. register_centroid_files does both from two CSV files and is what smint-alignment centroid runs.

flowchart LR
    A[source centroids] --> B[register_centroids<br/>multiround voting]
    T[target centroids] --> B
    B -->|transform| C[x/y_transformed_smooth]
    C --> D[refine_by_assignment<br/>snap, fill, assign]
    T --> D
    D --> E[x/y_transformed<br/>registration_status]

smint.alignment.register_centroid_files

register_centroid_files(source_file, target_file, source_cols=None, target_cols=None, output_path=None, x_col='x_transformed', y_col='y_transformed', refine=True, refine_kwargs=None, **kwargs)

Register centroids from two CSV files and write the transformed source.

Every source row is transformed, not just the matched ones -- matching selects the pairs used to fit the transform, which then applies everywhere.

Parameters:

Name Type Description Default
source_file str

CSVs holding the two centroid sets.

required
target_file str

CSVs holding the two centroid sets.

required
source_cols sequence of str

(x, y) column names in each file. By default the first pair present of x_centroid/y_centroid (Xenium, SMINT segmentation summaries), centroid_x/centroid_y and x/y.

None
target_cols sequence of str

(x, y) column names in each file. By default the first pair present of x_centroid/y_centroid (Xenium, SMINT segmentation summaries), centroid_x/centroid_y and x/y.

None
output_path str

Where to write the transformed source table.

None
x_col str

Output column names.

'x_transformed'
y_col str

Output column names.

'x_transformed'
refine bool

After registering, snap confidently matched points onto their targets with refine_by_assignment (default). x_col / y_col then hold the refined positions, x_col + "_smooth" the transform-only ones, and registration_status says how each point was placed.

True
refine_kwargs dict

Forwarded to refine_by_assignment.

None
**kwargs

Forwarded to register_centroids.

{}

Returns:

Name Type Description
result_df DataFrame

Source table with transformed coordinates appended.

result dict

The register_centroids result.

smint.alignment.register_centroids

register_centroids(source_xy, target_xy, method='multiround', max_distance=100.0, mutual=True, validation_fraction=0.25, residual_threshold=10.0, max_trials=1000, tps_regularization=None, n_control=1000, random_state=0, schedule=None, min_improvement=0.01)

Register two centroid sets acquired from the same physical section.

multiround (the default) re-matches and refits over several rounds; the single-round methods match once by nearest neighbour, hold out a fraction of the pairs, and fit the rest. All report TRE on held-out data.

Parameters:

Name Type Description Default
source_xy ndarray

(N, 2) / (M, 2) (x, y) centroids. The source is moved onto the target.

required
target_xy ndarray

(N, 2) / (M, 2) (x, y) centroids. The source is moved onto the target.

required
method (multiround, affine, ransac, tps, ransac + tps)

Registration model: affine (least squares on all matched pairs), ransac (outlier-robust affine), tps (affine, then a thin-plate spline), ransac+tps (RANSAC affine, then TPS on the inliers) or multiround (coarse-to-fine displacement voting: an affine through per-block displacement peaks, then a smooth TPS through finer blocks; recommended).

'multiround'
max_distance float

Correspondence cutoff, in coordinate units.

100.0
mutual bool

Require reciprocal nearest neighbours.

True
validation_fraction float

Fraction held out from fitting: matched pairs for the single-round methods, source points for multiround. Set to 0.0 only if you accept that the reported TRE is then uninformative.

0.25
residual_threshold optional

RANSAC settings (multiround derives each round's threshold from its radius).

10.0
max_trials optional

RANSAC settings (multiround derives each round's threshold from its radius).

10.0
tps_regularization optional

TPS settings. Regularisation defaults to 1000 for multiround (whose control points are block centres) and 1 for the single-round methods.

None
n_control optional

TPS settings. Regularisation defaults to 1000 for multiround (whose control points are block centres) and 1 for the single-round methods.

None
random_state int

Seed for the split, RANSAC and control-point sampling.

0
schedule sequence of (max_distance, block_size, bin_size)

multiround voting rounds, coarse to fine. Round 1 fits an affine through the block displacements; later rounds add a smooth TPS through them. Default ((max_distance, 500, 2), (0.25 * max_distance, 250, 1)), in coordinate units (microns).

None
min_improvement float

multiround keeps a round only if it lowers the fit points' median nearest-target distance by at least this fraction, and stops at the first round that does not.

0.01

Returns:

Type Description
dict

transform (callable mapping (N,2) -> (N,2)), affine (3x3 or None), tps (model or None), source_idx / target_idx / match_distances, inliers (or None), n_matched, tre_initial, tre_fit, tre_validation -- each a (mean, std) tuple, with tre_validation the one to trust. multiround adds rounds (a per-round log) and validation, alignment_quality of the held-out points before and after -- a correspondence-free check alongside TRE.

smint.alignment.refine_by_assignment

refine_by_assignment(source_xy, target_xy, transform=None, snap_radius=10.0, neighbour_radius=100.0, consistency_tol=None, min_neighbours=5, k_fill=8, validation_fraction=0.1, random_state=0, passes=4, assign_remaining=True, assign_radius=None)

Snap registered source points exactly onto their partner targets.

A port of the lab's alignment_refinement.r. A smooth transform leaves each nucleus a few microns from its cell -- partly because a nucleus really is off-centre in its cell. This step instead assigns nuclei to cells and moves each confidently assigned nucleus onto its partner:

  1. Pair transformed source points one-to-one with targets (mutual nearest neighbours within snap_radius).
  2. Keep a pair only if its offset agrees, within consistency_tol, with the median offset of the other pairs within neighbour_radius -- a pairing with the wrong cell disagrees with its neighbourhood.
  3. Snap kept pairs: the source point moves exactly onto its target.
  4. Every other point moves by the median offset of its k_fill nearest snapped neighbours (within neighbour_radius), or not at all.
  5. Repeat: the moved, still-unsnapped points are paired again with the still-unmatched targets, and kept if they agree with the snapped field around them -- up to passes times, until nothing new snaps.
  6. Assign what is left: the filled points and the still-unmatched targets are paired by a minimum-total-distance one-to-one assignment within assign_radius, and each assigned point moves onto its target (status 'assigned'). These pairs skipped the consistency test, so they are less certain than 'snapped' ones; filter on the status if that matters.

Snapped points coincide with their targets by construction, so their residual says nothing about correctness. The check is validation: a fraction of the kept pairs is left out of the offset field, their positions are predicted from their neighbours alone (step 4), and the distance from each prediction to its partner is reported.

Parameters:

Name Type Description Default
source_xy ndarray

(N, 2) / (M, 2) points. source_xy in its original frame.

required
target_xy ndarray

(N, 2) / (M, 2) points. source_xy in its original frame.

required
transform callable

Registration to apply first (e.g. register_centroids(...)["transform"]). Default: none, source_xy is already registered.

None
snap_radius float

Maximum distance for a pair.

10.0
neighbour_radius float

Neighbourhood for the consistency test and for filling in.

100.0
consistency_tol float

Allowed deviation from the neighbourhood's median offset. Default 0.4 x the target's median nearest-neighbour spacing.

None
min_neighbours int

Pairs with fewer neighbouring pairs than this cannot be checked and are not snapped.

5
passes int

Pairing passes (step 5); 1 disables re-pairing.

4
assign_remaining bool

Run step 6.

True
assign_radius float

Maximum distance for step 6. Default 0.75 x the target's median nearest-neighbour spacing.

None
k_fill int

Snapped neighbours whose median offset moves an unsnapped point.

8
validation_fraction float

Fraction of kept pairs used for the held-out check.

0.1

Returns:

Type Description
dict

refined_xy ((N, 2)), status ('snapped', 'assigned', 'filled' or 'unmoved' per point), target_idx (partner index, -1 if none), neighbour_deviation (per placed point: distance between its offset and its snapped neighbourhood's median offset -- small for right pairs, about a cell spacing for wrong ones), n_snapped, fraction_snapped (of source points), fraction_targets_matched, consistency_tol and validation (median_error, within_2, within_5 for the held-out pairs).

smint.alignment.alignment_quality

alignment_quality(moved_xy, target_xy)

Correspondence-free alignment summary of moved_xy against target_xy.

Returns median_nn (median distance to the nearest target) and mutual_within_5 / mutual_within_10: the fraction of points whose nearest target has them as its nearest point too, within 5 / 10 units.

smint.alignment.vote_block_displacements

vote_block_displacements(source_xy, target_xy, max_distance, block_size, bin_size=2.0, k=None, min_points=25)

Estimate the local source -> target displacement in square blocks.

Every source point votes for the displacements to its k nearest targets within max_distance. Wrong candidates point in random directions, but the true offset is shared by every point in the block, so it is the peak of the (lightly smoothed) 2D vote histogram. This works even when the misalignment exceeds the cell spacing, where nearest-neighbour matching cannot -- provided the tissue is not a regular lattice, whose periodicity makes every whole-spacing shift look equally good.

k defaults to the expected number of targets within max_distance (from the target density, capped at 128), so the true partner is among the candidates anywhere in that radius.

Returns:

Name Type Description
centres ndarray

(B, 2) centroids of the blocks holding at least min_points source points.

displacements ndarray

(B, 2) modal source -> target displacement of each block.

Correspondence and error

smint.alignment.match_centroids

match_centroids(source_xy, target_xy, max_distance=100.0, mutual=True)

Establish nearest-neighbour correspondences between two centroid sets.

Parameters:

Name Type Description Default
source_xy ndarray

(N, 2) / (M, 2) arrays of (x, y) centroids.

required
target_xy ndarray

(N, 2) / (M, 2) arrays of (x, y) centroids.

required
max_distance float

Maximum distance for a pair to be considered corresponding, in the coordinate units (microns for Xenium).

100.0
mutual bool

If True (default), keep only pairs that are each other's nearest neighbour. Plain one-directional matching lets many source points collapse onto a single popular target, which biases the fit and inflates the apparent match count.

True

Returns:

Name Type Description
source_idx ndarray

Source index of each matched pair.

target_idx ndarray

Target index of each matched pair.

distances ndarray

Distance for each matched pair.

smint.alignment.split_pairs

split_pairs(n_pairs, validation_fraction=0.25, random_state=0)

Split matched pairs into fit and held-out validation index sets.

Parameters:

Name Type Description Default
n_pairs int

Number of matched pairs.

required
validation_fraction float

Fraction held out, in [0, 1).

0.25
random_state int

Seed for the permutation.

0

Returns:

Name Type Description
fit_idx ndarray

Indices of the pairs to fit.

val_idx ndarray

Indices of the held-out pairs.

smint.alignment.target_registration_error

target_registration_error(source_xy, target_xy)

Target Registration Error between paired point sets.

Parameters:

Name Type Description Default
source_xy ndarray

(N, 2) arrays of corresponding points, same length and order.

required
target_xy ndarray

(N, 2) arrays of corresponding points, same length and order.

required

Returns:

Name Type Description
mean float

Mean per-pair distance.

std float

Standard deviation of the per-pair distance.

per_pair ndarray

The per-pair distances.

Notes

On the points used for fitting, TRE is circular: correspondences come from nearest neighbours, so a flexible enough transform drives it towards zero whether or not the pairs are right. Prefer the held-out value reported by register_centroids.

Transform models

smint.alignment.estimate_affine

estimate_affine(source_xy, target_xy)

Least-squares affine transform mapping source onto target.

Returns:

Type Description
ndarray

3x3 homogeneous matrix; identity if there are too few pairs.

smint.alignment.estimate_affine_ransac

estimate_affine_ransac(source_xy, target_xy, residual_threshold=10.0, max_trials=1000, min_samples=3, random_state=0)

RANSAC-robust affine transform, rejecting outlier correspondences.

Nearest-neighbour matching inevitably produces some wrong pairs -- an unmatched cell picks up whichever neighbour is closest. Least squares lets those wrong pairs drag the fit; RANSAC finds the transform supported by the largest consistent subset and reports the rest as outliers.

Parameters:

Name Type Description Default
source_xy ndarray

(N, 2) corresponding points.

required
target_xy ndarray

(N, 2) corresponding points.

required
residual_threshold float

Max residual (in coordinate units) for a pair to count as an inlier.

10.0
max_trials int

RANSAC iterations.

1000
min_samples int

Points per RANSAC sample; 3 is the minimum for an affine.

3
random_state int

Seed, for reproducibility.

0

Returns:

Name Type Description
matrix ndarray

3x3 homogeneous affine.

inliers ndarray

Boolean mask of inlier pairs.

smint.alignment.apply_affine

apply_affine(points_xy, matrix)

Apply a 3x3 homogeneous affine matrix to (N, 2) points.

smint.alignment.fit_tps

fit_tps(source_xy, target_xy, regularization=1.0, n_control=1000, random_state=0)

Fit a thin-plate spline warping source control points onto targets.

Parameters:

Name Type Description Default
source_xy ndarray

(N, 2) corresponding points.

required
target_xy ndarray

(N, 2) corresponding points.

required
regularization float

Ridge term added to the TPS system. 0 gives exact interpolation of every control point -- which drives fitted-point TRE to zero and tells you nothing. Larger values trade exact control-point fit for smoothness. Default 1.0.

1.0
n_control int

Number of control points to subsample. TPS solves an (n_control + 3) square system and evaluates an (N x n_control) kernel, so cost grows as the cube / product of this: 74,000 control points needs a ~44 GB solve. None uses every pair, which is rarely what you want.

1000
random_state int

Seed for control-point subsampling.

0

Returns:

Type Description
dict or None

Model with keys W, A, control_points; None if the system could not be solved.

smint.alignment.apply_tps

apply_tps(points_xy, model, chunk_size=50000)

Warp points with a fitted TPS model.

Parameters:

Name Type Description Default
points_xy ndarray

(N, 2) points to warp.

required
model dict or None

Output of fit_tps. None returns the input unchanged.

required
chunk_size int

Points processed per block. The kernel is (chunk x n_control) dense, so this bounds peak memory rather than materialising N x n_control at once.

50000

Returns:

Type Description
ndarray

Warped (N, 2) points.

ST–SM registration (sequential sections)

For spatial metabolomics (SM) and spatial transcriptomics (ST) from different sections. STalign's LDDMM does the warping, initialised from landmarks you pick by hand, so the workflow has two phases around a manual step:

flowchart LR
    A[ST CSV + SM matrix] --> B[prepare_landmark_inputs<br/>rasterise, write .npz]
    B --> C[pick landmarks<br/>napari plugin or point annotator]
    C --> D[_st_points.npy<br/>_sm_points.npy]
    D --> E[register_sm_to_st<br/>landmark-initialised LDDMM]
    A --> E
    E --> F[sm_transformed.csv<br/>x/y_transformed]

These functions import STalign, which needs numpy < 2 and torch. Run them in a worker environment (stalign_available tells you whether the current one qualifies), or submit them with the Jobs API.

smint.alignment.prepare_landmark_inputs

prepare_landmark_inputs(st_file, sm_file, output_prefix, dx=30.0, st_x_col=None, st_y_col=None, sm_kwargs=None)

Phase 1: rasterize an ST/SM pair and write .npz files for annotation.

After this returns, run point_annotator.py on each .npz and pick the same anatomical landmarks, in the same order, in both. The annotator writes <prefix>_st_points.npy / <prefix>_sm_points.npy next to them, which register_sm_to_st then consumes.

Parameters:

Name Type Description Default
st_file str

ST annotation CSV.

required
sm_file str

SM matrix CSV.

required
output_prefix str

Path prefix. Writes <prefix>_st.npz / <prefix>_sm.npz; the annotator then appends _points.npy to each stem, yielding the <prefix>_st_points.npy / <prefix>_sm_points.npy files that register_sm_to_st expects.

required
dx float

Rasterization pixel size, in microns. Must match between the pair.

30.0
st_x_col str

ST coordinate columns.

None
st_y_col str

ST coordinate columns.

None
sm_kwargs dict

Forwarded to read_sm_matrix (e.g. rotate_left_90).

None

Returns:

Type Description
dict

Paths written and the rasterized arrays, keyed st_npz, sm_npz, st_raster, sm_raster, plus st_points/sm_points giving the landmark paths the annotator will produce.

smint.alignment.register_sm_to_st

register_sm_to_st(st_file, sm_file, st_points_file, sm_points_file, output_path=None, dx=30.0, niter=1000, epV=200.0, device=None, orientation='dataset', st_x_col=None, st_y_col=None, sm_kwargs=None, lddmm_params=None)

Phase 2: register an SM section onto an ST section using manual landmarks.

Rasterizes both modalities, runs landmark-initialised LDDMM with SM as source and ST as target, transforms every SM pixel, and returns the SM table with transformed coordinates appended.

Parameters:

Name Type Description Default
st_file str

Input tables for the same physical section.

required
sm_file str

Input tables for the same physical section.

required
st_points_file str

*_points.npy landmark files from the manual annotation step.

required
sm_points_file str

*_points.npy landmark files from the manual annotation step.

required
output_path str

CSV destination for the transformed SM table.

None
dx float

Rasterization pixel size; must match what was annotated.

30.0
niter optional

LDDMM optimisation settings.

1000
epV optional

LDDMM optimisation settings.

1000
device str

Torch device; defaults to CUDA when available.

None
orientation (dataset, stalign)

Output coordinate frame. 'stalign' gives true x/y in the ST frame, so the result overlays the ST cells; 'dataset' (default) reproduces the project's historical outputs, which are transposed relative to ST.

'dataset'
st_x_col str

Which ST columns to register. Auto-detected when omitted; pass them explicitly when the table carries several coordinate pairs, since the wrong choice misregisters silently rather than raising.

None
st_y_col str

Which ST columns to register. Auto-detected when omitted; pass them explicitly when the table carries several coordinate pairs, since the wrong choice misregisters silently rather than raising.

None
sm_kwargs dict

Forwarded to read_sm_matrix, including x_col/y_col.

None
lddmm_params dict

Extra parameters forwarded to STalign.LDDMM.

None

Returns:

Type Description
DataFrame

SM data with x_transformed / y_transformed columns.

smint.alignment.stalign_available

stalign_available()

Return True if STalign can actually be imported and used.

Reading inputs

smint.alignment.read_sm_matrix

read_sm_matrix(mtx_file, scale_xy=10.0, rotate_left_90=False, keep_positive=True, origin=None, usecols=None, x_col=None, y_col=None, verbose=True)

Read a spatial metabolomics matrix file and normalise it for registration.

Applies, in order: delimiter sniffing, x/y name standardisation, X-prefixing of numeric m/z headers, coordinate scaling, optional 90 deg CCW rotation, and an optional shift to keep coordinates non-negative.

The SM grid is stored in pixel units; scale_xy converts it to the same micron scale as the ST centroids, so registration starts near the right magnitude. For the MALDI data SMINT was developed on this is 10.0.

Parameters:

Name Type Description Default
mtx_file str

Path to the *_information_matrix_tissue_only_full_with_xy.csv file.

required
scale_xy float

Multiplier applied to x and y. Use 1.0 to disable.

10.0
rotate_left_90 bool

If True, apply (x, y) -> (-y, x) after scaling.

False
keep_positive bool

If True, translate so coordinates are >= origin (default (0, 0)).

True
origin tuple of float

Target lower bound (ox, oy) when keep_positive is True.

None
usecols sequence of str

Restrict to these columns. Passing ['x', 'y'] reads coordinates only, which is much faster when the m/z intensities aren't needed (e.g. when rasterizing for landmark annotation).

None
x_col str

Which columns hold the coordinates. By default the columns literally named x and y are used; pass these when the matrix carries several coordinate pairs and you need a specific one.

None
y_col str

Which columns hold the coordinates. By default the columns literally named x and y are used; pass these when the matrix carries several coordinate pairs and you need a specific one.

None
verbose bool

Log progress.

True

Returns:

Type Description
DataFrame

Full frame with x/y columns plus X-prefixed m/z features.

Notes

This returns the whole DataFrame rather than a (coords, data, mz) tuple; use sm_coordinates to pull the coordinate array out.

smint.alignment.read_st_annotations

read_st_annotations(st_file, x_col=None, y_col=None, verbose=True)

Read an ST annotation table (e.g. sample2_xenium_z2.csv).

Parameters:

Name Type Description Default
st_file str

Path to the ST CSV.

required
x_col str

Coordinate column names. Auto-detected when omitted; see resolve_st_columns for why explicit names are safer.

None
y_col str

Coordinate column names. Auto-detected when omitted; see resolve_st_columns for why explicit names are safer.

None
verbose bool

Log progress.

True

Returns:

Type Description
DataFrame

The frame, with the coordinate columns guaranteed present and numeric.

smint.alignment.resolve_st_columns

resolve_st_columns(st_file, x_col=None, y_col=None)

Decide which ST columns hold the coordinates to register.

Explicit names win. When omitted, columns are auto-detected in the priority order in smint.alignment.columns, and the choice is logged.

Be deliberate here. ST tables often carry several plausible coordinate pairs from successive processing stages -- x_centroid alongside x_new and x_new_add, say -- and picking the wrong one produces a silently misregistered result rather than an error. Pass the names explicitly whenever more than one pair exists.

Parameters:

Name Type Description Default
st_file str

Path to the ST CSV.

required
x_col str

Explicit column names; auto-detected when omitted.

None
y_col str

Explicit column names; auto-detected when omitted.

None

Returns:

Name Type Description
x_col str

Column holding x.

y_col str

Column holding y.

smint.alignment.list_columns

list_columns(csv_file)

Column names of a CSV, without reading its contents.

Lets a caller offer a coordinate-column picker before committing to reading a 450 MB matrix.

smint.alignment.sm_coordinates

sm_coordinates(df)

Return the (N, 2) [x, y] array from an SM frame.

smint.alignment.st_coordinates

st_coordinates(df, x_col=None, y_col=None)

Return the (N, 2) [x, y] array from an ST frame.

Columns are auto-detected when omitted.

smint.alignment.find_sm_matrix_files

find_sm_matrix_files(base_path, layers)

Locate SM matrix files for the given layers.

Looks under base_path/layer_{n}/matrix/*information_matrix_tissue_only_full_with_xy.csv.

Parameters:

Name Type Description Default
base_path str

Root of the integrated directory for a sample.

required
layers sequence of int

Layer numbers to search.

required

Returns:

Type Description
dict

{layer: [paths]}.

Rasterising and landmarks

smint.alignment.rasterize_coordinates

rasterize_coordinates(x, y, dx=30.0, draw=0, **kwargs)

Rasterize point coordinates into a density image via STalign.rasterize.

Parameters:

Name Type Description Default
x array - like

Point coordinates.

required
y array - like

Point coordinates.

required
dx float

Pixel size, in the same units as x/y.

30.0
draw int

Passed to STalign; 0 suppresses the figure (the default here, so this is safe to call in batch/SLURM jobs).

0
**kwargs

Forwarded to STalign.rasterize (blur, expand, ...).

{}

Returns:

Name Type Description
X ndarray

Pixel locations along x.

Y ndarray

Pixel locations along y.

I ndarray

Rasterized image, channels first.

fig matplotlib figure or None

The drawn figure when draw is truthy, otherwise None.

Notes

STalign.rasterize returns a 3-tuple when draw is falsy and a 4-tuple when it is truthy. This wrapper always returns 4 values so callers can unpack unconditionally.

smint.alignment.save_rasterized

save_rasterized(output_path, X, Y, I)

Save a rasterized image as .npz for the manual point annotator.

point_annotator.py expects arrays named x, y and I, and writes its landmarks alongside as <stem>_points.npy.

Parameters:

Name Type Description Default
output_path str

Destination path; .npz is appended by numpy if absent.

required
X ndarray

Outputs of rasterize_coordinates.

required
Y ndarray

Outputs of rasterize_coordinates.

required
I ndarray

Outputs of rasterize_coordinates.

required

Returns:

Type Description
str

The path written.

smint.alignment.load_landmarks

load_landmarks(points_file)

Load landmarks written by point_annotator.py into row-col order.

The annotator stores a {label: [(x, y)]} dict. STalign's L_T_from_points and LDDMM both expect points in row-col (y, x) order, so the pair is swapped on load.

Parameters:

Name Type Description Default
points_file str

Path to a *_points.npy file.

required

Returns:

Type Description
ndarray

(N, 2) array in (row, col) == (y, x) order, ordered by label.

Raises:

Type Description
FileNotFoundError

If the landmark file is missing -- usually meaning the manual annotation step has not been run for this section yet.

smint.alignment.check_landmark_pair

check_landmark_pair(points_source, points_target)

Validate a source/target landmark pair before running LDDMM.

Raises:

Type Description
ValueError

If the counts differ or there are fewer than 3 points (an affine fit needs 3), which otherwise surfaces as an opaque failure inside LDDMM.

smint.alignment.affine_from_landmarks

affine_from_landmarks(points_source, points_target)

Compute an affine matrix from landmark correspondences.

Parameters:

Name Type Description Default
points_source ndarray

(N, 2) arrays in row-col order, as returned by load_landmarks.

required
points_target ndarray

(N, 2) arrays in row-col order, as returned by load_landmarks.

required

Returns:

Type Description
Tensor

3x3 affine matrix.

Notes

run_lddmm_alignment derives its own initialisation from points_source/points_target internally, so this is not needed for the standard workflow. It is kept for inspecting or seeding a landmark-only (affine) registration.

LDDMM and transforming points

smint.alignment.run_lddmm_alignment

run_lddmm_alignment(source_grid, source_image, target_grid, target_image, points_source=None, points_target=None, niter=1000, epV=200.0, device=None, init_from_landmarks=True, **params)

Run STalign LDDMM to register a source image onto a target image.

Parameters:

Name Type Description Default
source_grid sequence

Grids as [Y, X] (row-col), matching STalign's convention.

required
target_grid sequence

Grids as [Y, X] (row-col), matching STalign's convention.

required
source_image ndarray

Rasterized images from rasterize_coordinates.

required
target_image ndarray

Rasterized images from rasterize_coordinates.

required
points_source ndarray

Landmark correspondences in row-col order. STalign's LDDMM uses them only as a matching term and otherwise starts from the identity (it also skips its own pre-alignment when landmarks are given), so on its own a large offset barely moves. With init_from_landmarks the affine is first initialised from the landmarks.

None
points_target ndarray

Landmark correspondences in row-col order. STalign's LDDMM uses them only as a matching term and otherwise starts from the identity (it also skips its own pre-alignment when landmarks are given), so on its own a large offset barely moves. With init_from_landmarks the affine is first initialised from the landmarks.

None
niter int

Optimisation iterations.

1000
epV float

Velocity-field regularisation weight.

200.0
device str

Torch device; defaults to CUDA when available, else CPU.

None
init_from_landmarks bool

Initialise the affine (L, T) from the landmarks with STalign.L_T_from_points (default). Ignored when no landmarks are given or when L, T or A is passed explicitly.

True
**params

Forwarded to STalign.LDDMM. Note that sigmaM/sigmaB/ sigmaA are best left at their defaults when landmarks are given.

{}

Returns:

Type Description
dict

STalign LDDMM output, including A, v and xv.

smint.alignment.transform_points

transform_points(lddmm_output, x, y, orientation='dataset')

Apply an LDDMM result to source point coordinates.

Parameters:

Name Type Description Default
lddmm_output dict

Output of run_lddmm_alignment (needs A, v, xv).

required
x array - like

Source coordinates, as plain x and y vectors.

required
y array - like

Source coordinates, as plain x and y vectors.

required
orientation (dataset, stalign)

Which frame to return. STalign returns points in row-column order; 'dataset' reproduces the project's historical outputs and keeps the source dataset's orientation; 'stalign' returns true xy in the ST frame. The two differ by a transpose.

'dataset'

Returns:

Type Description
ndarray

(N, 2) array of transformed coordinates, columns ordered to match orientation.

smint.alignment.save_transformed_data

save_transformed_data(source_data, transformed_coords, output_path=None, x_col=TRANSFORMED_X_COL, y_col=TRANSFORMED_Y_COL, orientation=None)

Attach transformed coordinates to the source table and optionally save it.

Parameters:

Name Type Description Default
source_data DataFrame

The SM frame the coordinates were derived from.

required
transformed_coords ndarray

(N, 2) output of transform_points.

required
output_path str

CSV destination. Parent directories are created.

None
x_col str

Output column names.

TRANSFORMED_X_COL
y_col str

Output column names.

TRANSFORMED_X_COL
orientation str

Recorded in df.attrs['orientation'] in-process and, when the table is saved, in <output_path>.registration.json next to it, so the frame of x_col/y_col is known later.

None

Returns:

Type Description
DataFrame

Copy of source_data with the two coordinate columns appended.

Coordinate columns

SMINT tables name their coordinates differently depending on where they came from. These helpers resolve them in one place, trying the candidates below in order (case-insensitive). See Data formats for where auto-detection applies and where you must name columns yourself.

smint.alignment.columns.X_CANDIDATES module-attribute

X_CANDIDATES = ('x_final', 'x_centroid', 'centroid_x', 'x_transformed', 'x', 'X')

smint.alignment.columns.Y_CANDIDATES module-attribute

Y_CANDIDATES = ('y_final', 'y_centroid', 'centroid_y', 'y_transformed', 'y', 'Y')

smint.alignment.columns.detect_coordinate_columns

detect_coordinate_columns(columns)

Guess the (x, y) column names in a table.

Candidates are tried in priority order, so a table carrying both x_final and x resolves to the more specific one.

Parameters:

Name Type Description Default
columns sequence of str

The table's column names.

required

Returns:

Name Type Description
x_col str or None

The x column, or None if nothing matched.

y_col str or None

The y column, or None if nothing matched.

smint.alignment.columns.require_coordinate_columns

require_coordinate_columns(columns, source='table')

Detect coordinate columns, raising a useful error when they are missing.

Parameters:

Name Type Description Default
columns sequence of str

The table's column names.

required
source str

Name of the table, used in the error message.

'table'

Returns:

Name Type Description
x_col str

The x column.

y_col str

The y column.

Raises:

Type Description
ValueError

Naming the source and listing what was actually available, since the usual cause is pointing at the wrong file.

Next steps