Skip to content

feat: stalign api and landmark alignment - #1195

Open
selmanozleyen wants to merge 42 commits into
scverse:mainfrom
selmanozleyen:feat/experimental-fit-core
Open

selmanozleyen wants to merge 42 commits into
scverse:mainfrom
selmanozleyen:feat/experimental-fit-core

Conversation

@selmanozleyen

Copy link
Copy Markdown
Member

Continuation of #1162

@codecov

codecov Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 43.58707% with 541 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.85%. Comparing base (dc82c73) to head (04edb95).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...idpy/experimental/tl/_align/_stalign_impl/_core.py 0.00% 200 Missing ⚠️
src/squidpy/experimental/tl/_align/_stalign.py 30.26% 158 Missing and 1 partial ⚠️
...y/experimental/tl/_align/_stalign_impl/_helpers.py 0.00% 69 Missing ⚠️
src/squidpy/experimental/tl/_align/_api.py 58.45% 53 Missing and 6 partials ⚠️
src/squidpy/experimental/tl/_align/_io.py 53.57% 18 Missing and 8 partials ⚠️
src/squidpy/experimental/im/_rasterize_points.py 77.22% 12 Missing and 11 partials ⚠️
src/squidpy/_validators.py 70.00% 2 Missing and 1 partial ⚠️
src/squidpy/experimental/tl/_align/_landmark.py 93.75% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1195      +/-   ##
==========================================
- Coverage   78.95%   75.85%   -3.11%     
==========================================
  Files          63       70       +7     
  Lines        9439    10397     +958     
  Branches     1574     1704     +130     
==========================================
+ Hits         7453     7887     +434     
- Misses       1469     1971     +502     
- Partials      517      539      +22     
Files with missing lines Coverage Δ
src/squidpy/types.py 100.00% <100.00%> (ø)
src/squidpy/experimental/tl/_align/_landmark.py 93.75% <93.75%> (ø)
src/squidpy/_validators.py 87.23% <70.00%> (-2.18%) ⬇️
src/squidpy/experimental/im/_rasterize_points.py 77.22% <77.22%> (ø)
src/squidpy/experimental/tl/_align/_io.py 53.57% <53.57%> (ø)
src/squidpy/experimental/tl/_align/_api.py 58.45% <58.45%> (ø)
...y/experimental/tl/_align/_stalign_impl/_helpers.py 0.00% <0.00%> (ø)
src/squidpy/experimental/tl/_align/_stalign.py 30.26% <30.26%> (ø)
...idpy/experimental/tl/_align/_stalign_impl/_core.py 0.00% <0.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/squidpy/experimental/tl/__init__.py Resolved
radius = int(np.ceil(float(np.max(blur_values)) * 4.0))
denom = 2.0 * (dx * blur_values * 2.0) ** 2

for x_i, y_i in zip(x, y, strict=False):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How many points do we have? Should probably rasterize

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdym? This is the function for rasterization?

Comment thread pyproject.toml Resolved
@selmanozleyen selmanozleyen changed the title feat: stalign with a fit apply pattern. Also foundation for imputation feat: stalign with a fit apply pattern. Jun 20, 2026
@selmanozleyen selmanozleyen self-assigned this Jun 20, 2026
@selmanozleyen selmanozleyen changed the title feat: stalign with a fit apply pattern. feat: stalign api and landmark alignment Aug 11, 2026
@selmanozleyen
selmanozleyen requested a review from timtreis August 22, 2026 14:31
@selmanozleyen
selmanozleyen changed the base branch from main to feat/rng-seeding August 22, 2026 21:02
@selmanozleyen
selmanozleyen changed the base branch from feat/rng-seeding to main August 22, 2026 21:03
@selmanozleyen
selmanozleyen changed the base branch from main to feat/api-docs-restructure August 30, 2026 21:33
@selmanozleyen
selmanozleyen force-pushed the feat/api-docs-restructure branch 3 times, most recently from 3c54e85 to 7b33737 Compare August 30, 2026 23:20
@selmanozleyen
selmanozleyen force-pushed the feat/api-docs-restructure branch 3 times, most recently from d594650 to 11d8760 Compare September 25, 2026 16:05
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-fit-core branch from 69dae69 to 76d69a7 Compare September 25, 2026 16:05
@selmanozleyen
selmanozleyen force-pushed the feat/api-docs-restructure branch from 11d8760 to e5f0d6b Compare September 25, 2026 19:42
The volume fit never passes landmarks, so the point term is zero and the
value was dead; it existed only because `lddmm` required the argument.
`sigmaP` is now optional there and checked only when points are given.
test_stalign_reference.py, moved from squidpy-ports, compares the kernel, the
fits and the public entry points against values the original STalign computed.
They come from one zarr store squidpy-ports publishes, fetched with pooch by
sha256. The jax CI env sets JAX_ENABLE_X64, which the comparison needs.
…guide

They moved from extras to [dependency-groups], so `.[dev,test,doc]` installed
none of them and `uv sync --all-extras` only the default dev group.
- One session fixture downloads the bundle to scanpy's datasetdir, reads it
  into memory and closes the store; the fixture parameters are plain
  constants, checked against the bundle's root attributes in one test.
- The x64 requirement is a module-level skip, and the download is marked
  internet.
- Drop the unused slice_target_axes fixture, _run_image's unused argument,
  the clouds namespace, a duplicate velocity-grid test, and the sigmaP the
  landmark-free runs passed for nothing (_lddmm_loss now accepts None).
- Recover an axis' placement from its first step instead of four guesses.
- test_params.py goes back to main: since Array became HasArrayNamespace,
  every union arm is isinstance-checkable and _safe_matches never caught.
tests/experimental/conftest.py goes back to main: make_sdata_tables had no
caller, ALIGN_PTS and TINY_SOLVER one file each, and each file needs its own
variant of make_adata.
Rationale for a choice goes in the PR, not beside the line. What stays is one
line each where the code cannot show it: an upstream line reference, a JAX
indexing gotcha, why a division is split for bit-identity. Two comments were
also wrong: niter=0 evaluates nothing, and the initial weights were listed out
of order.
_check_direction only renamed a normalize_choice call, and reverse_axes and
_grid_points had one caller each.
@selmanozleyen
selmanozleyen marked this pull request as draft October 29, 2026 14:42
selmanozleyen and others added 14 commits October 29, 2026 15:43
transform_points maps query into reference on every fit; rank-2 fits add
inverse_transform_points and, for images, inverse_warp_image. The volume fit
no longer takes a `direction` it could only refuse, and its deformation_grid
no longer defaults to the solver's "backward": both fits' grids now mean the
query grid in the reference frame. The solver's own forward/backward stays
private.
- A 2D image passed where a volume is needed (or the reverse) is refused by
  its `z` dim, instead of its channels being read as z.
- Axes must be evenly spaced, and sample_volume's must match the volume.
- apply_affine takes only a (3, 3) matrix and finite points.
- A fit that ends with non-finite parameters raises FloatingPointError.
- The rotation error no longer points at an axes argument that doesn't exist.
- A test asserted a dataclass, which is always true; it now just runs the call.
It mapped the volume onto the section, the solver's moving-to-fixed order and
the reverse of the 2D paths. It now means section -> volume, the same map
transform_points gives, and is inverted for the solver.
stalign_align_image and stalign_align_volume take scale (one level, or a
(ref, query) pair; default scale0). The physical axes come from that level's
own transformation, so a coarser level fits faster in the same units.
It is public and JAX-free but had no test of its own.
squidpy-ports now publishes it as an AnnData in a zipped zarr store. Provenance
is checked for every section, not a hardcoded list that missed one.
…read aux

- Non-public functions keep a one- or two-line docstring; the rationale and
  parameter prose they carried is gone (users read tol/patience on the
  Stalign*Params types).
- interp works on the arrays it is given instead of re-casting them, and the
  single-sample axis check moves to resolve_axes, which every fit's axes pass.
- The `_axis = _rasterize_points.axis` alias becomes a direct import.
- _lddmm_loss's aux keeps what is read (the contrast-mapped source, and the
  regularisation energy the reference suite pins); landmarks are transformed
  only when there are some.
…tests

- The container helpers the fit classes use (table lookup, obsm read and
  write-back, the frame check) move to _io, which imports neither _api nor
  _stalign; fit serialisation moves next to the to_uns/from_uns methods it
  backs. Nothing is imported inside a method any more.
- _api's own channels-first coercion goes (the estimators run as_chw), and
  the obsm and fit-point shape checks reuse validate_xy, gaining its finite
  check.
- Three landmark tests repeated others line for line; the "returns None" check
  of the one dropped is kept in the test that inspects the registered values.
- With `tol` set, early stopping no longer fires before `diffeo_start`, where it
  could return a fit whose velocity was never updated.
- `rasterize_points` refuses `expand < 1`, and its mass claim says when it holds.
- `writeback_affine_sdata` checks every element before writing any, so a
  refusal leaves nothing half-registered.
- A `scale` level that doesn't exist raises, listing the levels, instead of
  falling back to scale0.
- `warp_image` checks the image against the axes it is sampled on.
- `align_landmarks` refuses a same-object table reference whose elements would
  move, and non-point landmark geometries.
- In the jax CI env a missing jax fails the run instead of skipping it.
… classes

- stalign_align_obs takes ref/query_coordinate_system and, for SpatialData,
  refuses obsm coordinates whose element is not placed there by the identity,
  the rule the image path already applies. Every fit now records the query
  frame, so transform checks it for all of them.
- initial_velocity / velocity_grid leave the public params and lddmm: nothing
  passed them, and a continued fit silently restarted its weights and schedule.
- The rank-2 fits share one inverse_transform_points; deformation_grid takes
  only the query axes it uses; _warp resolves its axes once; aligned_points is
  transform_points of the query.
- centred_axes's always-1 scale goes (inlined); _rasterize_points exports only
  its public functions; the tol/patience, axes and registration-target docs say
  what the code does.
- The mixed-unit velocity grid is compared by value, with no solver run first.
- The obs smoke test checks that what transform writes is the fit's
  aligned_points, not only its shape.
- The public-call reference tests say what they assert instead of naming the
  retired notebook harness.
- A landmark test that only checked a key existed goes; the two-pair minimum
  is asserted once, not once per `side`; the sample_volume test moves next to
  the rest of its module's tests.
- hatch.toml: the jax matrix row is aligned and each env var has its own
  comment.
The test kept its own copies of the image, slice, LDDMM and raster settings
the bundle was generated with. stalign-reference-v3 records all of them in
uns["fixtures"], so a `params` fixture reads them from there and the copies,
and the test that held them to the bundle, go.
- test_image_landmarks_reach_the_solver starts both fits from the same affine,
  so only the point term can tell them apart; before, landmarks also set the
  start and the test passed with the term dropped.
- A velocity grid with a single sample along an axis (`a` too wide for the
  image) raises naming `a` and `expand`, instead of fitting on a NaN kernel and
  blaming the step sizes when it diverges.
- Landmark arrays must pair up row by row even with `initial_affine`, which
  skipped the only check that compared them.
- StalignImageParams says its lengths and steps are in the coordinate system's
  units, with defaults sized for roughly pixel-sized ones.
@selmanozleyen
selmanozleyen marked this pull request as ready for review October 29, 2026 16:45
In the default float32, micron-scale axes (np.arange(200) * 2.7 + 1000)
carry spacing errors above allclose's tolerance once cast, so evenly spaced
axes were refused. The float64 CI env could not see it; the new test runs in
whatever precision the env has.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants