Skip to content

feat: anisotropic yx pixel size for fluorescence and phase#568

Draft
talonchandler wants to merge 12 commits into
mainfrom
feat/anisotropic-yx-pixel-size
Draft

feat: anisotropic yx pixel size for fluorescence and phase#568
talonchandler wants to merge 12 commits into
mainfrom
feat/anisotropic-yx-pixel-size

Conversation

@talonchandler

@talonchandler talonchandler commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #567.

Adds support for non-square pixels in fluorescence (2D + 3D) and phase (2D + 3D) reconstructions. Birefringence paths raise NotImplementedError when y != x. Scalar configs and y == x configs keep working everywhere.

Config (backwards compatible).

yx_pixel_size: 0.3

yx_pixel_size:
  y: 0.3
  x: 0.25

Both forms parse to a normalized YXPixelSize(y, x) internally.

What changed. New YXPixelSize value type with a from_value constructor that accepts scalar, mapping, or itself. Pydantic settings field flipped to YXPixelSize with a before-validator and a serializer that emits a scalar when isotropic so YAML stays tidy. util helpers, all phase and fluorescence models, the api/phase and api/fluorescence consumers, and the CLI output metadata path all accept YXPixelSize | float. Anisotropic Nyquist upsampling (separate y_factor and x_factor) in the four affected models. One new anisotropic example at docs/examples/api/phase_3d_anisotropic.py.

Tests (de-risking). 46 new tests. Settings: scalar and dict YAML parsing, YAML round-trip in both forms, typo and negative rejection. Utilities: isotropic equivalence vs legacy scalar, anisotropic frequency and sphere generation. Each modality (phase 2D/3D, fluor 2D/3D): isotropic equivalence (y == x reproduces legacy output bit for bit) plus anisotropic smoke. Birefringence: anisotropic input raises NotImplementedError. CLI: anisotropic config writes distinct y and x scales to the output zarr, mismatch warning reports y and x independently. Full regression benchmark suite passes with identical metrics.

Out of scope. Birefringence anisotropic support. Shift-variant fluorescence. Z pixel size stays scalar.

Sri to validate on DaXi zebrafish data before this merges.

Introduces a frozen pydantic model representing a lateral pixel size
that may be isotropic or anisotropic. The from_value classmethod
normalizes a scalar, mapping, or existing YXPixelSize into a single
canonical form, intended for use at internal function boundaries that
accept either a plain float (treated as isotropic) or this model.
generate_frequencies, generate_radial_frequencies, generate_sphere_target
and gen_coordinate now normalize their pixel size argument through
YXPixelSize.from_value at entry, so callers may pass either a scalar
(legacy isotropic shorthand) or a YXPixelSize. fftfreq uses .y and .x
separately, which produces identical output to the old scalar path
when y == x.
phase_thick_3d and isotropic_thin_3d normalize their yx_pixel_size
argument through YXPixelSize.from_value and compute independent y and
x Nyquist upsampling factors. The upsampled YX shape becomes
(Y * y_factor, X * x_factor); under isotropic input (y == x) this
collapses to the legacy single-factor path and the output matches
exactly.

api/phase.py simulate normalizes once at entry so the generated
coordinate arrays use the y and x spacings independently.

Tests add isotropic-equivalence cases (anisotropic input with y == x
produces identical transfer functions to the legacy scalar call) and
anisotropic smoke cases for both phase models and the reconstruct
wrappers.
isotropic_fluorescent_thick_3d and isotropic_fluorescent_thin_3d
normalize their yx_pixel_size argument through YXPixelSize.from_value
and compute independent y and x Nyquist upsampling factors. With
isotropic input (y == x) the output exactly matches the legacy
scalar path.

api/fluorescence.py simulate normalizes once at entry so the
generated coordinate arrays use the y and x spacings independently.

Tests add isotropic-equivalence and anisotropic smoke cases for both
fluorescence models and the reconstruct wrappers.
apply_inverse_transfer_function normalizes the configured
yx_pixel_size through YXPixelSize.from_value before composing the
output zarr scale, so anisotropic configs write distinct y and x
spacings. The pixel-size-mismatch warning also reports y and x
independently, with the x line suppressed when only y differs (and
vice versa).

Tests cover the anisotropic warning path and confirm an isotropic
config emits no warning when the input scale matches.
inplane_oriented_thick_pol3d_vector.calculate_transfer_function and
the api/birefringence_and_phase simulate routine now normalize the
incoming yx_pixel_size and raise NotImplementedError when y != x. A
shared config with equal y and x continues to run through both paths
unchanged.

This keeps anisotropic-aware fluorescence and phase reconstructions
behind a clean guard, so a config that accidentally routes to
birefringence with anisotropic spacing fails loudly instead of
producing a quietly-wrong result.
FourierTransferFunctionSettings.yx_pixel_size is now a YXPixelSize. A
before-validator normalizes scalar or {y, x} mapping forms into a
YXPixelSize, so existing YAML configs (yx_pixel_size: 0.3) continue
to parse unchanged and the new {y, x} mapping form is accepted.

A model_serializer on YXPixelSize emits scalar form when isotropic
so dumped configs stay tidy. The pixel-size and emission-wavelength
ratio warnings now report y and x independently and self-heal when
model_copy(update=...) bypasses validation. A field_serializer makes
serialization robust through the same bypass.

Tests cover scalar and dict YAML parsing, isotropic-equals-mapping
equivalence, YAML round-trip in both forms, end-to-end CLI write of
distinct y and x scales into the output zarr, and validation of typo
and negative inputs.
Documents the dict form of yx_pixel_size end-to-end for callers
working on rigs with non-square pixels. The scalar form continues to
work everywhere; this example just shows the new option.
Picks up the updated yx_pixel_size description from
FourierTransferFunctionSettings (scalar or {y, x} mapping for
anisotropic). Generated by tests/cli_tests/test_settings.py::
test_generate_example_settings.
@talonchandler

Copy link
Copy Markdown
Collaborator Author

Hi @srivarra, give this a try on your next zebrafish reconstruction trial.

@srivarra

Copy link
Copy Markdown
Contributor

@talonchandler Cool, I will give it a shot.

talonchandler and others added 3 commits June 25, 2026 15:46
CI surfaced callers (napari widget reading zarr scale, the legacy
2D_QLIPP and PTI example scripts) that pass numpy floats or 0-d
ndarrays where a Python float was expected. Switch the scalar branch
to a float() cast so any float-castable input works (numpy scalars,
0-d ndarrays, 0-d torch tensors). Explicit rejects for bool and str
preserve the previous error behavior. Multi-element arrays still
raise.

Tests cover np.float64, 0-d ndarray, 0-d tensor, and a multi-element
array rejection.
The napari tab_recon widget recurses into nested BaseModel fields to
build a sub-container, reading each inner field's default. Without
defaults on y and x, the widget rendered them as undefined, and the
form submit produced y=0 / x=0, failing PositiveFloat.

Matching the legacy scalar default (0.1) gives the GUI a sensible
starting value and keeps the existing yx_pixel_size=0.1 default on
FourierTransferFunctionSettings consistent with what users see when
they click New.
fix(fluorescence): pass confocal_pinhole_diameter into compute_transfer_function

The setting is defined on the fluorescence Settings but was never forwarded to
calculate_transfer_function (both recon_dim 2 and 3 branches), so a configured
confocal pinhole was silently ignored and every reconstruction ran widefield.
Thread s.confocal_pinhole_diameter through both branches.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

Support anisotropic yx pixel sizes in fluorescence and phase reconstructions

2 participants