Skip to content

fix(granularity): guard division when subsampling collapses an axis#91

Merged
afermg merged 5 commits into
mainfrom
worktree-fix-issue-90-granularity-zerodiv
Jun 29, 2026
Merged

fix(granularity): guard division when subsampling collapses an axis#91
afermg merged 5 commits into
mainfrom
worktree-fix-issue-90-granularity-zerodiv

Conversation

@afermg

@afermg afermg commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Closes #90.

For inputs with an axis shorter than 1/subsample_size, new_shape[k] collapses to 1 and the back-projection upsample divides by new_shape[k] - 1 == 0. Added _safe_ratio to return 0 in that case (the single output sample along the collapsed axis just reads back_pixels[0]), used at all five 2D/3D call sites. Bit-identical behavior whenever new_shape[k] > 1.

Tests cover the 2D and 3D paths.

cc @timtreis — this overlaps with the same upsample geometry your fused-operator perf PR rewrites in #76; you may want to fold the guard in when rebasing.

The `get_granularity` function could encounter a division by zero
error when an axis of the input mask collapses to a size of 1 during
subsampling (e.g., when `subsample_size=0.25` and the axis length is
< 8).  This occurred in the `map_coordinates` scaling factor
calculation.

Introduce `_safe_ratio` to return 0 when the denominator is 0,
correctly handling this edge case.

Adds new regression tests for 2D and 3D collapsed axes (issue #90).
@afermg afermg requested a review from timtreis June 26, 2026 21:53
The `num` and `den` parameters are already typed as `float`, making
the explicit `float()` casts redundant before division.
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Benchmark — 539ac7c vs main

speedup = main/head (>1 faster, <1 slower) · median per cell · showing functions that moved ≥1.05× either way

manders_fold

size \ objects 16 64 256
256 1.02× 1.00× 1.01×
512 0.99× 1.00× 1.08×
1024 1.03× 1.04× 0.96×

radial_zernikes

size \ objects 16 64 256
256 0.98× 0.99× 1.01×
512 0.98× 0.98× 1.00×
1024 1.08× 1.00× 0.99×

zernike

size \ objects 16 64 256
256 1.00× 0.99× 1.00×
512 0.99× 0.98× 1.02×
1024 1.05× 1.01× 1.00×

afermg added 2 commits June 26, 2026 18:31
The `subsample_size` calculation could result in `new_shape`
dimensions of 0 or 1, leading to: - Division by zero in coordinate
mapping (when `new_shape[k] - 1 == 0`).  - Empty array errors in
subsequent morphology operations (when `new_shape[k] == 0`).  This
change clamps the calculated `new_shape` dimensions to a minimum of
2 to resolve these issues (addresses #90).
A meaningful granularity spectrum cannot be computed when
subsampling reduces an axis to fewer than two samples. Return NaN to
prevent crashes and indicate an undefined spectrum. Addresses issue
effective_shape = (
(orig_shape * subsample_size).astype(int) if subsample_size < 1 else orig_shape
)
if (effective_shape < 2).any():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is granularity supported for 3D? I feel like this any check might cause unforseen complications if there's a third axis at play. Might be more robust to have that change conditional either on the shape length or the canonical positions for x/y

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This .any() essentially checks that all dimensions have at least 2 samples, regardless of whether or not it's 2 or 3D.

Comment thread test/test_granularity.py Outdated
@@ -1,37 +1,18 @@
"""Regression tests for ``get_granularity`` (issue #90).
"""Regression test for ``get_granularity`` (issue #90).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Probably shouldn't cite the issue in this line because we'll 100% forget to update this when we add more tests some day

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

(fixed), removed the issue tagging

@timtreis timtreis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two small nits, approving anyway so you can just merge afterwards

@afermg afermg merged commit 4fc0142 into main Jun 29, 2026
17 checks passed
@afermg afermg deleted the worktree-fix-issue-90-granularity-zerodiv branch June 29, 2026 03:30
@afermg

afermg commented Jun 29, 2026

Copy link
Copy Markdown
Owner Author

Merged, feel free to rebase the other ongoing changes into main.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

get_granularity raises ZeroDivisionError when resized shape has a value of 1

2 participants