Skip to content

Alignment API

Everything below is importable from smint.alignment.

from smint.alignment import register_sm_to_st, register_centroids, build_pretransform

Pre-registration

smint.alignment.pretransform — coarse scale, rotation and flips, composed into a single 3×3 affine so the transform is inspectable, savable and replayable.

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,
) -> np.ndarray            # 3x3 affine

Applies flip → rotate → scale → translate about the source centroid. Scale is fitted after orientation is corrected, because bounding-box extents change under rotation.

Parameter Meaning
scale_mode "extent" (match bounding-box spans), "max" (match maxima), "none"
preserve_aspect Isotropic scaling; avoids distorting tissue shape
rotation Degrees counter-clockwise
align_centroids Translate source centroid onto reference centroid

fit_scale_to_reference

fit_scale_to_reference(source_xy, reference_xy, mode="extent",
                       preserve_aspect=True) -> (sx, sy)

Bounding boxes are sensitive to outliers — a few stray points inflate the span and shrink the fitted factor. Check on a plot before trusting it.

overlap_score

overlap_score(source_xy, reference_xy, pixel_size=50.0) -> float

Fraction of occupied reference bins the source also occupies, 0–1. Needs no correspondences, so it is fast enough to drive an interactive control.

describe_pretransform

describe_pretransform(matrix) -> dict

Returns scale_x, scale_y, rotation_deg, shear_deg, translation, determinant, reflects. A negative determinant means the transform includes a reflection.

save_transform / load_transform

save_transform(matrix, path, **metadata) -> Path
load_transform(path) -> np.ndarray

Persists a 3×3 affine as JSON together with its decomposition and any metadata worth recording, so a coarse transform can be reproduced later rather than being an unrepeatable manual step.

Matrix helpers

compose, rotation_matrix, flip_matrix, scale_matrix, translation_matrix, apply_pretransform. compose(A, B) applies A then B.


ST↔SM registration

smint.alignment.st_sm_registration — STalign LDDMM for sequential sections.

prepare_landmark_inputs

prepare_landmark_inputs(st_file, sm_file, output_prefix, dx=30.0,
                        st_x_col="x_final", st_y_col="y_final",
                        sm_kwargs=None) -> dict

Phase 1. Rasterises both modalities and writes <prefix>_st.npz / <prefix>_sm.npz for annotation. Returns those paths plus the <prefix>_*_points.npy paths the annotator will produce.

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",
                  sm_kwargs=None, lddmm_params=None) -> pd.DataFrame

Phase 2. Rasterises, runs landmark-initialised LDDMM with SM as source and ST as target, transforms every SM pixel, and returns the SM table with x_transformed / y_transformed appended.

device defaults to CUDA when available.

read_sm_matrix

read_sm_matrix(mtx_file, scale_xy=10.0, rotate_left_90=False,
               keep_positive=True, origin=None, usecols=None,
               verbose=True) -> pd.DataFrame

Sniffs the delimiter, standardises x/y, prefixes numeric m/z headers with X, then scales, optionally rotates 90° CCW, and shifts to keep coordinates non-negative.

Tip

Pass usecols=["x", "y"] when you only need coordinates. On a 445 MB matrix this cuts load time to ~2 s.

transform_points

transform_points(lddmm_output, x, y, orientation="dataset") -> np.ndarray

orientation="dataset" restores the source dataset's frame (default); "stalign" returns true xy in the ST frame. The two differ by a transpose.

Landmarks

load_landmarks(points_file) -> np.ndarray     # (N, 2) row-col
check_landmark_pair(points_source, points_target) -> None
affine_from_landmarks(points_source, points_target)

load_landmarks reads point_annotator.py's {label: [(x, y)]} format and swaps to the row-col order STalign requires.

Note

affine_from_landmarks is not needed for the standard workflow — LDDMM derives its own initialisation from pointsI/pointsJ.

Other

read_st_annotations, st_coordinates, sm_coordinates, rasterize_coordinates, save_rasterized, run_lddmm_alignment, save_transformed_data, find_sm_matrix_files, stalign_available.

rasterize_coordinates always returns a 4-tuple (X, Y, I, fig), with fig=None when draw=0 — STalign's own function returns 3 or 4 values depending on that flag.


Centroid registration

smint.alignment.centroid_registration — correspondence fitting for post-staining on the same section.

register_centroids

register_centroids(source_xy, target_xy, method="ransac+tps",
                   max_distance=100.0, mutual=True,
                   validation_fraction=0.25, residual_threshold=10.0,
                   max_trials=1000, tps_regularization=1.0,
                   n_control=1000, random_state=0) -> dict

Returns transform (callable), affine, tps, source_idx, target_idx, match_distances, inliers, n_matched, and tre_initial, tre_fit, tre_validation as (mean, std) tuples.

Only tre_validation is meaningful — see Measuring quality honestly.

register_centroid_files

register_centroid_files(source_file, target_file,
                        source_cols=("centroid_x", "centroid_y"),
                        target_cols=("x_centroid", "y_centroid"),
                        output_path=None, **kwargs) -> (pd.DataFrame, dict)

Every source row is transformed, not just matched ones — matching only selects the pairs used to fit.

match_centroids

match_centroids(source_xy, target_xy, max_distance=100.0,
                mutual=True) -> (source_idx, target_idx, distances)

mutual=True keeps only reciprocal nearest neighbours.

fit_tps / apply_tps

fit_tps(source_xy, target_xy, regularization=1.0, n_control=1000) -> dict|None
apply_tps(points_xy, model, chunk_size=50000) -> np.ndarray

Control points cost cubically

TPS solves an (n_control + 3) square system. 74,000 control points needs a ~44 GB solve, plus as much again for the kernel. n_control=1000 is a sane default; regularization=0 gives exact interpolation, which is almost never what you want.

Other

estimate_affine, estimate_affine_ransac, apply_affine, target_registration_error, split_pairs.


Jobs

smint.alignment.jobs — run registration in a separate process, locally or via SLURM.

JobSpec

JobSpec(round, inputs, output_dir, params={}, backend="sbatch",
        resources=SlurmResources(), worker_python=None)

round is "st_sm" or "centroid". Required inputs:

Round Keys
st_sm st_file, sm_file, st_points, sm_points
centroid source_file, target_file

worker_python defaults to SMINT_WORKER_PYTHON, then an STalign_env beside the package, then the current interpreter.

validate() checks required keys exist on disk and — for sbatch — rejects node-local paths a compute node cannot see.

SlurmResources

SlurmResources(partition="regular", cpus_per_task=8, memory="64G",
               time_limit="04:00:00", job_name="smint_register", gpus=0)

check() warns when gpus > 0 on a non-GPU partition (the job would sit pending rather than fail) and vice versa.

submit / poll

submit(spec) -> dict      # job_id (sbatch) or pid (local)
poll(output_dir) -> dict  # state, message, metrics, outputs

poll reconciles the worker's status file with SLURM, so a job killed by OOM or timeout is reported as failed rather than appearing to run forever.

States: submitted, queued, running, completed, failed, cancelled, unknown.

Worker CLI

smint-register /path/to/job_spec.json
python -m smint.cli.register /path/to/job_spec.json

The only place STalign is imported — which is what lets a napari front end in a numpy≥2 environment drive a registration that needs numpy<2.


Command line

smint-alignment pretransform --source sm.csv --target xen.csv \
    --output sm_pre.csv --search

smint-alignment centroid --source nuc.csv --target xen.csv \
    --output-dir ./run --method ransac --watch

smint-alignment st-sm --st Z2.csv --sm Ven5B.csv \
    --st-points z2_st_points.npy --sm-points z2_sm_points.npy \
    --output-dir /vast/scratch/you/run --submit --watch

--submit sends the work to SLURM; --watch blocks until it finishes. smint-register <job_spec.json> runs a spec directly and is what the batch script invokes.


Removed in this release

st_align_wrapper has been deleted. Its align_spatial_transcriptomics shelled out to a stalign command-line tool that does not exist — STalign is a Python library — so every call returned None. load_alignment and save_alignment read and wrote a transformation.json / aligned_coordinates.csv format that nothing in the package produced.

Removed Use instead
align_spatial_transcriptomics register_sm_to_st or register_centroids
run_alignment register_centroids
transform_coordinates apply_affine / apply_pretransform
save_alignment / load_alignment save_transform / load_transform
apply_transformation apply_affine
prepare_visium_data — (the project uses Xenium/CosMx, not Visium)

The smint-alignment console script now points at smint.cli.align. It previously referenced scripts.run_alignment, which is not part of the installed package, so it failed with ModuleNotFoundError. smint-segmentation had the same defect and has been withdrawn until it has a real implementation.

smint.alignment.xenium_metabolomics remains, deprecated, superseded by st_sm_registration.