Skip to content

BF: Return complex data from get_fdata() for complex images - #1528

Open
Leonard013 wants to merge 1 commit into
nipy:masterfrom
Leonard013:fix/975-get-fdata-complex
Open

BF: Return complex data from get_fdata() for complex images#1528
Leonard013 wants to merge 1 commit into
nipy:masterfrom
Leonard013:fix/975-get-fdata-complex

Conversation

@Leonard013

@Leonard013 Leonard013 commented Jul 12, 2026

Copy link
Copy Markdown

Closes #975.

Why

get_fdata() defaults to dtype=np.float64. For a complex-valued image this
silently discards the imaginary part (emitting a NumPy ComplexWarning), so
users of complex data (e.g. MR spectroscopy via spec2nii) lose data unless they
know to pass dtype=np.complex64/128 explicitly.

In #975 both maintainers agreed on the fix. The last substantive comment
(@matthew-brett, 2020-12-05) settles the scope:

"if the image-implied dtype was complex, we'd return complex128 by default,
instead of float64, and we'd respect the explicit dtype whatever that was."

@effigies (2020-12-03): "I think detecting the complex case is worth doing."
(He also floated a broader dtype-resolution overhaul with a new mode argument;
this PR implements only the minimal, agreed behavior and does not preclude that
larger work.)

What

In DataobjImage.get_fdata():

  • Change the signature default from dtype=np.float64 to dtype=None (sentinel
    for "not specified").
  • When dtype is None: if self._dataobj.dtype is complex, resolve to
    np.promote_types(obj_dtype, np.complex128) (floors at complex128 but
    preserves wider complex types, e.g. complex256); otherwise np.float64
    (unchanged).
  • When dtype is passed: respect it exactly (unchanged).
  • Update the return / _fdata_cache type hints from np.floating to
    np.inexact, the docstring, and broaden the validation error message to
    "floating point or complex type".

Real-valued images and every explicit-dtype call are unaffected. Caching is
unchanged (dtype is resolved to a concrete dtype before the cache lookup).

Design note — wider-than-complex128 data (complex256)

NIfTI datatype code 2048 maps to np.clongdouble, which on Linux/x86 (the
primary CI platform) is complex256wider than complex128. A naive
dtype = np.complex128 default would silently downcast such data (the same class
of silent loss this PR fixes). Using np.promote_types(obj_dtype, np.complex128)
returns complex128 for complex64/complex128 and complex256 for
complex256 — never narrower, never lossy.

Tests

Added to nibabel/tests/test_image_api.py:

  • test_get_fdata_complex[complex64/complex128] — array and proxy images:
    default get_fdata() returns complex128 and preserves the imaginary part;
    explicit dtype=complex64/128 respected; explicit dtype=float64 still drops
    imag.
  • test_get_fdata_real_unchanged[uint8/int16/float32/float64] — real images
    still default to float64 (no regression).
  • test_get_fdata_complex256clongdouble image preserves complex256,
    guarded with skipif(np.dtype(np.clongdouble) == np.dtype(np.complex128)) so
    it runs on Linux/x86 CI and no-ops where clongdouble isn't wider.

Fail-before / pass-after verified. Full suite adds no regressions (the complex
cases fail on master with the imaginary part dropped, pass here).

⚠️ Default behavior change (please confirm)

This changes the default return dtype for complex images (float64
complex128). Code that calls get_fdata() on a complex image and assumes a
real float64 array would now receive complex output. This is the intended fix
(the old behavior was lossy and warned), but it is a behavior change — happy to
add an "API changes" changelog note, or a deprecation cycle if you'd prefer.
Callers wanting the old behavior can pass dtype=np.float64.

Since #975 is from 2020, flagging for @effigies / @matthew-brett: is the minimal
complex128-default approach (vs. the broader dtype-mode idea) still what you'd
like? Glad to adjust.

(Changelog fragment omitted from the commit — nibabel entries reference the PR
number and there's no open "upcoming" section at HEAD; happy to add one.)

``DataobjImage.get_fdata()`` defaulted to ``dtype=np.float64``, so calling
it on a complex-valued image silently discarded the imaginary part (with a
NumPy ComplexWarning).

Make the default dtype depend on the image: when no ``dtype`` is passed and
the data object has a complex dtype, return ``np.promote_types(obj_dtype,
np.complex128)`` -- i.e. at least ``complex128`` (preserving the imaginary
part), but wider complex types such as ``complex256`` are preserved rather
than silently downcast. Otherwise return ``np.float64`` as before. An
explicitly passed ``dtype`` is always respected, so real-valued images and
callers that request a specific dtype are unaffected.

Closes nipygh-975.

This change was developed with AI assistance (Claude Code); it has been
reviewed and verified locally by the commit author.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

BUG: Complex data cast to float in get_fdata()

1 participant