From 03fcd021d85eaaa4f6d2f89e5a32450f35fdd0b4 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:29:26 -0500 Subject: [PATCH 001/199] Backport PR #18823: Fix a typo in `coordinates` docs --- docs/coordinates/frames.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/coordinates/frames.rst b/docs/coordinates/frames.rst index bbc7069dc69c..9ab579b04f3a 100644 --- a/docs/coordinates/frames.rst +++ b/docs/coordinates/frames.rst @@ -176,9 +176,8 @@ if it is present, it can be accessed from the ``data`` attribute:: -All of the above methods can also accept array data (in the form of -class:`~astropy.units.Quantity`, or other Python sequences) to create arrays of -coordinates:: +All of the above methods can also accept array data (in the form of |Quantity|, +or other Python sequences) to create arrays of coordinates:: >>> ICRS(ra=[1.5, 2.5]*u.deg, dec=[3.5, 4.5]*u.deg) # doctest: +FLOAT_CMP Date: Mon, 3 Nov 2025 13:31:23 -0500 Subject: [PATCH 002/199] Delete greeter bot on v7.2.x branch [ci skip] --- .github/workflows/open_actions.yml | 47 ------------------------------ 1 file changed, 47 deletions(-) diff --git a/.github/workflows/open_actions.yml b/.github/workflows/open_actions.yml index a64743ce0389..1733dac6807e 100644 --- a/.github/workflows/open_actions.yml +++ b/.github/workflows/open_actions.yml @@ -44,53 +44,6 @@ jobs: - [ ] Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied? - [ ] At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) *before* merge.` }) - - name: Greet new contributors - uses: actions/first-interaction@1c4688942c71f71d4f5502a26ea67c331730fa4d # v3.1.0 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - issue-message: > - Welcome to Astropy 👋 and thank you for your first issue! - - - A project member will respond to you as soon as possible; in - the meantime, please double-check the [guidelines for submitting - issues](https://github.com/astropy/astropy/blob/main/CONTRIBUTING.md#reporting-issues) - and make sure you've provided the requested details. - - - GitHub issues in the Astropy repository are used to track bug - reports and feature requests; If your issue poses a question about - how to use Astropy, please instead raise your question in the - [Astropy Discourse user - forum](https://community.openastronomy.org/c/astropy/8) and close - this issue. - - - If you feel that this issue has not been responded to in a timely - manner, please send a message directly to the [development - mailing list](http://groups.google.com/group/astropy-dev). If - the issue is urgent or sensitive in nature (e.g., a security - vulnerability) please send an e-mail directly to the private e-mail - feedback@astropy.org. - pr-message: > - Welcome to Astropy 👋 and congratulations on your first pull - request! 🎉 - - - A project member will respond to you as soon as possible; in the - meantime, please have a look over the [Checklist for Contributed - Code](https://github.com/astropy/astropy/blob/main/CONTRIBUTING.md#checklist-for-contributed-code) - and make sure you've addressed as many of the questions there as - possible. - - - If you feel that this pull request has not been responded to in a - timely manner, please send a message directly to the - [development mailing - list](http://groups.google.com/group/astropy-dev). If the issue is - urgent or sensitive in nature (e.g., a security vulnerability) - please send an e-mail directly to the private e-mail - feedback@astropy.org. - name: 'Comment Draft PR' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 if: github.event.pull_request.draft == true From de95faf2b7fafb109ef500af4a5506f800a6ce61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Tue, 4 Nov 2025 16:26:36 +0100 Subject: [PATCH 003/199] Backport PR #18175: Update `sortedcontainers` version requirement --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b5dbbd186438..14fae5f42ad7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ all = [ "beautifulsoup4>=4.9.3", # imposed by pandas==2.0 "html5lib>=1.1", "bleach>=3.2.1", - "sortedcontainers>=1.5.7", # (older versions may work) + "sortedcontainers>=2.1.0", # imposed by testing with hypothesis "pytz>=2016.10", # (older versions may work) "jplephem>=2.17.0", "mpmath>=1.2.1", From af635234f8ee935773986c55c6b25c69bee0a105 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Wed, 5 Nov 2025 09:33:35 -0500 Subject: [PATCH 004/199] Backport PR #18821: BUG: resolve incompatibilities in `units` and `utils.masked` with numpy 2.4 (dev) --- .../units/quantity_helper/function_helpers.py | 71 +++++-- .../units/tests/test_quantity_non_ufuncs.py | 8 +- astropy/utils/masked/function_helpers.py | 195 ++++++++++++++---- .../masked/tests/test_function_helpers.py | 10 +- 4 files changed, 223 insertions(+), 61 deletions(-) diff --git a/astropy/units/quantity_helper/function_helpers.py b/astropy/units/quantity_helper/function_helpers.py index ec411be87576..ca3487e86cca 100644 --- a/astropy/units/quantity_helper/function_helpers.py +++ b/astropy/units/quantity_helper/function_helpers.py @@ -345,8 +345,7 @@ def full_like(a, fill_value, *args, **kwargs): return (a.view(np.ndarray), a._to_own_unit(fill_value)) + args, kwargs, unit, None -@function_helper -def putmask(a, mask, values): +def putmask_impl(a, /, mask, values): from astropy.units import Quantity if isinstance(a, Quantity): @@ -357,6 +356,18 @@ def putmask(a, mask, values): raise NotImplementedError +if not NUMPY_LT_2_4: + + @function_helper + def putmask(a, /, mask, values): + return putmask_impl(a, mask=mask, values=values) +else: + + @function_helper + def putmask(a, mask, values): + return putmask_impl(a, mask=mask, values=values) + + @function_helper def place(arr, mask, vals): from astropy.units import Quantity @@ -468,12 +479,26 @@ def _iterable_helper(*args, out=None, **kwargs): return arrays, kwargs, unit, out -@function_helper -def concatenate(arrays, axis=0, out=None, **kwargs): - # TODO: make this smarter by creating an appropriately shaped - # empty output array and just filling it. - arrays, kwargs, unit, out = _iterable_helper(*arrays, out=out, axis=axis, **kwargs) - return (arrays,), kwargs, unit, out +if NUMPY_LT_2_4: + + @function_helper + def concatenate(arrays, axis=0, out=None, **kwargs): + # TODO: make this smarter by creating an appropriately shaped + # empty output array and just filling it. + arrays, kwargs, unit, out = _iterable_helper( + *arrays, out=out, axis=axis, **kwargs + ) + return (arrays,), kwargs, unit, out +else: + + @function_helper + def concatenate(arrays, /, axis=0, out=None, **kwargs): + # TODO: make this smarter by creating an appropriately shaped + # empty output array and just filling it. + arrays, kwargs, unit, out = _iterable_helper( + *arrays, out=out, axis=axis, **kwargs + ) + return (arrays,), kwargs, unit, out def _block(arrays, max_depth, result_ndim, depth=0): @@ -777,7 +802,7 @@ def pad(array, pad_width, mode="constant", **kwargs): @function_helper -def where(condition, *args): +def where(condition, /, *args): from astropy.units import Quantity if isinstance(condition, Quantity) or len(args) != 2: @@ -880,14 +905,14 @@ def cross_like_a_b(a, b, *args, **kwargs): return (a.view(np.ndarray), b.view(np.ndarray)) + args, kwargs, unit, None -@function_helper( - helps={ - np.inner, - np.vdot, - np.correlate, - np.convolve, - } -) +@function_helper(helps={np.inner, np.vdot}) +def cross_like_a_b_posonly(a, b, /): + a, b = _as_quantities(a, b) + unit = a.unit * b.unit + return (a.view(np.ndarray), b.view(np.ndarray)), {}, unit, None + + +@function_helper(helps={np.correlate, np.convolve}) def cross_like_a_v(a, v, *args, **kwargs): a, v = _as_quantities(a, v) unit = a.unit * v.unit @@ -911,7 +936,7 @@ def einsum(*operands, out=None, **kwargs): @function_helper -def bincount(x, weights=None, minlength=0): +def bincount(x, /, weights=None, minlength=0): from astropy.units import Quantity if isinstance(x, Quantity): @@ -1277,7 +1302,15 @@ def array2string(a, *args, **kwargs): # also work around this by passing on a formatter (as is done in Angle). # So, we do nothing if the formatter argument is present and has the # relevant formatter for our dtype. - formatter = args[6] if len(args) >= 7 else kwargs.get("formatter") + import inspect + + sig = inspect.signature(np.array2string) + formatter_arg_pos = list(sig.parameters).index("formatter") - 1 + formatter = ( + args[formatter_arg_pos] + if len(args) >= formatter_arg_pos + else kwargs.get("formatter") + ) if formatter is None: a = a.value diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index 5ae22f17ac8e..a6d8aae9efa5 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -2066,9 +2066,11 @@ def test_array2string(self): expected2 = "[0.0 Jy, 1.0 Jy, 2.0 Jy]" assert out2 == expected2 # Also as positional argument (no, nobody will do this!) - out3 = np.array2string( - self.q, None, None, None, ", ", "", np._NoValue, {"float": str} - ) + if NUMPY_LT_2_4: + args = (self.q, None, None, None, ", ", "", np._NoValue, {"float": str}) + else: + args = (self.q, None, None, None, ", ", "", {"float": str}) + out3 = np.array2string(*args) assert out3 == expected2 # But not if the formatter is not relevant for us. out4 = np.array2string(self.q, separator=", ", formatter={"int": str}) diff --git a/astropy/utils/masked/function_helpers.py b/astropy/utils/masked/function_helpers.py index 61aeef1e4cc9..48ea494f6caf 100644 --- a/astropy/utils/masked/function_helpers.py +++ b/astropy/utils/masked/function_helpers.py @@ -297,9 +297,8 @@ def outer(a, b, out=None): if not NUMPY_LT_2_0: - @dispatched_function - def empty_like( - prototype, dtype=None, order="K", subok=True, shape=None, *, device=None + def empty_like_impl( + prototype, /, dtype=None, order="K", subok=True, shape=None, *, device=None ): """Return a new array with the same shape and type as a given array. @@ -330,6 +329,35 @@ def empty_like( return unmasked, mask, None + if not NUMPY_LT_2_4: + + @dispatched_function + def empty_like( + prototype, /, dtype=None, order="K", subok=True, shape=None, *, device=None + ): + return empty_like_impl( + prototype, + dtype=dtype, + order=order, + subok=subok, + shape=shape, + device=device, + ) + else: + + @dispatched_function + def empty_like( + prototype, dtype=None, order="K", subok=True, shape=None, *, device=None + ): + return empty_like_impl( + prototype, + dtype=dtype, + order=order, + subok=subok, + shape=shape, + device=device, + ) + @dispatched_function def zeros_like(a, dtype=None, order="K", subok=True, shape=None, *, device=None): """Return an array of zeros with the same shape and type as a given array. @@ -457,8 +485,7 @@ def put(a, ind, v, mode="raise"): np.put(a.mask, ind, v_mask, mode=mode) -@dispatched_function -def putmask(a, mask, values): +def putmask_impl(a, /, mask, values): """Changes elements of an array based on conditional and input values. Like `numpy.putmask`, but for masked array ``a`` and possibly masked @@ -477,6 +504,18 @@ def putmask(a, mask, values): np.putmask(a.mask, mask, values_mask) +if not NUMPY_LT_2_4: + + @dispatched_function + def putmask(a, /, mask, values): + return putmask_impl(a, mask=mask, values=values) +else: + + @dispatched_function + def putmask(a, mask, values): + return putmask_impl(a, mask=mask, values=values) + + @dispatched_function def place(arr, mask, vals): """Change elements of an array based on conditional and input values. @@ -518,14 +557,14 @@ def copyto(dst, src, casting="same_kind", where=True): @dispatched_function -def packbits(a, *args, **kwargs): +def packbits(a, /, *args, **kwargs): result = np.packbits(a.unmasked, *args, **kwargs) mask = np.packbits(a.mask, *args, **kwargs).astype(bool) return result, mask, None @dispatched_function -def unpackbits(a, *args, **kwargs): +def unpackbits(a, /, *args, **kwargs): result = np.unpackbits(a.unmasked, *args, **kwargs) mask = np.zeros(a.shape, dtype="u1") mask[a.mask] = 255 @@ -534,7 +573,7 @@ def unpackbits(a, *args, **kwargs): @dispatched_function -def bincount(x, weights=None, minlength=0): +def bincount(x, /, weights=None, minlength=0): """Count number of occurrences of each value in array of non-negative ints. Like `numpy.bincount`, but masked entries in ``x`` will be skipped. @@ -600,8 +639,7 @@ def sort_complex(a): return result, None, None -@dispatched_function -def concatenate(arrays, axis=0, out=None, dtype=None, casting="same_kind"): +def concatenate_impl(arrays, /, axis=0, out=None, *, dtype=None, casting="same_kind"): data, masks = _get_data_and_mask_arrays(arrays) if out is None: return ( @@ -619,6 +657,22 @@ def concatenate(arrays, axis=0, out=None, dtype=None, casting="same_kind"): return out, None, None +if NUMPY_LT_2_4: + + @dispatched_function + def concatenate(arrays, axis=0, out=None, dtype=None, casting="same_kind"): + return concatenate_impl( + arrays, axis=axis, out=out, dtype=dtype, casting=casting + ) +else: + + @dispatched_function + def concatenate(arrays, /, axis=0, out=None, *, dtype=None, casting="same_kind"): + return concatenate_impl( + arrays, axis=axis, out=out, dtype=dtype, casting=casting + ) + + @apply_to_both def append(arr, values, axis=None): data, masks = _get_data_and_mask_arrays((arr, values)) @@ -923,7 +977,7 @@ def array_equiv(a1, a2): @dispatched_function -def where(condition, *args): +def where(condition, /, *args): from astropy.utils.masked import Masked, get_data_and_mask if not args: @@ -1184,7 +1238,7 @@ def from_data(cls, data, **options): return cls(_get_format_function(data, **options)) -def _array2string(a, options, separator=" ", prefix=""): +def _array2string_impl(a, options, separator=" ", prefix=""): # Mostly copied from numpy.core.arrayprint, except: # - The format function is wrapped in a mask-aware class; # - Arrays scalars are not cast as arrays. @@ -1222,25 +1276,24 @@ def _array2string(a, options, separator=" ", prefix=""): return lst -@dispatched_function -def array2string( +def _array2string_main( a, - max_line_width=None, - precision=None, - suppress_small=None, - separator=" ", - prefix="", - style=np._NoValue, - formatter=None, - threshold=None, - edgeitems=None, - sign=None, - floatmode=None, - suffix="", - *, - legacy=None, + *, # make most arguments keyword only to minimize the risk of a human mistake + max_line_width, + precision, + suppress_small, + separator, + prefix, + formatter, + threshold, + edgeitems, + sign, + floatmode, + suffix, + legacy, + style=np._NoValue, # deprecated, removed in numpy 2.4 ): - # Copied from numpy.core.arrayprint, but using _array2string above. + # Copied from numpy.core.arrayprint, but using _array2string_impl above. if NUMPY_LT_2_1: if NUMPY_LT_2_0: from numpy.core.arrayprint import _format_options @@ -1263,11 +1316,11 @@ def array2string( edgeitems, max_line_width, suppress_small, - None, - None, - sign, - formatter, - floatmode, + nanstr=None, + infstr=None, + sign=sign, + formatter=formatter, + floatmode=floatmode, ) options.update(overrides) @@ -1280,11 +1333,83 @@ def array2string( if a.size == 0: result = "[]" else: - result = _array2string(a, options, separator, prefix) + result = _array2string_impl(a, options, separator, prefix) return result, None, None +if not NUMPY_LT_2_4: + + @dispatched_function + def array2string( + a, + max_line_width=None, + precision=None, + suppress_small=None, + separator=" ", + prefix="", + formatter=None, + threshold=None, + edgeitems=None, + sign=None, + floatmode=None, + suffix="", + *, + legacy=None, + ): + return _array2string_main( + a, + max_line_width=max_line_width, + precision=precision, + suppress_small=suppress_small, + separator=separator, + prefix=prefix, + formatter=formatter, + threshold=threshold, + edgeitems=edgeitems, + sign=sign, + floatmode=floatmode, + suffix=suffix, + legacy=legacy, + ) +else: + + @dispatched_function + def array2string( + a, + max_line_width=None, + precision=None, + suppress_small=None, + separator=" ", + prefix="", + style=np._NoValue, # removed in numpy 2.4 + formatter=None, + threshold=None, + edgeitems=None, + sign=None, + floatmode=None, + suffix="", + *, + legacy=None, + ): + return _array2string_main( + a, + max_line_width=max_line_width, + precision=precision, + suppress_small=suppress_small, + separator=separator, + prefix=prefix, + style=style, + formatter=formatter, + threshold=threshold, + edgeitems=edgeitems, + sign=sign, + floatmode=floatmode, + suffix=suffix, + legacy=legacy, + ) + + def _array_str_scalar(x): # This wraps np.array_str for use as a format function in # MaskedFormat. We cannot use it directly as format functions diff --git a/astropy/utils/masked/tests/test_function_helpers.py b/astropy/utils/masked/tests/test_function_helpers.py index 86dce9770531..34b8ef8018ed 100644 --- a/astropy/utils/masked/tests/test_function_helpers.py +++ b/astropy/utils/masked/tests/test_function_helpers.py @@ -328,7 +328,7 @@ def test_empty_like(self): assert o.shape == (2, 3) assert isinstance(o, Masked) assert isinstance(o, np.ndarray) - o2 = np.empty_like(prototype=self.ma) + o2 = np.empty_like(self.ma) assert o2.shape == (2, 3) assert isinstance(o2, Masked) assert isinstance(o2, np.ndarray) @@ -1299,9 +1299,11 @@ def test_array2string(self): out2 = np.array2string(self.ma, separator=", ", formatter={"all": hex}) assert out2 == "[———, 0x1, 0x2]" # Also as positional argument (no, nobody will do this!) - out3 = np.array2string( - self.ma, None, None, None, ", ", "", np._NoValue, {"int": hex} - ) + if NUMPY_LT_2_4: + args = (self.ma, None, None, None, ", ", "", np._NoValue, {"int": hex}) + else: + args = (self.ma, None, None, None, ", ", "", {"int": hex}) + out3 = np.array2string(*args) assert out3 == out2 # But not if the formatter is not relevant for us. out4 = np.array2string(self.ma, separator=", ", formatter={"float": hex}) From fdf4d44ade0e262683dd6bd5be4b804af7c78ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 6 Nov 2025 17:47:05 +0100 Subject: [PATCH 005/199] Backport PR #18841: BUG: fix np.array's dispatcher for compatibility with numpy 2.4 --- .../units/quantity_helper/function_helpers.py | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/astropy/units/quantity_helper/function_helpers.py b/astropy/units/quantity_helper/function_helpers.py index ca3487e86cca..72ea3781f3ca 100644 --- a/astropy/units/quantity_helper/function_helpers.py +++ b/astropy/units/quantity_helper/function_helpers.py @@ -617,12 +617,37 @@ def require(a, dtype=None, requirements=None): return (a, dtype, requirements), {}, out_unit, None -@function_helper -def array(object, dtype=None, *, copy=True, order="K", subok=False, ndmin=0): +if not NUMPY_LT_2_4: + + @function_helper + def array( + object, dtype=None, *, copy=True, order="K", subok=False, ndmin=0, ndmax=0 + ): + return array_impl( + object, + dtype=dtype, + copy=copy, + order=order, + subok=subok, + ndmin=ndmin, + ndmax=ndmax, + ) +else: + + @function_helper + def array(object, dtype=None, *, copy=True, order="K", subok=False, ndmin=0): + return array_impl( + object, dtype=dtype, copy=copy, order=order, subok=subok, ndmin=ndmin + ) + + +def array_impl(object, *, dtype, copy, order, subok, ndmin, ndmax=0): out_unit = getattr(object, "unit", UNIT_FROM_LIKE_ARG) if out_unit is not UNIT_FROM_LIKE_ARG: object = _as_quantity(object).value kwargs = {"copy": copy, "order": order, "subok": subok, "ndmin": ndmin} + if not NUMPY_LT_2_4: + kwargs |= {"ndmax": ndmax} return (object, dtype), kwargs, out_unit, None From 19daf39859dc36e4839b1c72d68f6913c8cb426d Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 10 Nov 2025 12:51:31 +0000 Subject: [PATCH 006/199] Backport PR #18863: Updated list of contributors and .mailmap file --- .mailmap | 14 ++++++++++++++ docs/credits.rst | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/.mailmap b/.mailmap index f60bffd33dd4..d59c207a98ce 100644 --- a/.mailmap +++ b/.mailmap @@ -12,6 +12,7 @@ Aleh Khvalko Aleksi Suutarinen Alex Conley Alex Conley +Alex Fox <156043000+AlexFoxOSU@users.noreply.github.com> Alex Hagen Alex Rudy Alexander Bakanov @@ -123,6 +124,9 @@ E. Madison Bray E. Rykoff Emir Karamehmetoglu Emir Karamehmetoglu Emir +Evan Jones <60061381+E-W-Jones@users.noreply.github.com> +Everett Schlawin +Everett Schlawin Esteban Pardo Sánchez Francesco Montesano Gabriel Brammer Gabriel Brammer @@ -142,6 +146,7 @@ Hannes Breytenbach Hans Moritz Günther Hans Moritz Günther Harry Ferguson +Harshada Raut Henrik Norman Henrik Norman Henry Schreiner @@ -151,6 +156,7 @@ Humna Awan Igor Lemos Ivo Busko Ivo Busko +Jackson Hayward Jaime Andrés Jake VanderPlas Jake VanderPlas @@ -160,6 +166,7 @@ James McCormac James Tocknell James Tocknell James Turner +James Turner Jane Rigby Jani Šumak Jason Segnini <47617351+JasonS09@users.noreply.github.com> @@ -243,12 +250,18 @@ Marcello Nascif <118627858+marcellonascif@u Marten van Kerkwijk Marten van Kerkwijk Marten H. van Kerkwijk Marten van Kerkwijk Marten Henric van Kerkwijk +Marten van Kerkwijk +Marten van Kerkwijk +Marten van Kerkwijk Matt Davis Matteo Bachetti Matthew Craig Matthias Stein Matthieu Baumann +Matthieu Bec +Matthieu Bec Mavani Bhautik +Michael Belfrage <216956+mikez@users.noreply.github.com> Michael Brewer Michael Brewer Michael Hirsch @@ -337,6 +350,7 @@ Simon Liedtke Somia Floret Somia FLORET Somia Floret Somia Floret <57394764+somilia@users.noreply.github.com> Sourabh Cheedella +Stelios Voutsinas Steve Crawford Steve Crawford Steve Crawford diff --git a/docs/credits.rst b/docs/credits.rst index 3e16da1a6e2a..37cc5b7526b4 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -17,6 +17,7 @@ Core Package Contributors * Adele Plunkett * Aditya Sharma * Adrian Price-Whelan +* Adrien Thob * Akash Deshpande * Akeem * Akshat Dixit @@ -27,6 +28,7 @@ Core Package Contributors * Alex Conley * Alex de la Vega * Alex Drlica-Wagner +* Alex Fox * Alex Hagen * Alex Rudy * Alexander Bakanov @@ -51,11 +53,13 @@ Core Package Contributors * Anne Archibald * Antetokounpo * Anthony Horton +* Antonio Bento Pereira * Antony Lee * Arfon Smith * Arie Kurniawan * Arne de Laat * Arthur Eigenbrot +* Arthur Sardella * Arthur Xavier Joao Pedro Maia * Aryan Shukla * Asish Panda @@ -68,6 +72,7 @@ Core Package Contributors * Ben Greiner * Benjamin Alan Weaver * Benjamin Roulston +* Benjamin Scully * Benjamin Winkel * Bernardo Sulzbach * Bernie Simon @@ -92,6 +97,7 @@ Core Package Contributors * CaioCoutinhoP * Carl Osterwisch * Carl Schaffer +* Caspar van Leeuwen * Chiara Marmo * Chris Beaumont * Chris Hanley @@ -153,8 +159,10 @@ Core Package Contributors * Eero Vaher * Eli Bressert * Elijah Bernstein-Cooper +* Elise Chavez * Eloy Salinas * Emily Deibert +* Emily Hu * Emir Karamehmetoglu * Emma Hogan * Eric Depagne @@ -163,11 +171,14 @@ Core Package Contributors * Erik Tollerud * Erin Allard * Esteban Pardo Sánchez +* Evan Jones * Even Rouault +* Everett Schlawin * Evert Rol * Felipe Cybis Pereira * Felipe Gameleira * Felix Yan +* Finn Womack * fockez * Francesc Vilardell * Francesco Biscani @@ -196,6 +207,7 @@ Core Package Contributors * Hannes Breytenbach * Hans Moritz Günther * Harry Ferguson +* Harshada Raut * Heinz-Alexander Fuetterer * Helen Sherwood-Taylor * Hélvio Peixoto @@ -213,6 +225,7 @@ Core Package Contributors * Inada Naoki * J\. Goutin * J\. Xavier Prochaska +* Jackson Hayward * Jake VanderPlas * Jakob Maljaars * James Davies @@ -276,6 +289,7 @@ Core Package Contributors * Kevin Sooley * Kewei Li * Kieran Leschinski +* Kim Searle * Kirill Tchernyshyov * Kris Stern * Kristin Berry @@ -353,6 +367,7 @@ Core Package Contributors * Médéric Boquien * Megan Sosey * Melissa Weber Mendonça +* Michael Belfrage * Michael Brewer * Michael Droettboom * Michael Hirsch @@ -376,6 +391,7 @@ Core Package Contributors * Miruna Oprescu * Moataz Hisham * Mohan Agrawal +* Mohsin Mehmood * Molly Peeples * Mridul Seth * Mubin Manasia @@ -419,18 +435,22 @@ Core Package Contributors * Pauline Barmby * Perry Greenfield * Peter Cock +* Peter Scicluna * Peter Teuben * Peter Yoachim +* Pieter Eendebak * Piyush Sharma * Porter Averett * Prajwel Joseph * Prasanth Nair * Pratik Patel +* Preshanth Jagannathan * Pritish Chakraborty * Pushkar Kopparla * Rachel Guo * Raghuram Devarakonda * Ralf Gommers +* Raphael Erik Hviding * Rashid Khan * Rasmus Handberg * Ravi Kumar @@ -491,6 +511,7 @@ Core Package Contributors * Shilpi Jain * Shivan Sornarajah * Shivansh Mishra +* Shreeharsh Shinde * Shresth Verma * Shreyas Bapat * Sigurd Næss @@ -500,11 +521,13 @@ Core Package Contributors * Simon Liedtke * Simon Torres * Somia Floret +* Sonu Singh * Sourabh Cheedella * Srikrishna Sekhar * srirajshukla * Stefan Becker * Stefan Nelson +* Stelios Voutsinas * Stephen Bailey * Stephen Portillo * Steve Crawford From d49622f09e200ff11c63a1bba3beaf77b1cc0599 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 10 Nov 2025 13:26:32 +0000 Subject: [PATCH 007/199] Added contributor statistics and names --- docs/whatsnew/7.2.rst | 84 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/docs/whatsnew/7.2.rst b/docs/whatsnew/7.2.rst index 868264185949..dc313319ac92 100644 --- a/docs/whatsnew/7.2.rst +++ b/docs/whatsnew/7.2.rst @@ -18,21 +18,16 @@ In particular, this release includes: * :ref:`whatsnew-7.2-cosmology` * :ref:`whatsnew-7.2-fits-wcs-units` * :ref:`whatsnew-7.2-cls-concat-stack` -* ... In addition to these major changes, Astropy v7.2 includes a large number of smaller improvements and bug fixes, which are described in the :ref:`changelog`. By the numbers: -* X issues have been closed since v7.1 -* X pull requests have been merged since v7.1 -* X distinct people have contributed code - -Full change log -=============== - -To see a detailed list of all changes in version v7.2, including changes in -API, please see the :ref:`changelog`. +* 830 commits have been added since 7.1 +* 140 issues have been closed since 7.1 +* 310 pull requests have been merged since 7.1 +* 49 people have contributed since 7.1 +* 22 of which are new contributors .. _whatsnew-7.2-faster-ecsv-tab-rdr: @@ -167,8 +162,8 @@ astropy 9.0. Here the index identifier was the first element of the item, e.g., .. _whatsnew-7.2-cosmology: -Cosmology -========= +Cosmology traits +================ The :mod:`~astropy.cosmology.traits` module provides reusable components, called :term:`traits `, that encapsulate specific cosmological properties or @@ -325,3 +320,68 @@ that the existing functions allowed one to concatenate scalars together with one-dimensional arrays, while this is not allowed with `~numpy.concatenate`. Instead, like for arrays, one has to explicitly ensure arrays, e.g., by using ``np.concatenate(np.atleast_1d(coords))``. + +Full change log +=============== + +To see a detailed list of all changes in version v7.2, including changes in +API, please see the :ref:`changelog`. + +Contributors to the 7.2 release +=============================== + +The people who have contributed to the code for this release are: + +.. hlist:: + :columns: 4 + + - Adrien Thob * + - Albert Y. Shih + - Alex Fox * + - Antonio Bento Pereira * + - Benjamin Scully * + - Brigitta Sipőcz + - Caspar van Leeuwen * + - Chiara Marmo + - Clément Robert + - Eero Vaher + - Elise Chavez * + - Emily Hu * + - Evan Jones * + - Everett Schlawin * + - Finn Womack * + - Hans Moritz Günther + - Harshada Raut * + - Igor Lemos + - Jackson Hayward * + - James Turner + - Kim Searle * + - Larry Bradley + - Leo Singer + - Manon Marchand + - Marten van Kerkwijk + - Matthieu Bec + - Maximilian Linhoff + - Michael Belfrage * + - Mihai Cara + - Mohsin Mehmood * + - Nathaniel Starkman + - P\. L\. Lim + - Peter Scicluna * + - Pieter Eendebak * + - Preshanth Jagannathan * + - Raphael Erik Hviding * + - Sam Lee + - Shreeharsh Shinde * + - Simon Conseil + - Sonu Singh * + - Stelios Voutsinas * + - Stuart Mumford + - Thomas Robitaille + - Tiago Gomes + - Tim Jenness + - Tom Aldcroft + - Varun Nikam + - William Jamieson + +Where a * indicates that this release contains their first contribution to astropy. From 1b51cf1a33f51faf55b5f1f156ab0ee135465318 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Mon, 10 Nov 2025 14:29:51 -0500 Subject: [PATCH 008/199] Backport PR #18868: Enable inplace multiplications of `numpy` array and `Quantity` with any dimensionless and unscaled unit --- astropy/units/quantity_helper/converters.py | 2 +- astropy/units/tests/test_quantity_ufuncs.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/astropy/units/quantity_helper/converters.py b/astropy/units/quantity_helper/converters.py index 5c02ab022c22..56e4783e580d 100644 --- a/astropy/units/quantity_helper/converters.py +++ b/astropy/units/quantity_helper/converters.py @@ -375,7 +375,7 @@ def check_output(output, unit, inputs, function=None): else: # output is not a Quantity, so cannot obtain a unit. - if not (unit is None or unit is dimensionless_unscaled): + if not (unit is None or unit == dimensionless_unscaled): raise UnitTypeError( "Cannot store quantity with dimension " "{}in a non-Quantity instance.".format( diff --git a/astropy/units/tests/test_quantity_ufuncs.py b/astropy/units/tests/test_quantity_ufuncs.py index 17c25638299d..51846f8c225f 100644 --- a/astropy/units/tests/test_quantity_ufuncs.py +++ b/astropy/units/tests/test_quantity_ufuncs.py @@ -1112,6 +1112,14 @@ def test_ndarray_inplace_op_with_quantity(self): a[:2] += q # This used to fail assert_array_equal(a, np.array([0.125, 1.25, 2.0])) + def test_ndarray_inplace_op_with_dimensionless_quantity(self): + # Regression test for #18866 - multiplying a bare array inplace with + # a dimensionless Quantity required the unit to be u.dimensionless_unscaled. + # Mere equality was not good enough. + arr = np.ones((1,)) + arr *= 1 / np.cos(0 * u.deg) + assert arr[0] == 1 + class TestWhere: """Test the where argument in ufuncs.""" From 25e7a59ef2583502121f5ec623eabc9b87c35971 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Tue, 11 Nov 2025 16:47:10 -0500 Subject: [PATCH 009/199] Backport PR #18885: BUG: fix forward compatibility with numpy 2.4 (dev) in `np.array2string` helper function and tests --- astropy/units/quantity_helper/function_helpers.py | 13 ++++--------- astropy/units/tests/test_quantity_non_ufuncs.py | 6 ++---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/astropy/units/quantity_helper/function_helpers.py b/astropy/units/quantity_helper/function_helpers.py index ca3487e86cca..8583ab589251 100644 --- a/astropy/units/quantity_helper/function_helpers.py +++ b/astropy/units/quantity_helper/function_helpers.py @@ -1302,15 +1302,10 @@ def array2string(a, *args, **kwargs): # also work around this by passing on a formatter (as is done in Angle). # So, we do nothing if the formatter argument is present and has the # relevant formatter for our dtype. - import inspect - - sig = inspect.signature(np.array2string) - formatter_arg_pos = list(sig.parameters).index("formatter") - 1 - formatter = ( - args[formatter_arg_pos] - if len(args) >= formatter_arg_pos - else kwargs.get("formatter") - ) + if NUMPY_LT_2_4: + formatter = args[6] if len(args) >= 7 else kwargs.get("formatter") + else: + formatter = kwargs.get("formatter") if formatter is None: a = a.value diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index a6d8aae9efa5..af28930c81bc 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -2068,10 +2068,8 @@ def test_array2string(self): # Also as positional argument (no, nobody will do this!) if NUMPY_LT_2_4: args = (self.q, None, None, None, ", ", "", np._NoValue, {"float": str}) - else: - args = (self.q, None, None, None, ", ", "", {"float": str}) - out3 = np.array2string(*args) - assert out3 == expected2 + out3 = np.array2string(*args) + assert out3 == expected2 # But not if the formatter is not relevant for us. out4 = np.array2string(self.q, separator=", ", formatter={"int": str}) assert out4 == expected1 From 9fae3260722cdbbac85087210bae5786f93deb27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Tue, 11 Nov 2025 23:24:24 +0100 Subject: [PATCH 010/199] Backport PR #18849: BUG: fix forward compatibility for `np.arange(, ...)` against numpy 2.4 (dev) --- .../units/quantity_helper/function_helpers.py | 151 ++++++++++++------ .../units/tests/test_quantity_non_ufuncs.py | 39 +++-- 2 files changed, 118 insertions(+), 72 deletions(-) diff --git a/astropy/units/quantity_helper/function_helpers.py b/astropy/units/quantity_helper/function_helpers.py index c67689e4574d..29d958cfd5be 100644 --- a/astropy/units/quantity_helper/function_helpers.py +++ b/astropy/units/quantity_helper/function_helpers.py @@ -514,68 +514,115 @@ def _block(arrays, max_depth, result_ndim, depth=0): UNIT_FROM_LIKE_ARG = object() -if NUMPY_LT_2_0: - @function_helper - def arange(*args, start=None, stop=None, step=None, dtype=None): - return arange_impl(*args, start=start, stop=stop, step=step, dtype=dtype) -else: +if not NUMPY_LT_2_0: @function_helper - def arange(*args, start=None, stop=None, step=None, dtype=None, device=None): + def arange( + start_or_stop, + /, + stop=None, + step=1, + *, + dtype=None, + device=None, + ): return arange_impl( - *args, start=start, stop=stop, step=step, dtype=dtype, device=device + start_or_stop, stop=stop, step=step, dtype=dtype, device=device ) +else: -def arange_impl(*args, start=None, stop=None, step=None, dtype=None, **kwargs): - # NumPy is supposed to validate the input parameters before this dispatched - # function is reached. Nevertheless, we'll sprinkle a few rundundant - # sanity checks in the form of `assert` statements. - # As they are not part of the business logic, it is fine if they are - # compiled-away (e.g. the Python interpreter runs with -O) - assert len(args) <= 4 - - # bind positional arguments to their meaningful names - # following the (complex) logic of np.arange - match args: - case (pos1,): - assert stop is None or start is None - if stop is None: - stop = pos1 - elif start is None: - start = pos1 - case start, stop, *rest: - if start is not None and stop is None: - start, stop = stop, start - match rest: - # rebind step and dtype if possible - case (step,): - pass - case step, dtype: - pass - - # as the only required argument, we want stop to set the unit of the output - # so it's important that it comes first in the qty_kwargs - qty_kwargs = { - k: v - for k, v in (("stop", stop), ("start", start), ("step", step)) - if v is not None - } + @function_helper + def arange(start_or_stop, /, stop=None, step=1, *, dtype=None): + return arange_impl(start_or_stop, stop=stop, step=step, dtype=dtype) + + +def unwrap_arange_args(*, start_or_stop, stop_, step_): + # handle the perilous task of disentangling original arguments + # This isn't trivial because start_or_stop may actually bind to two + # different inputs, as the name suggests. + # We (ab)use structural pattern matching here to bind output variables + # (start, stop, step), so no additional logic is actually needed after + # a match is found. + match (start_or_stop, stop_, step_): + case (stop, None as start, step): + pass + case (start, stop, step): + pass + + # purely defensive programming + assert stop is not None, "Please report this." + return start, stop, step + + +def wrap_arange_args(*, start, stop, step, expected_out_unit): + # do the reverse operation than unwrap_arange_args + # this is needed because start_or_stop *must* be passed as positional + + # purely defensive programming + assert stop is not None, "Please report this." + + match start, stop: + case (None, _): + qty_args = (stop,) + case _: + qty_args = (start, stop) + + step_val = step.to_value(expected_out_unit) if hasattr(step, "unit") else step + + kwargs = {} if step == 1 else {"step": step_val} + + # reverse positional arguments so `stop` always comes first + # this is done to ensure that the arrays are first converted to the + # expected unit, which we guarantee should be stop's + args_rev, out_unit = _quantities2arrays(*qty_args[::-1]) + if expected_out_unit is not UNIT_FROM_LIKE_ARG: + assert out_unit == expected_out_unit + if hasattr(stop, "unit"): + assert out_unit == stop.unit + + # reverse args again to restore initial order + args = args_rev[::-1] + + if "step" in kwargs: + kwargs["step"] = step_val + return args, kwargs + + +def arange_impl(start_or_stop, /, *, stop, step, dtype, device=None): + # Because this wrapper requires exceptional amounts of additional logic + # to unwrap/wrap its complicated signature, we'll sprinkle a few + # sanity checks in the form of `assert` statements, which should help making + # heads or tails of what's happening in the event of an unexpected exception. + # + # also note that we intentionally choose to match numpy.arange's signature + # at typecheck time, as opposed to its actual runtime signature, which is + # even richer (as of numpy 2.4). For instance, this means we don't support + # `start` being passed as keyword, or `dtype` being passed as positional. + # This is done to improve the overall stability and maintainability of this + # complicated wrapper function. + start, stop, step = unwrap_arange_args( + start_or_stop=start_or_stop, stop_=stop, step_=step + ) out_unit = getattr(stop, "unit", UNIT_FROM_LIKE_ARG) - if out_unit is UNIT_FROM_LIKE_ARG: - if hasattr(start, "unit") or hasattr(step, "unit"): - raise TypeError( - "stop without a unit cannot be combined with start or step with a unit." - ) - kwargs.update(qty_kwargs) - else: - # Convert possible start, step to stop units. - new_values, _ = _quantities2arrays(*qty_kwargs.values()) - kwargs.update(zip(qty_kwargs.keys(), new_values)) + + if out_unit is UNIT_FROM_LIKE_ARG and ( + hasattr(start, "unit") or hasattr(step, "unit") + ): + raise TypeError( + "stop without a unit cannot be combined with start or step with a unit." + ) + + args, kwargs = wrap_arange_args( + start=start, stop=stop, step=step, expected_out_unit=out_unit + ) kwargs["dtype"] = dtype - return (), kwargs, out_unit, None + if not NUMPY_LT_2_0: + kwargs["device"] = device + + return args, kwargs, out_unit, None if NUMPY_LT_2_0: diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index af28930c81bc..b2ff1fd2de35 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -396,29 +396,33 @@ def test_astype(self): id="pos: start; kw: stop", ), pytest.param( - (10 * u.radian, None), - {}, - np.rad2deg(np.arange(10, dtype=float) * ARCSEC_PER_DEGREE), - id="pos: stop, followed by 1 None", + (10,), + {"step": 2}, + np.arange(10, step=2, dtype=float) * ARCSEC_PER_DEGREE, + id="pos: stop; kw: step; unit from like", ), pytest.param( - (10 * u.radian, None, None), + (10 * u.radian, None), {}, np.rad2deg(np.arange(10, dtype=float) * ARCSEC_PER_DEGREE), - id="pos: stop, followed by 2 None", + id="pos: stop, None", ), pytest.param( - (10 * u.radian, None, None, None), + (10 * u.radian, None, 1), {}, np.rad2deg(np.arange(10, dtype=float) * ARCSEC_PER_DEGREE), - id="pos: stop, followed by 3 None", + id="pos: stop, None, 1", ), ], ) def test_arange(self, args, kwargs, expected): arr = np.arange(*args, **kwargs, like=u.Quantity([], u.degree)) assert type(arr) is u.Quantity - assert arr.unit == u.radian + if any(hasattr(arg, "unit") for arg in itertools.chain(args, kwargs.keys())): + expected_unit = u.radian + else: + expected_unit = u.degree + assert arr.unit == expected_unit assert arr.dtype == expected.dtype assert_allclose(arr.to_value(u.arcsec), expected) @@ -434,13 +438,6 @@ class AngularUnits(u.SpecificTypeQuantity): assert arr.dtype == np.dtype(float) assert_array_equal(arr.value, np.arange(10)) - def test_arange_pos_dtype(self): - arr = np.arange(0 * u.s, 10 * u.s, 1 * u.s, int, like=u.Quantity([], u.radian)) - assert type(arr) is u.Quantity - assert arr.unit == u.s - assert arr.dtype == np.dtype(int) - assert_array_equal(arr.value, np.arange(10)) - def test_arange_default_unit(self): arr = np.arange(10, like=u.Quantity([], u.s)) assert type(arr) is u.Quantity @@ -455,10 +452,12 @@ def test_arange_invalid_inputs(self): def test_arange_unit_from_stop(self): Q = 1 * u.km - a = np.arange(start=1 * u.s, stop=10 * u.min, like=Q) - b = np.arange(stop=10 * u.min, start=1 * u.s, like=Q) - assert a.unit == u.min - assert b.unit == u.min + start = 1 * u.s + stop = 10 * u.min + a = np.arange(start, stop, like=Q) + b = np.arange(start, stop=stop, like=Q) + assert a.unit == stop.unit + assert b.unit == stop.unit assert_array_equal(a.value, b.value) From 066759e6648c37e851d75a02261bfc365fcacbb1 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Wed, 12 Nov 2025 07:23:30 -0500 Subject: [PATCH 011/199] Backport PR #18889: BUG: fix forward compatibility with numpy 2.4 (dev) in `np.array2string` helper function and tests (utils.masked) --- astropy/utils/masked/function_helpers.py | 2 +- astropy/utils/masked/tests/test_function_helpers.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/astropy/utils/masked/function_helpers.py b/astropy/utils/masked/function_helpers.py index 48ea494f6caf..c8aebdc3f833 100644 --- a/astropy/utils/masked/function_helpers.py +++ b/astropy/utils/masked/function_helpers.py @@ -1348,13 +1348,13 @@ def array2string( suppress_small=None, separator=" ", prefix="", + *, formatter=None, threshold=None, edgeitems=None, sign=None, floatmode=None, suffix="", - *, legacy=None, ): return _array2string_main( diff --git a/astropy/utils/masked/tests/test_function_helpers.py b/astropy/utils/masked/tests/test_function_helpers.py index 34b8ef8018ed..8021d69d37fd 100644 --- a/astropy/utils/masked/tests/test_function_helpers.py +++ b/astropy/utils/masked/tests/test_function_helpers.py @@ -1301,10 +1301,8 @@ def test_array2string(self): # Also as positional argument (no, nobody will do this!) if NUMPY_LT_2_4: args = (self.ma, None, None, None, ", ", "", np._NoValue, {"int": hex}) - else: - args = (self.ma, None, None, None, ", ", "", {"int": hex}) - out3 = np.array2string(*args) - assert out3 == out2 + out3 = np.array2string(*args) + assert out3 == out2 # But not if the formatter is not relevant for us. out4 = np.array2string(self.ma, separator=", ", formatter={"float": hex}) assert out4 == out1 From ebc5c0c13ccc15b81c756caf6d80b48f81b40fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Wed, 12 Nov 2025 12:35:31 +0100 Subject: [PATCH 012/199] Backport PR #18859: TST/RFC: avoid upcoming deprecation warnings from pytest 9.1 (dev) --- .../tests/test_skyoffset_transformations.py | 16 +++++++++------- .../coordinates/tests/test_spectral_quantity.py | 4 +++- astropy/modeling/tests/test_pickle.py | 4 ++-- astropy/nddata/tests/test_nddata.py | 4 +++- astropy/nddata/tests/test_nduncertainty.py | 8 +++++--- astropy/nddata/tests/test_utils.py | 2 +- astropy/time/tests/test_methods.py | 4 ++-- astropy/units/tests/test_logarithmic.py | 5 +++-- astropy/units/tests/test_quantity_non_ufuncs.py | 16 +++++++++------- .../utils/masked/tests/test_function_helpers.py | 16 +++++++++------- 10 files changed, 46 insertions(+), 33 deletions(-) diff --git a/astropy/coordinates/tests/test_skyoffset_transformations.py b/astropy/coordinates/tests/test_skyoffset_transformations.py index 39679f8aba02..3f8de42d966a 100644 --- a/astropy/coordinates/tests/test_skyoffset_transformations.py +++ b/astropy/coordinates/tests/test_skyoffset_transformations.py @@ -225,13 +225,15 @@ def test_skycoord_skyoffset_frame(): @pytest.mark.parametrize( "from_origin,to_origin", - combinations( - ( - ICRS(10.6847929 * u.deg, 41.2690650 * u.deg, M31_DISTANCE), - FK5(10.6847929 * u.deg, 41.2690650 * u.deg, M31_DISTANCE), - Galactic(121.1744050 * u.deg, -21.5729360 * u.deg, M31_DISTANCE), - ), - r=2, + list( + combinations( + ( + ICRS(10.6847929 * u.deg, 41.2690650 * u.deg, M31_DISTANCE), + FK5(10.6847929 * u.deg, 41.2690650 * u.deg, M31_DISTANCE), + Galactic(121.1744050 * u.deg, -21.5729360 * u.deg, M31_DISTANCE), + ), + r=2, + ) ), ) def test_m31_coord_transforms(from_origin, to_origin): diff --git a/astropy/coordinates/tests/test_spectral_quantity.py b/astropy/coordinates/tests/test_spectral_quantity.py index 5646faf14b92..feac18ad7c8a 100644 --- a/astropy/coordinates/tests/test_spectral_quantity.py +++ b/astropy/coordinates/tests/test_spectral_quantity.py @@ -33,7 +33,9 @@ def test_init_invalid(self, unit): ): SpectralQuantity(1 * unit) - @pytest.mark.parametrize(("unit1", "unit2"), zip(SPECTRAL_UNITS, SPECTRAL_UNITS)) + @pytest.mark.parametrize( + ("unit1", "unit2"), list(zip(SPECTRAL_UNITS, SPECTRAL_UNITS)) + ) def test_spectral_conversion(self, unit1, unit2): sq1 = SpectralQuantity(1 * unit1) sq2 = sq1.to(unit2) diff --git a/astropy/modeling/tests/test_pickle.py b/astropy/modeling/tests/test_pickle.py index d8dcea322417..cdc1a3c6bdfa 100644 --- a/astropy/modeling/tests/test_pickle.py +++ b/astropy/modeling/tests/test_pickle.py @@ -22,7 +22,7 @@ from astropy.modeling.math_functions import ArctanhUfunc from astropy.utils.compat.optional_deps import HAS_SCIPY -MATH_FUNCTIONS = (func for func in math_functions.__all__ if func != "ArctanhUfunc") +MATH_FUNCTIONS = [func for func in math_functions.__all__ if func != "ArctanhUfunc"] PROJ_TO_REMOVE = ( @@ -45,7 +45,7 @@ + [f"Sky2Pix_{code}" for code in projections.projcodes] ) -PROJECTIONS = (func for func in projections.__all__ if func not in PROJ_TO_REMOVE) +PROJECTIONS = [func for func in projections.__all__ if func not in PROJ_TO_REMOVE] OTHER_MODELS = [ mappings.Mapping((1, 0)), diff --git a/astropy/nddata/tests/test_nddata.py b/astropy/nddata/tests/test_nddata.py index 820be3fc8345..5dfa57f03ce4 100644 --- a/astropy/nddata/tests/test_nddata.py +++ b/astropy/nddata/tests/test_nddata.py @@ -640,7 +640,9 @@ def test_overriden_wcs(): @pytest.mark.parametrize( "mask, unit, propagate_uncertainties, operation_ignores_mask", - zip(collapse_masks, collapse_units, collapse_propagate, collapse_ignore_masked), + list( + zip(collapse_masks, collapse_units, collapse_propagate, collapse_ignore_masked) + ), ) def test_collapse(mask, unit, propagate_uncertainties, operation_ignores_mask): # unique set of combinations of each of the N-1 axes for an N-D cube: diff --git a/astropy/nddata/tests/test_nduncertainty.py b/astropy/nddata/tests/test_nduncertainty.py index 398b678d7cb2..0df66d54afe4 100644 --- a/astropy/nddata/tests/test_nduncertainty.py +++ b/astropy/nddata/tests/test_nduncertainty.py @@ -379,9 +379,11 @@ def test_self_conversion_via_variance_supported(UncertClass): @pytest.mark.parametrize( "UncertClass,to_variance_func", - zip( - uncertainty_types_with_conversion_support, - (lambda x: x**2, lambda x: x, lambda x: 1 / x), + list( + zip( + uncertainty_types_with_conversion_support, + (lambda x: x**2, lambda x: x, lambda x: 1 / x), + ) ), ) def test_conversion_to_from_variance_supported(UncertClass, to_variance_func): diff --git a/astropy/nddata/tests/test_utils.py b/astropy/nddata/tests/test_utils.py index bd3039b46106..b0a1d3319468 100644 --- a/astropy/nddata/tests/test_utils.py +++ b/astropy/nddata/tests/test_utils.py @@ -482,7 +482,7 @@ def test_add_array_equal_shape(): @pytest.mark.parametrize( - ("position", "subpixel_index"), zip(test_positions, test_position_indices) + ("position", "subpixel_index"), list(zip(test_positions, test_position_indices)) ) def test_subpixel_indices(position, subpixel_index): """ diff --git a/astropy/time/tests/test_methods.py b/astropy/time/tests/test_methods.py index 69bf39ef4403..f295a4978b96 100644 --- a/astropy/time/tests/test_methods.py +++ b/astropy/time/tests/test_methods.py @@ -570,7 +570,7 @@ def create_data(self, use_mask): self.t2 = self.__class__.t2[use_mask] self.jd = self.__class__.jd[use_mask] - @pytest.mark.parametrize("kw, func", itertools.product(kwargs, functions)) + @pytest.mark.parametrize("kw, func", list(itertools.product(kwargs, functions))) def test_argfuncs(self, kw, func, use_mask): """ Test that ``np.argfunc(jd, **kw)`` is the same as ``t0.argfunc(**kw)`` @@ -594,7 +594,7 @@ def test_argfuncs(self, kw, func, use_mask): assert t0v.shape == jdv.shape assert t1v.shape == jdv.shape - @pytest.mark.parametrize("kw, func", itertools.product(kwargs, functions)) + @pytest.mark.parametrize("kw, func", list(itertools.product(kwargs, functions))) def test_funcs(self, kw, func, use_mask): """ Test that ``np.func(jd, **kw)`` is the same as ``t1.func(**kw)`` where diff --git a/astropy/units/tests/test_logarithmic.py b/astropy/units/tests/test_logarithmic.py index 19b05f1ab599..21b73a506bed 100644 --- a/astropy/units/tests/test_logarithmic.py +++ b/astropy/units/tests/test_logarithmic.py @@ -36,7 +36,7 @@ def test_logarithmic_units(self): assert u.dex.to(u.mag) == -2.5 assert u.mag.to(u.dB) == -4 - @pytest.mark.parametrize("lu_unit, lu_cls", zip(lu_units, lu_subclasses)) + @pytest.mark.parametrize("lu_unit, lu_cls", list(zip(lu_units, lu_subclasses))) def test_callable_units(self, lu_unit, lu_cls): assert isinstance(lu_unit, u.UnitBase) assert callable(lu_unit) @@ -510,7 +510,8 @@ def test_hashable(): class TestLogQuantityCreation: @pytest.mark.parametrize( - "lq, lu", zip(lq_subclasses + [u.LogQuantity], lu_subclasses + [u.LogUnit]) + "lq, lu", + list(zip(lq_subclasses + [u.LogQuantity], lu_subclasses + [u.LogUnit])), ) def test_logarithmic_quantities(self, lq, lu): """Check logarithmic quantities are all set up correctly""" diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index b2ff1fd2de35..b7a128588e50 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -2870,14 +2870,16 @@ def test_testing_completeness(): class TestFunctionHelpersCompleteness: @pytest.mark.parametrize( "one, two", - itertools.combinations( - ( - SUBCLASS_SAFE_FUNCTIONS, - UNSUPPORTED_FUNCTIONS, - set(FUNCTION_HELPERS.keys()), - set(DISPATCHED_FUNCTIONS.keys()), + list( + itertools.combinations( + ( + SUBCLASS_SAFE_FUNCTIONS, + UNSUPPORTED_FUNCTIONS, + set(FUNCTION_HELPERS.keys()), + set(DISPATCHED_FUNCTIONS.keys()), + ), + 2, ), - 2, ), ) def test_no_duplicates(self, one, two): diff --git a/astropy/utils/masked/tests/test_function_helpers.py b/astropy/utils/masked/tests/test_function_helpers.py index 8021d69d37fd..acec5f225a5e 100644 --- a/astropy/utils/masked/tests/test_function_helpers.py +++ b/astropy/utils/masked/tests/test_function_helpers.py @@ -1772,14 +1772,16 @@ def test_testing_completeness(): class TestFunctionHelpersCompleteness: @pytest.mark.parametrize( "one, two", - itertools.combinations( - ( - MASKED_SAFE_FUNCTIONS, - UNSUPPORTED_FUNCTIONS, - set(APPLY_TO_BOTH_FUNCTIONS.keys()), - set(DISPATCHED_FUNCTIONS.keys()), + list( + itertools.combinations( + ( + MASKED_SAFE_FUNCTIONS, + UNSUPPORTED_FUNCTIONS, + set(APPLY_TO_BOTH_FUNCTIONS.keys()), + set(DISPATCHED_FUNCTIONS.keys()), + ), + 2, ), - 2, ), ) def test_no_duplicates(self, one, two): From 4d27cf7481161ebc327c88cb132cb643edfdaa77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 13 Nov 2025 14:28:33 +0100 Subject: [PATCH 013/199] Backport PR #18890: TST: use `--strict-markers` when running pytest with `--pyargs` --- .github/workflows/ci_cron_monthly.yml | 2 +- .github/workflows/ci_cron_weekly.yml | 2 +- .github/workflows/ci_workflows.yml | 2 +- .github/workflows/publish.yml | 2 +- tox.ini | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci_cron_monthly.yml b/.github/workflows/ci_cron_monthly.yml index 9a0c20445172..9945f17fb5e0 100644 --- a/.github/workflows/ci_cron_monthly.yml +++ b/.github/workflows/ci_cron_monthly.yml @@ -104,4 +104,4 @@ jobs: pip install -U --no-build-isolation pyerfa pip install -v --no-build-isolation -e .[test] pip list - python3 -m pytest --pyargs astropy -m "not hypothesis" + python3 -m pytest --strict-markers --pyargs astropy -m "not hypothesis" diff --git a/.github/workflows/ci_cron_weekly.yml b/.github/workflows/ci_cron_weekly.yml index c607c6b6d12a..61c59178ba94 100644 --- a/.github/workflows/ci_cron_weekly.yml +++ b/.github/workflows/ci_cron_weekly.yml @@ -185,4 +185,4 @@ jobs: pip install -U --no-build-isolation pyerfa ASTROPY_USE_SYSTEM_ALL=1 pip install -v --no-build-isolation -e .[test] pip list - python3 -m pytest --pyargs astropy -m "not hypothesis" + python3 -m pytest --strict-markers --pyargs astropy -m "not hypothesis" diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index 7195ed129484..c739978136bc 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -145,7 +145,7 @@ jobs: upload_to_pypi: false upload_to_anaconda: false test_extras: test - test_command: pytest -p no:warnings --astropy-header -m "not hypothesis" -k "not test_data_out_of_range and not test_set_locale and not TestQuantityTyping" --pyargs astropy + test_command: pytest -Wdefault --astropy-header -m "not hypothesis" -k "not test_data_out_of_range and not test_set_locale and not TestQuantityTyping" --strict-markers --pyargs astropy targets: | - cp311-manylinux_x86_64 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b9ee5bca8197..aad72a7d0f0a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -57,7 +57,7 @@ jobs: # currently fails, see https://github.com/astropy/astropy/issues/10409 # We also exclude test_set_locale as it sometimes relies on the correct locale # packages being installed, which it isn't always. - test_command: pytest -p no:warnings --astropy-header -m "not hypothesis" -k "not test_data_out_of_range and not test_set_locale and not TestQuantityTyping" --pyargs astropy + test_command: pytest -Wdefault --astropy-header -m "not hypothesis" -k "not test_data_out_of_range and not test_set_locale and not TestQuantityTyping" --strict-markers --pyargs astropy targets: | # Linux wheels - cp3*-manylinux_x86_64 diff --git a/tox.ini b/tox.ini index 869ebfa86a12..a795523bdbbb 100644 --- a/tox.ini +++ b/tox.ini @@ -116,10 +116,10 @@ dependency-groups = commands = {list_dependencies_command} - !cov-!double: pytest --pyargs astropy {toxinidir}/docs {env:MPLFLAGS} {posargs} - cov-!double: pytest --pyargs astropy {toxinidir}/docs {env:MPLFLAGS} --cov astropy --cov-config={toxinidir}/pyproject.toml --cov-report xml:{toxinidir}/coverage.xml {posargs} + !cov-!double: pytest --strict-markers --pyargs astropy {toxinidir}/docs {env:MPLFLAGS} {posargs} + cov-!double: pytest --strict-markers --pyargs astropy {toxinidir}/docs {env:MPLFLAGS} --cov astropy --cov-config={toxinidir}/pyproject.toml --cov-report xml:{toxinidir}/coverage.xml {posargs} - double: pytest --keep-duplicates --pyargs astropy {toxinidir}/docs astropy {toxinidir}/docs {env:MPLFLAGS} {posargs} + double: pytest --keep-duplicates --strict-markers --pyargs astropy {toxinidir}/docs astropy {toxinidir}/docs {env:MPLFLAGS} {posargs} pip_pre = devdeps: true From d72b91202933a6655e577699c7eb1eab81a5d373 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Thu, 13 Nov 2025 10:46:26 -0500 Subject: [PATCH 014/199] Backport PR #18899: TST: stop skipping signature checks for numpy functions missing a runtime signature (for sufficiently recent versions of numpy) --- astropy/units/tests/test_quantity_non_ufuncs.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index b7a128588e50..57d0dc596d47 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -2927,7 +2927,10 @@ def test_all_arguments_reexposed(self, target, helper): try: sig_target = inspect.signature(target) except ValueError: - pytest.skip("Non Python function cannot be inspected at runtime") + if NUMPY_LT_2_4: + pytest.skip("Non Python function cannot be inspected at runtime") + else: + raise params_target = sig_target.parameters sig_helper = inspect.signature(helper) @@ -2988,7 +2991,10 @@ def test_known_arguments(self, target, helper): try: sig_target = inspect.signature(target) except ValueError: - pytest.skip("Non Python function cannot be inspected at runtime") + if NUMPY_LT_2_4: + pytest.skip("Non Python function cannot be inspected at runtime") + else: + raise params_target = sig_target.parameters sig_helper = inspect.signature(helper) From 1b660054655d382d6225e81fb874ffc21babd2fa Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Thu, 13 Nov 2025 11:03:34 -0500 Subject: [PATCH 015/199] Backport PR #18903: TST: check for private keyword arguments without default values --- astropy/units/tests/test_quantity_non_ufuncs.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index b7a128588e50..7a98c39f9161 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -34,6 +34,7 @@ POSITIONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY POSITIONAL_OR_KEYWORD = inspect.Parameter.POSITIONAL_OR_KEYWORD +EMPTY_DEFAULT = inspect.Parameter.empty ARCSEC_PER_DEGREE = 60 * 60 ARCSEC_PER_RADIAN = ARCSEC_PER_DEGREE * np.rad2deg(1) @@ -3014,12 +3015,18 @@ def test_known_arguments(self, target, helper): self.get_param_group(params_helper, [KEYWORD_ONLY, POSITIONAL_OR_KEYWORD]) ) - # additional private keyword-only argument are allowed because - # they are only intended for testing purposes. + # additional private keyword-only argument are allowed, as long as they + # have default values. They are only intended for testing purposes. # For instance, quantile has such a parameter '_q_unit' - keyword_allowed_helper = { - name for name in keyword_allowed_helper if not name.startswith("_") + private_kwargs = { + name for name in keyword_allowed_helper if name.startswith("_") } + for pk in private_kwargs: + assert params_helper[pk].default is not EMPTY_DEFAULT, ( + f"private argument {pk} must provide a default value" + ) + + keyword_allowed_helper -= private_kwargs diff = keyword_allowed_helper - keyword_allowed_target assert not diff, ( From 879e683a3e330f8f57e1c99e7aeb60cc2885fcef Mon Sep 17 00:00:00 2001 From: Tiago Gomes Date: Mon, 10 Nov 2025 17:56:13 +0000 Subject: [PATCH 016/199] Backport PR #18818: Fix incorrect handling of masked integers when writing `Table` to FITS --- astropy/io/fits/convenience.py | 2 +- astropy/io/fits/tests/test_convenience.py | 15 +++++++++++++++ docs/changes/table/18818.bugfix.rst | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docs/changes/table/18818.bugfix.rst diff --git a/astropy/io/fits/convenience.py b/astropy/io/fits/convenience.py index 7e9dcdce3a17..3253ba9caa2f 100644 --- a/astropy/io/fits/convenience.py +++ b/astropy/io/fits/convenience.py @@ -553,7 +553,7 @@ def table_to_hdu(table, character_as_bytes=False, name=None): # Be careful that we do not set null for columns that were not masked! int_formats = ("B", "I", "J", "K") if ( - col.format in int_formats or col.format.p_format in int_formats + col.format.format in int_formats or col.format.p_format in int_formats ) and hasattr(table[col.name], "mask"): fill_value = tarray[col.name].fill_value col.null = fill_value.astype(int) diff --git a/astropy/io/fits/tests/test_convenience.py b/astropy/io/fits/tests/test_convenience.py index 52bd61d2384a..14c021dadea2 100644 --- a/astropy/io/fits/tests/test_convenience.py +++ b/astropy/io/fits/tests/test_convenience.py @@ -91,6 +91,21 @@ def test_masked_table_to_hdu(self): filename = self.temp("test_table_to_hdu.fits") hdu.writeto(filename, overwrite=True) + def test_masked_integer_arrays(self): + # Regression test for #18817 + testfile = self.temp("test_masked_integer_arrays.fits") + t_w = Table( + rows=[ + [[np.ma.masked, np.ma.masked]], + [[1, 2]], + [[1, np.ma.masked]], + ], + names=["a"], + ) + t_w.write(testfile, overwrite=True) + t_r = Table.read(testfile) + assert repr(t_w) == repr(t_r) + def test_table_non_stringifyable_unit_to_hdu(self): table = Table( [[1, 2, 3], ["a", "b", "c"], [2.3, 4.5, 6.7]], diff --git a/docs/changes/table/18818.bugfix.rst b/docs/changes/table/18818.bugfix.rst new file mode 100644 index 000000000000..545083fcea85 --- /dev/null +++ b/docs/changes/table/18818.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug when writing ``Table`` to FITS files, if the table contained masked arrays of integers. From f85f17f9c552041cc7cbd4b246b2a869afe7c500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 13 Nov 2025 19:08:01 +0100 Subject: [PATCH 017/199] Backport PR #18882: BUG: warn instead of raise when failing to read specific files in fitsdiff script --- astropy/io/fits/scripts/fitsdiff.py | 37 +++++++++++++--------- astropy/io/fits/tests/test_fitsdiff.py | 43 ++++++++++++++++++++++++-- docs/changes/io.fits/18882.bugfix.rst | 3 ++ 3 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 docs/changes/io.fits/18882.bugfix.rst diff --git a/astropy/io/fits/scripts/fitsdiff.py b/astropy/io/fits/scripts/fitsdiff.py index a5e77eabf3f9..e12f0c33fae3 100644 --- a/astropy/io/fits/scripts/fitsdiff.py +++ b/astropy/io/fits/scripts/fitsdiff.py @@ -400,20 +400,29 @@ def main(args=None): try: for a, b in files: # TODO: pass in any additional arguments here too - diff = fits.diff.FITSDiff( - a, - b, - ignore_hdus=opts.ignore_hdus, - ignore_keywords=opts.ignore_keywords, - ignore_comments=opts.ignore_comments, - ignore_fields=opts.ignore_fields, - numdiffs=opts.numdiffs, - rtol=opts.rtol, - atol=opts.atol, - ignore_blanks=opts.ignore_blanks, - ignore_blank_cards=opts.ignore_blank_cards, - ) - + try: + diff = fits.diff.FITSDiff( + a, + b, + ignore_hdus=opts.ignore_hdus, + ignore_keywords=opts.ignore_keywords, + ignore_comments=opts.ignore_comments, + ignore_fields=opts.ignore_fields, + numdiffs=opts.numdiffs, + rtol=opts.rtol, + atol=opts.atol, + ignore_blanks=opts.ignore_blanks, + ignore_blank_cards=opts.ignore_blank_cards, + ) + except OSError: + if not opts.quiet: + msg = f"Warning: failed to open {a}" + if b != a: + msg += f" (or {b})" + msg += ". Skipping." + print(msg, file=sys.stderr) + identical.append(None) + continue diff.report(fileobj=out_file) identical.append(diff.identical) diff --git a/astropy/io/fits/tests/test_fitsdiff.py b/astropy/io/fits/tests/test_fitsdiff.py index 1953e8495fe9..55ef702dc7c7 100644 --- a/astropy/io/fits/tests/test_fitsdiff.py +++ b/astropy/io/fits/tests/test_fitsdiff.py @@ -11,6 +11,7 @@ from astropy.io.fits.convenience import writeto from astropy.io.fits.hdu import PrimaryHDU, hdulist from astropy.io.fits.scripts import fitsdiff +from astropy.utils.compat.optional_deps import HAS_UNCOMPRESSPY from astropy.utils.misc import _NOT_OVERWRITING_MSG_MATCH from .conftest import FitsTestCase @@ -236,7 +237,6 @@ def test_quiet(self, capsys): assert out == "" assert err == "" - @pytest.mark.slow def test_path(self, capsys): os.mkdir(self.temp("sub/")) tmp_b = self.temp("sub/ascii.fits") @@ -256,7 +256,9 @@ def test_path(self, capsys): tmp_d = self.temp("sub/") assert fitsdiff.main(["-q", self.data_dir, tmp_d]) == 1 assert fitsdiff.main(["-q", tmp_d, self.data_dir]) == 1 - assert fitsdiff.main(["-q", self.data_dir, self.data_dir]) == 0 + + expected_retv = int(not HAS_UNCOMPRESSPY) + assert fitsdiff.main(["-q", self.data_dir, self.data_dir]) == expected_retv # no match tmp_c = self.data("arange.fits") @@ -273,6 +275,43 @@ def test_path(self, capsys): assert fitsdiff.main(["-q", tmp_f, self.data_dir]) == 0 assert fitsdiff.main(["-q", self.data_dir, tmp_f]) == 0 + @pytest.mark.filterwarnings("ignore:unclosed file:ResourceWarning") + def test_warning_unreadable_file(self, capsys, monkeypatch): + # simulate not having uncompresspy installed regardless of the actual state + monkeypatch.setattr(fits.file, "HAS_UNCOMPRESSPY", False) + + Zfile = self.data("lzw.fits.Z") + assert fitsdiff.main([Zfile, Zfile]) != 0 + out, err = capsys.readouterr() + assert out == "" + assert err == f"Warning: failed to open {Zfile}. Skipping.\n" + + os.mkdir(self.temp("sub/")) + tmp = self.temp("sub/ascii.fits") + tmp_h = self.data("group.fits") + with hdulist.fitsopen(tmp_h) as hdu: + hdu.writeto(tmp) + + assert fitsdiff.main([Zfile, tmp]) != 0 + out, err = capsys.readouterr() + assert out == "" + assert err == f"Warning: failed to open {Zfile} (or {tmp}). Skipping.\n" + + assert fitsdiff.main(["-q", Zfile, tmp]) != 0 + out, err = capsys.readouterr() + assert out == "" + assert err == "" + + assert fitsdiff.main([tmp, Zfile]) != 0 + out, err = capsys.readouterr() + assert out == "" + assert err == f"Warning: failed to open {tmp} (or {Zfile}). Skipping.\n" + + assert fitsdiff.main(["-q", tmp, Zfile]) != 0 + out, err = capsys.readouterr() + assert out == "" + assert err == "" + def test_ignore_hdus(self): a = np.arange(100).reshape(10, 10) b = a.copy() + 1 diff --git a/docs/changes/io.fits/18882.bugfix.rst b/docs/changes/io.fits/18882.bugfix.rst new file mode 100644 index 000000000000..c457f82abbc3 --- /dev/null +++ b/docs/changes/io.fits/18882.bugfix.rst @@ -0,0 +1,3 @@ +Fixed a bug in ``fitsdiff`` script where failing to read a single file could +crash the entire program. A warning is now printed instead, and such files +are simply ignored. From 3e25a7ea9e4cc7b052c715d32f812c38a2866956 Mon Sep 17 00:00:00 2001 From: Tom Aldcroft Date: Fri, 14 Nov 2025 13:57:11 -0500 Subject: [PATCH 018/199] Backport PR #18911: Fix Table class methods `from_pandas/from_df()` to return correct class --- astropy/table/_dataframes.py | 23 +++++++++++++---------- astropy/table/table.py | 4 ++-- astropy/table/tests/test_df.py | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/astropy/table/_dataframes.py b/astropy/table/_dataframes.py index 71ed8e7b1abc..15eeb1b784cc 100644 --- a/astropy/table/_dataframes.py +++ b/astropy/table/_dataframes.py @@ -247,11 +247,13 @@ def to_df( def from_df( - df: Any, *, index: bool = False, units: Mapping[str, UnitLike] | None = None + cls: type[Table], + df: Any, + *, + index: bool = False, + units: Mapping[str, UnitLike] | None = None, ) -> Table: - """Create a Table from any narwhals-compatible DataFrame.""" - from .table import Table - + """Create an instance of ``cls`` from any narwhals-compatible DataFrame.""" if not HAS_NARWHALS: raise ModuleNotFoundError( "The narwhals library is required for the generic from_df method. " @@ -357,7 +359,7 @@ def from_df( else: out[name] = Column(data=data, unit=unit, copy=False) - return Table(out) + return cls(out) def to_pandas( @@ -432,11 +434,12 @@ def to_pandas( def from_pandas( - dataframe: Any, index: bool = False, units: Mapping[str, UnitLike] | None = None + cls: type[Table], + dataframe: Any, + index: bool = False, + units: Mapping[str, UnitLike] | None = None, ) -> Table: - """Create a Table from a pandas DataFrame.""" - from .table import Table - + """Create an instance of ``cls`` from a pandas DataFrame.""" out = {} names = list(dataframe.columns) @@ -514,4 +517,4 @@ def from_pandas( else: out[name] = Column(data=data, name=name, unit=unit) - return Table(out) + return cls(out) diff --git a/astropy/table/table.py b/astropy/table/table.py index e08b790d23ea..540ba48ec62f 100644 --- a/astropy/table/table.py +++ b/astropy/table/table.py @@ -4192,7 +4192,7 @@ def from_df( """ from ._dataframes import from_df - return from_df(df, index=index, units=units) + return from_df(cls, df, index=index, units=units) def to_pandas( self, index: bool | str | None = None, use_nullable_int: bool = True @@ -4333,7 +4333,7 @@ def from_pandas( """ from ._dataframes import from_pandas - return from_pandas(dataframe, index=index, units=units) + return from_pandas(cls, dataframe, index=index, units=units) info = TableInfo() diff --git a/astropy/table/tests/test_df.py b/astropy/table/tests/test_df.py index 7d8d92c062bc..197af4623673 100644 --- a/astropy/table/tests/test_df.py +++ b/astropy/table/tests/test_df.py @@ -665,3 +665,18 @@ def test_masked_int_data(self, backend, use_legacy_pandas_api): assert val == 2 assert nulls_first_two.all() + + +@pytest.mark.skipif( + not HAS_PANDAS or not HAS_NARWHALS, + reason="requires pandas and narwhals", +) +@pytest.mark.parametrize("method", ["from_df", "from_pandas"]) +def test_from_pandas_df_with_qtable(method): + """Test fix for QTable.from_pandas / from_df returns Table not QTable #18909""" + t = table.QTable() + t["a"] = [1, 2] + t["q"] = [3.0, 4.0] + df = t.to_pandas() + qt = getattr(table.QTable, method)(df) + assert isinstance(qt, table.QTable) From 98e15b9f5269570e50d79c5ff55c1881fd0adff2 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Mon, 17 Nov 2025 12:16:57 -0500 Subject: [PATCH 019/199] Backport PR #18323: Avoid stringified type annotations in `stats` --- astropy/stats/bayesian_blocks.py | 246 +++++++++++++++---------------- astropy/stats/info_theory.py | 2 - astropy/stats/setup_package.py | 2 - 3 files changed, 122 insertions(+), 128 deletions(-) diff --git a/astropy/stats/bayesian_blocks.py b/astropy/stats/bayesian_blocks.py index e94793570431..3b932aa31f67 100644 --- a/astropy/stats/bayesian_blocks.py +++ b/astropy/stats/bayesian_blocks.py @@ -45,8 +45,6 @@ https://www.tandfonline.com/doi/abs/10.1080/01621459.1969.10501038 """ -from __future__ import annotations - import warnings from collections.abc import KeysView from inspect import signature @@ -64,128 +62,6 @@ __all__ = ["Events", "FitnessFunc", "PointMeasures", "RegularEvents", "bayesian_blocks"] -def bayesian_blocks( - t: ArrayLike, - x: ArrayLike | None = None, - sigma: ArrayLike | float | None = None, - fitness: Literal["events", "regular_events", "measures"] | FitnessFunc = "events", - **kwargs, -) -> NDArray[float]: - r"""Compute optimal segmentation of data with Scargle's Bayesian Blocks. - - This is a flexible implementation of the Bayesian Blocks algorithm - described in Scargle 2013 [1]_. - - Parameters - ---------- - t : array-like - data times (one dimensional, length N) - x : array-like, optional - data values - sigma : array-like or float, optional - data errors - fitness : str or object - the fitness function to use for the model. - If a string, the following options are supported: - - - 'events' : binned or unbinned event data. Arguments are ``gamma``, - which gives the slope of the prior on the number of bins, or - ``ncp_prior``, which is :math:`-\ln({\tt gamma})`. - - 'regular_events' : non-overlapping events measured at multiples of a - fundamental tick rate, ``dt``, which must be specified as an - additional argument. Extra arguments are ``p0``, which gives the - false alarm probability to compute the prior, or ``gamma``, which - gives the slope of the prior on the number of bins, or ``ncp_prior``, - which is :math:`-\ln({\tt gamma})`. - - 'measures' : fitness for a measured sequence with Gaussian errors. - Extra arguments are ``p0``, which gives the false alarm probability - to compute the prior, or ``gamma``, which gives the slope of the - prior on the number of bins, or ``ncp_prior``, which is - :math:`-\ln({\tt gamma})`. - - In all three cases, if more than one of ``p0``, ``gamma``, and - ``ncp_prior`` is chosen, ``ncp_prior`` takes precedence over ``gamma`` - which takes precedence over ``p0``. - - Alternatively, the fitness parameter can be an instance of - :class:`FitnessFunc` or a subclass thereof. - - **kwargs : - any additional keyword arguments will be passed to the specified - :class:`FitnessFunc` derived class. - - Returns - ------- - edges : ndarray - array containing the (N+1) edges defining the N bins - - Examples - -------- - .. testsetup:: - - >>> np.random.seed(12345) - - Event data: - - >>> t = np.random.normal(size=100) - >>> edges = bayesian_blocks(t, fitness='events', p0=0.01) - - Event data with repeats: - - >>> t = np.random.normal(size=100) - >>> t[80:] = t[:20] - >>> edges = bayesian_blocks(t, fitness='events', p0=0.01) - - Regular event data: - - >>> dt = 0.05 - >>> t = dt * np.arange(1000) - >>> x = np.zeros(len(t)) - >>> x[np.random.randint(0, len(t), len(t) // 10)] = 1 - >>> edges = bayesian_blocks(t, x, fitness='regular_events', dt=dt) - - Measured point data with errors: - - >>> t = 100 * np.random.random(100) - >>> x = np.exp(-0.5 * (t - 50) ** 2) - >>> sigma = 0.1 - >>> x_obs = np.random.normal(x, sigma) - >>> edges = bayesian_blocks(t, x_obs, sigma, fitness='measures') - - References - ---------- - .. [1] Scargle, J et al. (2013) - https://ui.adsabs.harvard.edu/abs/2013ApJ...764..167S - - .. [2] Bellman, R.E., Dreyfus, S.E., 1962. Applied Dynamic - Programming. Princeton University Press, Princeton. - https://press.princeton.edu/books/hardcover/9780691651873/applied-dynamic-programming - - .. [3] Bellman, R., Roth, R., 1969. Curve fitting by segmented - straight lines. J. Amer. Statist. Assoc. 64, 1079–1084. - https://www.tandfonline.com/doi/abs/10.1080/01621459.1969.10501038 - - See Also - -------- - astropy.stats.histogram : compute a histogram using bayesian blocks - """ - FITNESS_DICT = { - "events": Events, - "regular_events": RegularEvents, - "measures": PointMeasures, - } - fitness = FITNESS_DICT.get(fitness, fitness) - - if type(fitness) is type and issubclass(fitness, FitnessFunc): - fitfunc = fitness(**kwargs) - elif isinstance(fitness, FitnessFunc): - fitfunc = fitness - else: - raise ValueError("fitness parameter not understood") - - return fitfunc.fit(t, x, sigma) - - class FitnessFunc: """Base class for bayesian blocks fitness functions. @@ -602,3 +478,125 @@ def validate_input( if x is None: raise ValueError("x must be specified for point measures") return super().validate_input(t, x, sigma) + + +def bayesian_blocks( + t: ArrayLike, + x: ArrayLike | None = None, + sigma: ArrayLike | float | None = None, + fitness: Literal["events", "regular_events", "measures"] | FitnessFunc = "events", + **kwargs, +) -> NDArray[float]: + r"""Compute optimal segmentation of data with Scargle's Bayesian Blocks. + + This is a flexible implementation of the Bayesian Blocks algorithm + described in Scargle 2013 [1]_. + + Parameters + ---------- + t : array-like + data times (one dimensional, length N) + x : array-like, optional + data values + sigma : array-like or float, optional + data errors + fitness : str or object + the fitness function to use for the model. + If a string, the following options are supported: + + - 'events' : binned or unbinned event data. Arguments are ``gamma``, + which gives the slope of the prior on the number of bins, or + ``ncp_prior``, which is :math:`-\ln({\tt gamma})`. + - 'regular_events' : non-overlapping events measured at multiples of a + fundamental tick rate, ``dt``, which must be specified as an + additional argument. Extra arguments are ``p0``, which gives the + false alarm probability to compute the prior, or ``gamma``, which + gives the slope of the prior on the number of bins, or ``ncp_prior``, + which is :math:`-\ln({\tt gamma})`. + - 'measures' : fitness for a measured sequence with Gaussian errors. + Extra arguments are ``p0``, which gives the false alarm probability + to compute the prior, or ``gamma``, which gives the slope of the + prior on the number of bins, or ``ncp_prior``, which is + :math:`-\ln({\tt gamma})`. + + In all three cases, if more than one of ``p0``, ``gamma``, and + ``ncp_prior`` is chosen, ``ncp_prior`` takes precedence over ``gamma`` + which takes precedence over ``p0``. + + Alternatively, the fitness parameter can be an instance of + :class:`FitnessFunc` or a subclass thereof. + + **kwargs : + any additional keyword arguments will be passed to the specified + :class:`FitnessFunc` derived class. + + Returns + ------- + edges : ndarray + array containing the (N+1) edges defining the N bins + + Examples + -------- + .. testsetup:: + + >>> np.random.seed(12345) + + Event data: + + >>> t = np.random.normal(size=100) + >>> edges = bayesian_blocks(t, fitness='events', p0=0.01) + + Event data with repeats: + + >>> t = np.random.normal(size=100) + >>> t[80:] = t[:20] + >>> edges = bayesian_blocks(t, fitness='events', p0=0.01) + + Regular event data: + + >>> dt = 0.05 + >>> t = dt * np.arange(1000) + >>> x = np.zeros(len(t)) + >>> x[np.random.randint(0, len(t), len(t) // 10)] = 1 + >>> edges = bayesian_blocks(t, x, fitness='regular_events', dt=dt) + + Measured point data with errors: + + >>> t = 100 * np.random.random(100) + >>> x = np.exp(-0.5 * (t - 50) ** 2) + >>> sigma = 0.1 + >>> x_obs = np.random.normal(x, sigma) + >>> edges = bayesian_blocks(t, x_obs, sigma, fitness='measures') + + References + ---------- + .. [1] Scargle, J et al. (2013) + https://ui.adsabs.harvard.edu/abs/2013ApJ...764..167S + + .. [2] Bellman, R.E., Dreyfus, S.E., 1962. Applied Dynamic + Programming. Princeton University Press, Princeton. + https://press.princeton.edu/books/hardcover/9780691651873/applied-dynamic-programming + + .. [3] Bellman, R., Roth, R., 1969. Curve fitting by segmented + straight lines. J. Amer. Statist. Assoc. 64, 1079–1084. + https://www.tandfonline.com/doi/abs/10.1080/01621459.1969.10501038 + + See Also + -------- + astropy.stats.histogram : compute a histogram using bayesian blocks + """ + FITNESS_DICT = { + "events": Events, + "regular_events": RegularEvents, + "measures": PointMeasures, + } + fitness = FITNESS_DICT.get(fitness, fitness) + + if type(fitness) is type and issubclass(fitness, FitnessFunc): + fitfunc = fitness(**kwargs) + elif isinstance(fitness, FitnessFunc): + fitfunc = fitness + else: + raise ValueError("fitness parameter not understood") + + return fitfunc.fit(t, x, sigma) diff --git a/astropy/stats/info_theory.py b/astropy/stats/info_theory.py index 7b2d1e02da0c..99b5b5fb5907 100644 --- a/astropy/stats/info_theory.py +++ b/astropy/stats/info_theory.py @@ -4,8 +4,6 @@ This module contains simple functions for model selection. """ -from __future__ import annotations - import numpy as np __all__ = [ diff --git a/astropy/stats/setup_package.py b/astropy/stats/setup_package.py index d2c4c7140f29..d2739cbbdd09 100644 --- a/astropy/stats/setup_package.py +++ b/astropy/stats/setup_package.py @@ -1,7 +1,5 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst -from __future__ import annotations - import os from numpy import get_include as get_numpy_include From 5d4f3661f01fc078aeb21e142f42c82f611f2302 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Mon, 17 Nov 2025 14:18:31 -0500 Subject: [PATCH 020/199] Backport PR #18958: Fix units issue in modeling.tabular --- astropy/modeling/tabular.py | 2 +- astropy/modeling/tests/test_models.py | 11 +++++++++++ docs/changes/modeling/18958.bugfix.rst | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 docs/changes/modeling/18958.bugfix.rst diff --git a/astropy/modeling/tabular.py b/astropy/modeling/tabular.py index 7203ee142cdd..15dce2aa72dc 100644 --- a/astropy/modeling/tabular.py +++ b/astropy/modeling/tabular.py @@ -247,7 +247,7 @@ def evaluate(self, *inputs): # return_units not respected when points has no units if isinstance(self.lookup_table, u.Quantity) and not isinstance( - self.points[0], u.Quantity + result, u.Quantity ): result = result * self.lookup_table.unit diff --git a/astropy/modeling/tests/test_models.py b/astropy/modeling/tests/test_models.py index fc401f1b3e5b..40567f829c13 100644 --- a/astropy/modeling/tests/test_models.py +++ b/astropy/modeling/tests/test_models.py @@ -729,6 +729,17 @@ def test_tabular_interp_1d(): assert_quantity_allclose(model(xnew.to(u.nm)), ans1) assert model.bounding_box == (0 * u.nm, 4 * u.nm) + # Test with no units on points. + model = LookupTable(points=points, lookup_table=values * u.nJy) + assert_quantity_allclose(model(xnew), ans1) + assert_quantity_allclose(model(xnew.to(u.nm)), ans1) + assert model.bounding_box == (0, 4) + + model = LookupTable(points=points, lookup_table=values * u.nJy, method="nearest") + assert_quantity_allclose(model(xnew), np.array([1, 10, 10, 2, -3]) * u.nJy) + assert_quantity_allclose(model(xnew.to(u.nm)), np.array([1, 10, 10, 2, -3]) * u.nJy) + assert model.bounding_box == (0, 4) + # Test fill value unit conversion and unitless input on table with unit model = LookupTable( [1, 2, 3], diff --git a/docs/changes/modeling/18958.bugfix.rst b/docs/changes/modeling/18958.bugfix.rst new file mode 100644 index 000000000000..babcd756541b --- /dev/null +++ b/docs/changes/modeling/18958.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug in ``modeling.tabular`` models when the ``lookup_table`` is a Quantity, where the result might lose its unit in some cases. From cc00f29e121c11ad51ddab2afe79f223b55905e7 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Tue, 18 Nov 2025 15:26:45 -0500 Subject: [PATCH 021/199] Backport PR #18953: TST: make an explicit escape path to skip the single known case of a missing runtime signature in numpy 2.4 (np.fromstring) --- astropy/units/tests/test_quantity_non_ufuncs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index 3f116aed190e..785663b843ab 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -2930,6 +2930,8 @@ def test_all_arguments_reexposed(self, target, helper): except ValueError: if NUMPY_LT_2_4: pytest.skip("Non Python function cannot be inspected at runtime") + elif target is np.fromstring: + pytest.skip(f"known case of missing runtime signature ({target})") else: raise @@ -2994,6 +2996,8 @@ def test_known_arguments(self, target, helper): except ValueError: if NUMPY_LT_2_4: pytest.skip("Non Python function cannot be inspected at runtime") + elif target is np.fromstring: + pytest.skip(f"known case of missing runtime signature ({target})") else: raise From bbe48c4641f636a4a89b70da992ac62516987b9e Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:44:11 -0500 Subject: [PATCH 022/199] Backport PR #18873: doc(table): follow numpydoc in ColumnECSV & ECSVHeader --- astropy/io/misc/ecsv.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/astropy/io/misc/ecsv.py b/astropy/io/misc/ecsv.py index ea98f8e84dbd..efca71b7ab9b 100644 --- a/astropy/io/misc/ecsv.py +++ b/astropy/io/misc/ecsv.py @@ -116,7 +116,7 @@ class ColumnECSV: """ Class representing attributes of a column in an ECSV header. - Attributes + Parameters ---------- name : str The name of the column. @@ -133,7 +133,7 @@ class ColumnECSV: meta : dict or None, optional Additional metadata associated with the column. - Properties + Attributes ---------- csv_np_type : str Numpy type string describing the column CSV data. In practice this is the same @@ -141,7 +141,7 @@ class ColumnECSV: engine ``convert_np_type()`` method to generate the engine-specific type provided to the CSV reader. For instance, for pandas the ``int32`` type gets converted to ``Int32`` to read columns as a nullable int32. - dtype : np.dtype + dtype : `numpy.dtype` Numpy dtype in the final column data. This may be entirely different from ``csv_np_type`` in some cases, in particular JSON-encoded fields. shape : tuple of int @@ -182,7 +182,7 @@ class ECSVHeader: """ Class representing the information in an ECSV header. - Attributes + Parameters ---------- n_header : int Total number of header lines in the ECSV file (including empty). @@ -217,7 +217,7 @@ class ECSVEngine(metaclass=abc.ABCMeta): - `engines` is a base class-level dictionary that maps engine names to their respective engine classes. Subclasses should not modify this directly. - Properties + Parameters ---------- name : str Name of the engine, used for ``engine`` parameter in a call like: From a76e7aa1bfa927fffb77580a89065b7509e1fb0d Mon Sep 17 00:00:00 2001 From: Nathaniel Starkman Date: Fri, 14 Nov 2025 16:06:36 -0500 Subject: [PATCH 023/199] Backport PR #18869: types(cosmo): fix trait annotations --- astropy/cosmology/_src/traits/baryons.py | 3 ++- astropy/cosmology/_src/traits/darkenergy.py | 2 +- astropy/cosmology/_src/traits/darkmatter.py | 3 ++- astropy/cosmology/_src/traits/matterdensity.py | 3 ++- astropy/cosmology/_src/traits/photoncomponent.py | 3 ++- astropy/cosmology/_src/traits/totalcomponent.py | 3 ++- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/astropy/cosmology/_src/traits/baryons.py b/astropy/cosmology/_src/traits/baryons.py index 317023f9de40..11f553b4f25f 100644 --- a/astropy/cosmology/_src/traits/baryons.py +++ b/astropy/cosmology/_src/traits/baryons.py @@ -6,6 +6,7 @@ from collections.abc import Callable from typing import Any +import numpy as np from numpy.typing import ArrayLike, NDArray from astropy.cosmology._src.typing import FArray @@ -16,7 +17,7 @@ class BaryonComponent: """The cosmology has attributes and methods for the baryon density.""" - Ob0: float + Ob0: float | np.floating """Omega baryons: density of baryonic matter in units of the critical density at z=0.""" inv_efunc: Callable[[NDArray[Any]], NDArray[Any]] diff --git a/astropy/cosmology/_src/traits/darkenergy.py b/astropy/cosmology/_src/traits/darkenergy.py index e83a87eb67f5..67739c5ba6a9 100644 --- a/astropy/cosmology/_src/traits/darkenergy.py +++ b/astropy/cosmology/_src/traits/darkenergy.py @@ -12,7 +12,7 @@ class DarkEnergyComponent: # Subclasses should use `Parameter` to make this a parameter of the cosmology. - Ode0: float + Ode0: float | np.floating """Omega dark energy; dark energy density/critical density at z=0.""" @abstractmethod diff --git a/astropy/cosmology/_src/traits/darkmatter.py b/astropy/cosmology/_src/traits/darkmatter.py index 78a38f48aaf0..ac1e23b66435 100644 --- a/astropy/cosmology/_src/traits/darkmatter.py +++ b/astropy/cosmology/_src/traits/darkmatter.py @@ -3,6 +3,7 @@ from collections.abc import Callable from typing import Any +import numpy as np from numpy.typing import ArrayLike, NDArray from astropy.cosmology._src.typing import FArray @@ -17,7 +18,7 @@ class DarkMatterComponent: density parameter (i.e., total matter minus baryons) at redshift ``z``. """ - Odm0: float + Odm0: float | np.floating """Omega dark matter: dark matter density/critical density at z=0.""" inv_efunc: Callable[[NDArray[Any]], NDArray[Any]] diff --git a/astropy/cosmology/_src/traits/matterdensity.py b/astropy/cosmology/_src/traits/matterdensity.py index 3cb2a89fce34..68adeedc56e5 100644 --- a/astropy/cosmology/_src/traits/matterdensity.py +++ b/astropy/cosmology/_src/traits/matterdensity.py @@ -1,5 +1,6 @@ """Matter component.""" +import numpy as np from numpy.typing import ArrayLike from astropy.cosmology._src.typing import FArray @@ -10,7 +11,7 @@ class MatterComponent: - Om0: Quantity + Om0: float | np.floating """Omega matter; matter density/critical density at z=0.""" @deprecated_keywords("z", since="7.0") diff --git a/astropy/cosmology/_src/traits/photoncomponent.py b/astropy/cosmology/_src/traits/photoncomponent.py index 738f35554a65..c838d3fc66aa 100644 --- a/astropy/cosmology/_src/traits/photoncomponent.py +++ b/astropy/cosmology/_src/traits/photoncomponent.py @@ -6,6 +6,7 @@ from collections.abc import Callable from typing import Any +import numpy as np from numpy.typing import ArrayLike, NDArray from astropy.cosmology._src.typing import FArray @@ -16,7 +17,7 @@ class PhotonComponent: """The cosmology has attributes and methods for the photon density.""" - Ogamma0: float + Ogamma0: float | np.floating """Omega gamma; the density/critical density of photons at z=0.""" inv_efunc: Callable[[NDArray[Any]], NDArray[Any]] diff --git a/astropy/cosmology/_src/traits/totalcomponent.py b/astropy/cosmology/_src/traits/totalcomponent.py index cf4ef77b9298..402bafa42130 100644 --- a/astropy/cosmology/_src/traits/totalcomponent.py +++ b/astropy/cosmology/_src/traits/totalcomponent.py @@ -4,6 +4,7 @@ from abc import abstractmethod +import numpy as np from numpy.typing import ArrayLike from astropy.cosmology._src.typing import FArray @@ -19,7 +20,7 @@ class TotalComponent: @property @abstractmethod - def Otot0(self) -> float: + def Otot0(self) -> float | np.floating: """Omega total; the total density/critical density at z=0.""" raise NotImplementedError # pragma: no cover From f191b2608816189072f4c8cbc9dc44ab626a7fac Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 25 Nov 2025 14:14:18 +0000 Subject: [PATCH 024/199] Finalizing changelog for v7.2.0 --- CHANGES.rst | 456 +++++++++++++++++++ docs/changes/18160.other.rst | 1 - docs/changes/18164.other.rst | 1 - docs/changes/18657.other.rst | 1 - docs/changes/18689.other.rst | 1 - docs/changes/18786.other.rst | 1 - docs/changes/config/17934.bugfix.rst | 3 - docs/changes/constants/18118.feature.rst | 15 - docs/changes/coordinates/18193.api.rst | 5 - docs/changes/coordinates/18193.feature.rst | 7 - docs/changes/coordinates/18418.api.rst | 15 - docs/changes/coordinates/18459.feature.rst | 3 - docs/changes/coordinates/18504.bugfix.rst | 4 - docs/changes/coordinates/18638.api.rst | 1 - docs/changes/coordinates/18649.api.rst | 6 - docs/changes/cosmology/18232.feature.rst | 2 - docs/changes/cosmology/18271.feature.rst | 1 - docs/changes/cosmology/18447.feature.rst | 1 - docs/changes/cosmology/18632.feature.rst | 1 - docs/changes/cosmology/18760.feature.rst | 2 - docs/changes/cosmology/18769.feature.rst | 6 - docs/changes/cosmology/18787.feature.rst | 1 - docs/changes/cosmology/18794.feature.rst | 1 - docs/changes/io.ascii/18506.feature.rst | 1 - docs/changes/io.fits/18151.feature.rst | 2 - docs/changes/io.fits/18379.feature.rst | 3 - docs/changes/io.fits/18487.bugfix.rst | 1 - docs/changes/io.fits/18574.bugfix.rst | 1 - docs/changes/io.fits/18681.bugfix.rst | 1 - docs/changes/io.fits/18717.feature.rst | 1 - docs/changes/io.fits/18882.bugfix.rst | 3 - docs/changes/io.misc/18267.feature.rst | 5 - docs/changes/io.misc/18677.bugfix.rst | 2 - docs/changes/io.misc/18712.feature.rst | 3 - docs/changes/io.registry/17572.api.rst | 2 - docs/changes/io.registry/18470.feature.rst | 2 - docs/changes/io.votable/18151.feature.rst | 1 - docs/changes/io.votable/18366.api.rst | 2 - docs/changes/io.votable/18366.feature.rst | 1 - docs/changes/io.votable/18483.bugfix.rst | 6 - docs/changes/io.votable/18771.bugfix.rst | 1 - docs/changes/modeling/17304.feature.rst | 3 - docs/changes/modeling/18958.bugfix.rst | 1 - docs/changes/nddata/18205.feature.rst | 1 - docs/changes/nddata/18392.bugfix.rst | 7 - docs/changes/samp/18151.feature.rst | 1 - docs/changes/samp/18169.bugfix.rst | 2 - docs/changes/stats/18676.bugfix.rst | 1 - docs/changes/table/17631.api.rst | 2 - docs/changes/table/18151.feature.rst | 1 - docs/changes/table/18435.feature.rst | 8 - docs/changes/table/18641.feature.rst | 10 - docs/changes/table/18680.api.rst | 5 - docs/changes/table/18680.feature.rst | 10 - docs/changes/table/18694.bugfix.rst | 4 - docs/changes/table/18725.bugfix.rst | 2 - docs/changes/table/18752.bugfix.rst | 5 - docs/changes/table/18818.bugfix.rst | 1 - docs/changes/tests/17874.api.rst | 10 - docs/changes/time/18193.feature.rst | 2 - docs/changes/time/18330.feature.rst | 6 - docs/changes/units/18500.bugfix.rst | 4 - docs/changes/units/18586.feature.rst | 5 - docs/changes/units/18723.bugfix.rst | 3 - docs/changes/units/18776.bugfix.rst | 2 - docs/changes/utils/18053.api.rst | 2 - docs/changes/utils/18173.bugfix.rst | 3 - docs/changes/utils/18193.bugfix.rst | 3 - docs/changes/utils/18518.api.rst | 5 - docs/changes/visualization/18151.feature.rst | 1 - docs/changes/visualization/18312.feature.rst | 2 - docs/changes/visualization/18324.api.rst | 4 - docs/changes/visualization/18443.feature.rst | 3 - docs/changes/visualization/18590.bugfix.rst | 2 - docs/changes/visualization/18602.feature.rst | 3 - docs/changes/visualization/18792.api.rst | 3 - docs/changes/wcs/18151.feature.rst | 1 - docs/changes/wcs/18338.feature.rst | 3 - docs/changes/wcs/18730.bugfix.rst | 1 - 79 files changed, 456 insertions(+), 249 deletions(-) delete mode 100644 docs/changes/18160.other.rst delete mode 100644 docs/changes/18164.other.rst delete mode 100644 docs/changes/18657.other.rst delete mode 100644 docs/changes/18689.other.rst delete mode 100644 docs/changes/18786.other.rst delete mode 100644 docs/changes/config/17934.bugfix.rst delete mode 100644 docs/changes/constants/18118.feature.rst delete mode 100644 docs/changes/coordinates/18193.api.rst delete mode 100644 docs/changes/coordinates/18193.feature.rst delete mode 100644 docs/changes/coordinates/18418.api.rst delete mode 100644 docs/changes/coordinates/18459.feature.rst delete mode 100644 docs/changes/coordinates/18504.bugfix.rst delete mode 100644 docs/changes/coordinates/18638.api.rst delete mode 100644 docs/changes/coordinates/18649.api.rst delete mode 100644 docs/changes/cosmology/18232.feature.rst delete mode 100644 docs/changes/cosmology/18271.feature.rst delete mode 100644 docs/changes/cosmology/18447.feature.rst delete mode 100644 docs/changes/cosmology/18632.feature.rst delete mode 100644 docs/changes/cosmology/18760.feature.rst delete mode 100644 docs/changes/cosmology/18769.feature.rst delete mode 100644 docs/changes/cosmology/18787.feature.rst delete mode 100644 docs/changes/cosmology/18794.feature.rst delete mode 100644 docs/changes/io.ascii/18506.feature.rst delete mode 100644 docs/changes/io.fits/18151.feature.rst delete mode 100644 docs/changes/io.fits/18379.feature.rst delete mode 100644 docs/changes/io.fits/18487.bugfix.rst delete mode 100644 docs/changes/io.fits/18574.bugfix.rst delete mode 100644 docs/changes/io.fits/18681.bugfix.rst delete mode 100644 docs/changes/io.fits/18717.feature.rst delete mode 100644 docs/changes/io.fits/18882.bugfix.rst delete mode 100644 docs/changes/io.misc/18267.feature.rst delete mode 100644 docs/changes/io.misc/18677.bugfix.rst delete mode 100644 docs/changes/io.misc/18712.feature.rst delete mode 100644 docs/changes/io.registry/17572.api.rst delete mode 100644 docs/changes/io.registry/18470.feature.rst delete mode 100644 docs/changes/io.votable/18151.feature.rst delete mode 100644 docs/changes/io.votable/18366.api.rst delete mode 100644 docs/changes/io.votable/18366.feature.rst delete mode 100644 docs/changes/io.votable/18483.bugfix.rst delete mode 100644 docs/changes/io.votable/18771.bugfix.rst delete mode 100644 docs/changes/modeling/17304.feature.rst delete mode 100644 docs/changes/modeling/18958.bugfix.rst delete mode 100644 docs/changes/nddata/18205.feature.rst delete mode 100644 docs/changes/nddata/18392.bugfix.rst delete mode 100644 docs/changes/samp/18151.feature.rst delete mode 100644 docs/changes/samp/18169.bugfix.rst delete mode 100644 docs/changes/stats/18676.bugfix.rst delete mode 100644 docs/changes/table/17631.api.rst delete mode 100644 docs/changes/table/18151.feature.rst delete mode 100644 docs/changes/table/18435.feature.rst delete mode 100644 docs/changes/table/18641.feature.rst delete mode 100644 docs/changes/table/18680.api.rst delete mode 100644 docs/changes/table/18680.feature.rst delete mode 100644 docs/changes/table/18694.bugfix.rst delete mode 100644 docs/changes/table/18725.bugfix.rst delete mode 100644 docs/changes/table/18752.bugfix.rst delete mode 100644 docs/changes/table/18818.bugfix.rst delete mode 100644 docs/changes/tests/17874.api.rst delete mode 100644 docs/changes/time/18193.feature.rst delete mode 100644 docs/changes/time/18330.feature.rst delete mode 100644 docs/changes/units/18500.bugfix.rst delete mode 100644 docs/changes/units/18586.feature.rst delete mode 100644 docs/changes/units/18723.bugfix.rst delete mode 100644 docs/changes/units/18776.bugfix.rst delete mode 100644 docs/changes/utils/18053.api.rst delete mode 100644 docs/changes/utils/18173.bugfix.rst delete mode 100644 docs/changes/utils/18193.bugfix.rst delete mode 100644 docs/changes/utils/18518.api.rst delete mode 100644 docs/changes/visualization/18151.feature.rst delete mode 100644 docs/changes/visualization/18312.feature.rst delete mode 100644 docs/changes/visualization/18324.api.rst delete mode 100644 docs/changes/visualization/18443.feature.rst delete mode 100644 docs/changes/visualization/18590.bugfix.rst delete mode 100644 docs/changes/visualization/18602.feature.rst delete mode 100644 docs/changes/visualization/18792.api.rst delete mode 100644 docs/changes/wcs/18151.feature.rst delete mode 100644 docs/changes/wcs/18338.feature.rst delete mode 100644 docs/changes/wcs/18730.bugfix.rst diff --git a/CHANGES.rst b/CHANGES.rst index bc647bb417fd..6057fe864a47 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,459 @@ +Version 7.2.0 (2025-11-25) +========================== + + +New Features +------------ + +astropy.constants +^^^^^^^^^^^^^^^^^ + +- Added CODATA 2022 support in ``astropy.constants``. + + This update affects the following constants while the rest are unchanged from CODATA 2018: + + - ``m_p`` (Proton mass) + - ``m_n`` (Neutron mass) + - ``m_e`` (Electron mass) + - ``u`` (Atomic mass) + - ``eps0`` (Vacuum electric permittivity) + - ``Ryd`` (Rydberg constant) + - ``a0`` (Bohr radius) + - ``muB`` (Bohr magneton) + - ``alpha`` (Fine-structure constant) + - ``mu0`` (Vacuum magnetic permeability) + - ``sigma_T`` (Thomson scattering cross-section) [#18118] + +astropy.coordinates +^^^^^^^^^^^^^^^^^^^ + +- Allow ``np.concatenate``, ``np.stack`` and similar numpy functions to + be applied on representations and differentials. + + They can also be applied to coordinate frames and ``SkyCoord``, though + with the same limitation as for setting elements of frames and + coordinates: all frame attributes have to be scalars (or arrays with + only identical elements). [#18193] + +- The results of ``match_coordinates_3d()``, ``match_coordinates_sky()``, + ``search_around_3d()`` and ``search_around_sky()`` and the corresponding + ``SkyCoord`` methods now have named attributes. [#18459] + +astropy.cosmology +^^^^^^^^^^^^^^^^^ + +- The trait ``astropy.cosmology.traits.CurvatureComponent`` has been added to work with + objects that have attributes and methods related to the global curvature. [#18232] + +- The trait ``astropy.cosmology.traits.HubbleParameter`` has been added to work with objects that have attributes and methods related to the Hubble parameter. [#18271] + +- The trait ``astropy.cosmology.traits.DarkEnergyComponent`` has been added to work with objects that have attributes and methods related to the Dark Energy component. [#18447] + +- Cosmology methods now exclusively return arrays, not floats or other scalars. [#18632] + +- The trait ``astropy.cosmology.traits.DarkMatterComponent`` has been added to work with + objects that have attributes and methods related to dark matter. [#18760] + +- The trait ``astropy.cosmology.traits.MatterComponent`` has been added to work with + objects that have attributes and methods related to matter density. + The trait ``astropy.cosmology.traits.BaryonComponent`` has been added to work with + objects that have attributes and methods related to baryonic matter. + The trait ``astropy.cosmology.traits.CriticalDensity`` has been added to work with + objects that have attributes and methods related to the critical density. [#18769] + +- The trait ``astropy.cosmology.traits.PhotonComponent`` has been added to work with objects that have attributes and methods related to photons. [#18787] + +- The trait ``astropy.cosmology.traits.TotalComponent`` has been added to work with objects that have attributes and methods related to the total density component of the universe. [#18794] + +astropy.io.ascii +^^^^^^^^^^^^^^^^ + +- CDS table reader will find the metadata for gzipped tables in the accompanying ReadMe file. [#18506] + +astropy.io.fits +^^^^^^^^^^^^^^^ + +- Enable color and suggestion-on-typos in all ``argparse`` CLIs for Python 3.14 + (``fitscheck``, ``fitsdiff``, ``fitsheader`` and ``fitsinfo``). [#18151] + +- Allow reading a FITS file hosted on a cloud resource like Amazon S3 via + ``Table.read()``. This is done with a new ``fsspec_kwargs`` dict argument + that gets passed through to ``fsspec`` to access cloud resources. [#18379] + +- It is now possible to check the existence of ``Columns`` in ``ColDefs`` by using the membership operator. [#18717] + +astropy.io.misc +^^^^^^^^^^^^^^^ + +- Added a new ECSV table reading module that supports different backend engines for the + CSV data parsing. In addition to the default "io.ascii" engine, this includes engines + that use the PyArrow and Pandas CSV readers. These can be up to 16 times faster and are + more memory efficient than the native astropy ECSV reader. To get help with this + interface run ``Table.read.help(format="ecsv")``. [#18267] + +- Improve support for compressed file formats in the ECSV and the pyarrow CSV + Table readers. All formats supported by ``astropy.utils.data.get_readable_fileobj()`` + (currently gzip, bzip2, lzma (xz) or lzw (Z)) will now work with these readers. [#18712] + +astropy.io.registry +^^^^^^^^^^^^^^^^^^^ + +- Allow setting EXTNAME when writing a ``Table`` to a FITS file, e.g. + ``tbl.write("filename.fits", name="CAT", append=True)``. [#18470] + +astropy.io.votable +^^^^^^^^^^^^^^^^^^ + +- Enable color and suggestion-on-typos in ``volint`` CLI for Python 3.14 [#18151] + +- Modified the constructor for ``astropy.io.votable.tree.TableElement`` to use the version configuration information from the parent ``VOTableFile`` instance. This allows for better handling of version-specific features and ensures that the table element is created with the correct context regarding the VOTable version. [#18366] + +astropy.modeling +^^^^^^^^^^^^^^^^ + +- Add support for unit change propagation through the ``|`` (model composition) operator, + using either `~astropy.modeling.compose_models_with_units` or by setting the + ``unit_change_composition`` attribute on the model after composition. [#17304] + +astropy.nddata +^^^^^^^^^^^^^^ + +- The ``interpret_bit_flags`` function now strips whitespace from flag names. [#18205] + +astropy.samp +^^^^^^^^^^^^ + +- Enable color and suggestion-on-typos in ``samp_hub`` CLI for Python 3.14 [#18151] + +astropy.table +^^^^^^^^^^^^^ + +- Enable color and suggestion-on-typos in ``showtable`` CLI for Python 3.14 [#18151] + +- Added generic ``from_df`` and ``to_df`` methods to ``astropy.Table`` using + ``narwhals``. These methods provide a unified interface for converting between + Astropy Tables and various DataFrame formats (pandas, polars, pyarrow, etc.) + through the narwhals library. The ``to_df`` method converts an Astropy Table + to any supported DataFrame format, while ``from_df`` creates an Astropy Table + from any narwhals-compatible DataFrame. Narwhals is a lightweight compatibility + layer that provides a unified API across different DataFrame libraries, allowing + seamless interoperability without requiring all DataFrame libraries as dependencies. [#18435] + +- Setting the ``units`` or ``descriptions`` of ``QTable`` and ``Table`` + has been made more flexible for tables with optional columns that may + or may not appear in the data. This applies to directly creating a table + as well as reading formatted data with the ``read()`` method. + + In both cases you can supply ``units`` and ``description`` arguments as a + ``dict`` that specifies the units and descriptions for column names in + the table. Previously, if the input table did not contain a column that + was specified in the ``units`` or ``description`` dict, a ``ValueError`` + was raised. Now, such columns are simply ignored. [#18641] + +- A new method has been added for accessing a table index for tables with multiple + indices. You can now select the index with the ``with_index(index_id)`` method of the + ``.loc``, ``.iloc``, and ``.loc_indices`` properties. For example, for a table ``t`` + which has two indices on columns ``"a"`` and ``"b"`` respectively, + ``t.loc.with_index("b")[2]`` will use index ``"b"`` to find all the table rows where + ``t["b"] == 2``. Doing this query using the previous syntax ``t.loc["b", 2]`` is + deprecated and this functionality is planned for removal in astropy 9.0. + + In addition, support has been added for using ``.loc``, ``.iloc``, and ``.loc_indices`` + with an index based on two or more key columns. Previously this raised a ``ValueError``. [#18680] + +astropy.time +^^^^^^^^^^^^ + +- Allow ``np.concatenate``, ``np.stack`` and similar numpy functions to + be applied on ``Time`` and ``TimeDelta`` instances. [#18193] + +- Add a new time format ``galex`` for the GALEX satellite. + + In GALEX data, due to uncertainty in the spacecraft clock, the absolute time is only accurate to + about 1-10 seconds while the relative time within an observation is better than 0.005 s or so, + except on days with leap seconds, where relative times can be wrong by up to 1 s. + See question 101.2 in https://www.galex.caltech.edu/researcher/faq.html [#18330] + +astropy.units +^^^^^^^^^^^^^ + +- Some unit formats have deprecated units and converting such units to strings + emits a warning. + The new ``deprecations`` parameter of the unit ``to_string()`` methods allows + automatically converting deprecated units (if possible), silencing the warnings + or raising them as errors instead. [#18586] + +astropy.visualization +^^^^^^^^^^^^^^^^^^^^^ + +- Enable color and suggestion-on-typos in ``fits2bitmap`` CLI for Python 3.14 [#18151] + +- Added ``show_decimal_unit`` to ``set_major_formatter`` to control whether + or not units are shown in decimal mode. [#18312] + +- Added the methods ``set_visible()`` and ``set_position()`` to control the visibility and position of ticks, tick labels, and axis labels in a single call. + + Also added ``get_ticks_visible()``, ``get_ticklabel_visible()``, and ``get_axislabel_visible()`` methods to get the visibility state of each coordinate element. [#18443] + +- Added an image interval option (``SymmetricInterval``) for specifying a + symmetric extent about a midpoint, and the extent that contains both the image + minimum and maximum can be automatically determined. [#18602] + +astropy.wcs +^^^^^^^^^^^ + +- Enable color and suggestion-on-typos in all ``wcslint`` CLI for Python 3.14 [#18151] + +- Added a ``perserve_units`` keyword argument to ``WCS`` to optionally request + that units are not converted to SI (the default behavior is for celestial axes + to have units converted to degrees, and spectral axes to m or Hz). [#18338] + + +API Changes +----------- + +astropy.coordinates +^^^^^^^^^^^^^^^^^^^ + +- The functionality of ``astropy.coordinates.concatenate`` and + ``astropy.coordinates.concatenate_representations`` is now available using + ``np.concatenate``. Hence, these functions are being deprecated, emitting an + ``AstropyPendingDeprecationWarning`` starting with astropy 7.2. This will be + followed by a regular deprecation warning in astropy 8.0, and removal in 9.0. [#18193] + +- The ``matrix_utilities`` module was not included in the ``astropy`` API + documentation, but it was nonetheless explicitly referred to in some of the + other documentation. + This made it unclear if the functions in the module are public or private. + The public matrix utilities ``is_rotation_or_reflection()`` and + ``rotation_matrix()`` have been made available from the ``astropy.coordinates`` + namespace and should be imported from there. + Functions not available from the ``astropy.coordinate`` namespace are private + and may be changed or removed without warning. + However, three functions have been explicitly deprecated, despite being + private, as a courtesy to existing users. + ``matrix_utilites.angle_axis()`` and ``matrix_utilites.is_rotation()`` are + deprecated without replacement. + ``matrix_utilities.is_O3()`` is deprecated and the public + ``is_rotation_or_reflection()`` function can be used as a replacement. [#18418] + +- The undocumented ``earth_orientation`` module has been removed. [#18638] + +- ``astropy`` prefers reading data required for ``EarthLocation.of_site()`` from + a local cache and tries downloading (and caching) the data from the Internet if + the cache is empty. + As a last resort ``astropy`` has so far read a small bundled data file that + provided data for Greenwich as the single entry, but now ``astropy`` will raise + an error. [#18649] + +astropy.io.registry +^^^^^^^^^^^^^^^^^^^ + +- ``UnifiedInputRegistry`` and ``UnifiedOutputRegistry``'s ``delay_doc_updates`` + method's effect is disabled under Python's optimized mode (``-OO`` flag). [#17572] + +astropy.io.votable +^^^^^^^^^^^^^^^^^^ + +- Added a ``config`` property to ``astropy.io.votable.tree.VOTableFile``. + This property can be passed to the ``config`` parameter of constructors that need to know the associated VOTable version, such as ``TimeSys`` and ``CooSys``. [#18366] + +astropy.table +^^^^^^^^^^^^^ + +- Add additional detail to the text of the ``ValueError`` that is raised when + ``pprint`` cannot parse a column format string. [#17631] + +- Selecting a table index in the ``.loc``, ``.iloc``, or ``.loc_indices`` properties by + passing the index identifier as the first element of the item is deprecated and is + planned for removal in astropy 9.0. For example, if a table ``t`` has two indices on + columns ``"a"`` and ``"b"`` respectively, then ``t.loc["b", 2]`` (to find table rows + where ``t["b"] == 2``) is deprecated. This is replaced by ``t.loc.with_index("b")[2]``. [#18680] + +astropy.tests +^^^^^^^^^^^^^ + +- API changes towards a future deprecation of astropy test runner: + + * ``astropy.tests.runner.keyword`` is removed from public API. + It is used internally as a decorator within astropy test runner and + its exposure as public API was a mistake. In the future, it will be + removed without any deprecation. + * ``astropy.test``, ``astropy.tests.runner.TestRunnerBase``, and ``astropy.tests.runner.TestRunner`` + are now pending deprecation (``AstropyPendingDeprecationWarning``). + This will also affect downstream ``packagename.test`` generated using ``TestRunner``. + They may start to emit ``AstropyDeprecationWarning`` in v8.0 (but no earlier). [#17874] + +astropy.utils +^^^^^^^^^^^^^ + +- The ``isiterable()`` utility is deprecated. + ``numpy.iterable()`` can be used as a drop-in replacement. [#18053] + +- ``astropy.utils.metadata.MergeStrategy`` no longer modifies the ``merge()`` + methods of its subclasses at runtime to re-raise all exceptions as + ``MergeConflictError``. + This does not affect the functionality of ``MergeStrategy`` subclasses within + the ``astropy`` metadata merging machinery. [#18518] + +astropy.visualization +^^^^^^^^^^^^^^^^^^^^^ + +- A warning is now emitted for each axis name which is + invalid in ``set_ticklabel_position``, ``set_axislabel_position``, + and ``set_ticks_position``. This is a deprecation warning, + and in future invalid axis names will result in an error. [#18324] + +- A warning is now emitted if arguments are given to the getter method + ``get_axislabel_visibility_rule``. This is a deprecation warning, and in + future, giving arguments to this method will result in an error. [#18792] + + +Bug Fixes +--------- + +astropy.config +^^^^^^^^^^^^^^ + +- ``get_config_dir()`` and ``get_cache_dir()`` now emit warnings in all cases + where the ``XDG_CACHE_HOME`` (``XDG_CONFIG_HOME``, respectively) environment + variable doesn't meet internal assumptions and is ignored as a result. [#17934] + +astropy.coordinates +^^^^^^^^^^^^^^^^^^^ + +- The ``angle`` argument of the ``rotation_matrix()`` function can now be any + angle-like value, like its docstring states. + Previously some angle-like values (e.g. angle-like strings) were erroneously + rejected. [#18504] + +astropy.io.fits +^^^^^^^^^^^^^^^ + +- Fix bug with heap which was not updated after a VLA column is modified. [#18487] + +- Make ``fitscheck`` verify all HDUs before listing errors. [#18574] + +- Fix calculation of DATASUM/CHECKSUM for heap data in ``BinTableHDU``. [#18681] + +- Fixed a bug in ``fitsdiff`` script where failing to read a single file could + crash the entire program. A warning is now printed instead, and such files + are simply ignored. [#18882] + +astropy.io.misc +^^^^^^^^^^^^^^^ + +- Fixed a bug where writing a table to ECSV fails if meta + contains a value that is a numpy string. [#18677] + +astropy.io.votable +^^^^^^^^^^^^^^^^^^ + +- Updated IVOA UCD1+ controlled vocabulary from version 1.5 to 1.6. This adds + support for new atmospheric observation terms including ``obs.atmos.wind``, + ``obs.atmos.humidity``, ``obs.atmos.rain``, ``obs.atmos.turbulence``, + ``obs.atmos.turbulence.isoplanatic``, ``obs.atmos.water``, and + ``phys.temperature.dew`` which are now recognized when parsing UCDs with + ``check_controlled_vocabulary=True``. [#18483] + +- Fixed a bug in ``add_data_origin_info()`` where ``content`` is ignored for some INFO names. [#18771] + +astropy.modeling +^^^^^^^^^^^^^^^^ + +- Fixed a bug in ``modeling.tabular`` models when the ``lookup_table`` is a Quantity, where the result might lose its unit in some cases. [#18958] + +astropy.nddata +^^^^^^^^^^^^^^ + +- Fixed unexpected upcasting to 64 bits when doing arithmetic with Python scalars + on ``numpy`` 2. + + Don't upcast ``NDData`` unnecessarily when doing arithmetic involving a single + unit (consistent with the behaviour when there are no units). Upcasting still + occurs if an operand's unit gets converted to match the other, or where + required by the other operand's dtype. [#18392] + +astropy.samp +^^^^^^^^^^^^ + +- ``SAMPHubServer._call_and_wait`` raises a new ``SAMPProxyTimeoutError`` (derived from ``SAMPProxyError``) exception on timeout. + This allows client code to more easily distinguish timeouts from other kind of exceptions. [#18169] + +astropy.stats +^^^^^^^^^^^^^ + +- ``poisson_conf_interval`` ``kraft-burrows-nousek`` no longer fails for large N. [#18676] + +astropy.table +^^^^^^^^^^^^^ + +- Fix a bug when slicing a table that has a multi-column index. Previously, after slicing + the table then ``tbl.indices`` would show duplicates of the multi-column index, one for + each column in the index. The underlying indices on the index columns were incorrectly + distinct objects instead of the expected reference to a single index object. [#18694] + +- Fix bugs when indexing a ``QTable`` with a ``Quantity`` column. Previously, after adding + the index then indexed item access via with a ``Quantity`` or slicing was failing. [#18725] + +- Fix a bug where the ECSV writer was not correctly quoting column names if the first name + starts with the "#" character or any names contain leading/trailing whitespace. In this + situation, all column names are now surrounded by double quotes per the ECSV standard. + Likewise the ECSV reader was incorrectly stripping surrounding whitespace from column + names, leading to a consistency check failure when reading. [#18752] + +- Fixed a bug when writing ``Table`` to FITS files, if the table contained masked arrays of integers. [#18818] + +astropy.units +^^^^^^^^^^^^^ + +- The string representations of the liter with the different ``astropy`` unit + formatters are now more consistent with each other. + This change only affects converting units to strings, it has no effect on + parsing strings to units. [#18500] + +- So far only the ``"cds"`` unit format has been capable of parsing the string + ``"as"`` as the attosecond, but now the other unit formats recognize that + string too. [#18723] + +- The ``"ogip"`` unit formatter can now parse strings that include signed + fractions in the exponent, e.g. ``u.Unit("m**(-1/2)", format="ogip")``. [#18776] + +astropy.utils +^^^^^^^^^^^^^ + +- If ``numpy.msort()`` is called with a ``Masked`` array then ``astropy`` no + longer erroneously hides the deprecation warning (with ``numpy`` versions + 1.24-1.26). [#18173] + +- For ``numpy < 2.0``, applying ``np.atleast_*d`` to iterables of most astropy + classes will now return a list of instances instead of a tuple, to match the + behaviour for arrays. For numpy >= 2.0, tuples continue to be returned. [#18193] + +astropy.visualization +^^^^^^^^^^^^^^^^^^^^^ + +- Fixed an image-normalization bug where the interval on a ``ImageNormalize`` + instance could be ignored when plotting. [#18590] + +astropy.wcs +^^^^^^^^^^^ + +- Fixed a bug that caused world_to_array_index to return lists instead of Numpy arrays. [#18730] + +Other Changes and Additions +--------------------------- + +- The minimum required NumPy version is now 1.24. [#18160] + +- The minimum required Matplotlib version is now 3.8.0. [#18164] + +- Bundled ``expat`` is updated to version 2.7.3. [#18657] + +- Updated the bundled CFITSIO library to 4.6.3. [#18689] + +- Wheels are now provided for Windows arm64. [#18786] + Version 7.1.1 (2025-10-10) ========================== diff --git a/docs/changes/18160.other.rst b/docs/changes/18160.other.rst deleted file mode 100644 index 349ccab6ab63..000000000000 --- a/docs/changes/18160.other.rst +++ /dev/null @@ -1 +0,0 @@ -The minimum required NumPy version is now 1.24. diff --git a/docs/changes/18164.other.rst b/docs/changes/18164.other.rst deleted file mode 100644 index 4b2b28b37d70..000000000000 --- a/docs/changes/18164.other.rst +++ /dev/null @@ -1 +0,0 @@ -The minimum required Matplotlib version is now 3.8.0. diff --git a/docs/changes/18657.other.rst b/docs/changes/18657.other.rst deleted file mode 100644 index f9065ce260cc..000000000000 --- a/docs/changes/18657.other.rst +++ /dev/null @@ -1 +0,0 @@ -Bundled ``expat`` is updated to version 2.7.3. diff --git a/docs/changes/18689.other.rst b/docs/changes/18689.other.rst deleted file mode 100644 index 3fbbd48b7bf0..000000000000 --- a/docs/changes/18689.other.rst +++ /dev/null @@ -1 +0,0 @@ -Updated the bundled CFITSIO library to 4.6.3. diff --git a/docs/changes/18786.other.rst b/docs/changes/18786.other.rst deleted file mode 100644 index 528deb7001db..000000000000 --- a/docs/changes/18786.other.rst +++ /dev/null @@ -1 +0,0 @@ -Wheels are now provided for Windows arm64. diff --git a/docs/changes/config/17934.bugfix.rst b/docs/changes/config/17934.bugfix.rst deleted file mode 100644 index 15bdf0ce67d7..000000000000 --- a/docs/changes/config/17934.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -``get_config_dir()`` and ``get_cache_dir()`` now emit warnings in all cases -where the ``XDG_CACHE_HOME`` (``XDG_CONFIG_HOME``, respectively) environment -variable doesn't meet internal assumptions and is ignored as a result. diff --git a/docs/changes/constants/18118.feature.rst b/docs/changes/constants/18118.feature.rst deleted file mode 100644 index bb6b06e91d8e..000000000000 --- a/docs/changes/constants/18118.feature.rst +++ /dev/null @@ -1,15 +0,0 @@ -Added CODATA 2022 support in ``astropy.constants``. - -This update affects the following constants while the rest are unchanged from CODATA 2018: - -- ``m_p`` (Proton mass) -- ``m_n`` (Neutron mass) -- ``m_e`` (Electron mass) -- ``u`` (Atomic mass) -- ``eps0`` (Vacuum electric permittivity) -- ``Ryd`` (Rydberg constant) -- ``a0`` (Bohr radius) -- ``muB`` (Bohr magneton) -- ``alpha`` (Fine-structure constant) -- ``mu0`` (Vacuum magnetic permeability) -- ``sigma_T`` (Thomson scattering cross-section) diff --git a/docs/changes/coordinates/18193.api.rst b/docs/changes/coordinates/18193.api.rst deleted file mode 100644 index 37922f4ebc1f..000000000000 --- a/docs/changes/coordinates/18193.api.rst +++ /dev/null @@ -1,5 +0,0 @@ -The functionality of ``astropy.coordinates.concatenate`` and -``astropy.coordinates.concatenate_representations`` is now available using -``np.concatenate``. Hence, these functions are being deprecated, emitting an -``AstropyPendingDeprecationWarning`` starting with astropy 7.2. This will be -followed by a regular deprecation warning in astropy 8.0, and removal in 9.0. diff --git a/docs/changes/coordinates/18193.feature.rst b/docs/changes/coordinates/18193.feature.rst deleted file mode 100644 index 972133429750..000000000000 --- a/docs/changes/coordinates/18193.feature.rst +++ /dev/null @@ -1,7 +0,0 @@ -Allow ``np.concatenate``, ``np.stack`` and similar numpy functions to -be applied on representations and differentials. - -They can also be applied to coordinate frames and ``SkyCoord``, though -with the same limitation as for setting elements of frames and -coordinates: all frame attributes have to be scalars (or arrays with -only identical elements). diff --git a/docs/changes/coordinates/18418.api.rst b/docs/changes/coordinates/18418.api.rst deleted file mode 100644 index cb2442befa33..000000000000 --- a/docs/changes/coordinates/18418.api.rst +++ /dev/null @@ -1,15 +0,0 @@ -The ``matrix_utilities`` module was not included in the ``astropy`` API -documentation, but it was nonetheless explicitly referred to in some of the -other documentation. -This made it unclear if the functions in the module are public or private. -The public matrix utilities ``is_rotation_or_reflection()`` and -``rotation_matrix()`` have been made available from the ``astropy.coordinates`` -namespace and should be imported from there. -Functions not available from the ``astropy.coordinate`` namespace are private -and may be changed or removed without warning. -However, three functions have been explicitly deprecated, despite being -private, as a courtesy to existing users. -``matrix_utilites.angle_axis()`` and ``matrix_utilites.is_rotation()`` are -deprecated without replacement. -``matrix_utilities.is_O3()`` is deprecated and the public -``is_rotation_or_reflection()`` function can be used as a replacement. diff --git a/docs/changes/coordinates/18459.feature.rst b/docs/changes/coordinates/18459.feature.rst deleted file mode 100644 index a41dd90d963e..000000000000 --- a/docs/changes/coordinates/18459.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -The results of ``match_coordinates_3d()``, ``match_coordinates_sky()``, -``search_around_3d()`` and ``search_around_sky()`` and the corresponding -``SkyCoord`` methods now have named attributes. diff --git a/docs/changes/coordinates/18504.bugfix.rst b/docs/changes/coordinates/18504.bugfix.rst deleted file mode 100644 index 1961b61d8dbb..000000000000 --- a/docs/changes/coordinates/18504.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -The ``angle`` argument of the ``rotation_matrix()`` function can now be any -angle-like value, like its docstring states. -Previously some angle-like values (e.g. angle-like strings) were erroneously -rejected. diff --git a/docs/changes/coordinates/18638.api.rst b/docs/changes/coordinates/18638.api.rst deleted file mode 100644 index cf4eed9f5153..000000000000 --- a/docs/changes/coordinates/18638.api.rst +++ /dev/null @@ -1 +0,0 @@ -The undocumented ``earth_orientation`` module has been removed. diff --git a/docs/changes/coordinates/18649.api.rst b/docs/changes/coordinates/18649.api.rst deleted file mode 100644 index bd463b31f187..000000000000 --- a/docs/changes/coordinates/18649.api.rst +++ /dev/null @@ -1,6 +0,0 @@ -``astropy`` prefers reading data required for ``EarthLocation.of_site()`` from -a local cache and tries downloading (and caching) the data from the Internet if -the cache is empty. -As a last resort ``astropy`` has so far read a small bundled data file that -provided data for Greenwich as the single entry, but now ``astropy`` will raise -an error. diff --git a/docs/changes/cosmology/18232.feature.rst b/docs/changes/cosmology/18232.feature.rst deleted file mode 100644 index fb90dddf06cb..000000000000 --- a/docs/changes/cosmology/18232.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -The trait ``astropy.cosmology.traits.CurvatureComponent`` has been added to work with -objects that have attributes and methods related to the global curvature. diff --git a/docs/changes/cosmology/18271.feature.rst b/docs/changes/cosmology/18271.feature.rst deleted file mode 100644 index dfe8bf5fb6fc..000000000000 --- a/docs/changes/cosmology/18271.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The trait ``astropy.cosmology.traits.HubbleParameter`` has been added to work with objects that have attributes and methods related to the Hubble parameter. diff --git a/docs/changes/cosmology/18447.feature.rst b/docs/changes/cosmology/18447.feature.rst deleted file mode 100644 index 643936962987..000000000000 --- a/docs/changes/cosmology/18447.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The trait ``astropy.cosmology.traits.DarkEnergyComponent`` has been added to work with objects that have attributes and methods related to the Dark Energy component. diff --git a/docs/changes/cosmology/18632.feature.rst b/docs/changes/cosmology/18632.feature.rst deleted file mode 100644 index 50450fa1d100..000000000000 --- a/docs/changes/cosmology/18632.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Cosmology methods now exclusively return arrays, not floats or other scalars. diff --git a/docs/changes/cosmology/18760.feature.rst b/docs/changes/cosmology/18760.feature.rst deleted file mode 100644 index 02fec8861b63..000000000000 --- a/docs/changes/cosmology/18760.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -The trait ``astropy.cosmology.traits.DarkMatterComponent`` has been added to work with -objects that have attributes and methods related to dark matter. diff --git a/docs/changes/cosmology/18769.feature.rst b/docs/changes/cosmology/18769.feature.rst deleted file mode 100644 index 89f1548ebefd..000000000000 --- a/docs/changes/cosmology/18769.feature.rst +++ /dev/null @@ -1,6 +0,0 @@ -The trait ``astropy.cosmology.traits.MatterComponent`` has been added to work with -objects that have attributes and methods related to matter density. -The trait ``astropy.cosmology.traits.BaryonComponent`` has been added to work with -objects that have attributes and methods related to baryonic matter. -The trait ``astropy.cosmology.traits.CriticalDensity`` has been added to work with -objects that have attributes and methods related to the critical density. diff --git a/docs/changes/cosmology/18787.feature.rst b/docs/changes/cosmology/18787.feature.rst deleted file mode 100644 index 749a2a64c095..000000000000 --- a/docs/changes/cosmology/18787.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The trait ``astropy.cosmology.traits.PhotonComponent`` has been added to work with objects that have attributes and methods related to photons. diff --git a/docs/changes/cosmology/18794.feature.rst b/docs/changes/cosmology/18794.feature.rst deleted file mode 100644 index 9609b6f1ccba..000000000000 --- a/docs/changes/cosmology/18794.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The trait ``astropy.cosmology.traits.TotalComponent`` has been added to work with objects that have attributes and methods related to the total density component of the universe. diff --git a/docs/changes/io.ascii/18506.feature.rst b/docs/changes/io.ascii/18506.feature.rst deleted file mode 100644 index 6c1bd8a89779..000000000000 --- a/docs/changes/io.ascii/18506.feature.rst +++ /dev/null @@ -1 +0,0 @@ -CDS table reader will find the metadata for gzipped tables in the accompanying ReadMe file. diff --git a/docs/changes/io.fits/18151.feature.rst b/docs/changes/io.fits/18151.feature.rst deleted file mode 100644 index 172428b8d3b5..000000000000 --- a/docs/changes/io.fits/18151.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Enable color and suggestion-on-typos in all ``argparse`` CLIs for Python 3.14 -(``fitscheck``, ``fitsdiff``, ``fitsheader`` and ``fitsinfo``). diff --git a/docs/changes/io.fits/18379.feature.rst b/docs/changes/io.fits/18379.feature.rst deleted file mode 100644 index 499556d83902..000000000000 --- a/docs/changes/io.fits/18379.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Allow reading a FITS file hosted on a cloud resource like Amazon S3 via -``Table.read()``. This is done with a new ``fsspec_kwargs`` dict argument -that gets passed through to ``fsspec`` to access cloud resources. diff --git a/docs/changes/io.fits/18487.bugfix.rst b/docs/changes/io.fits/18487.bugfix.rst deleted file mode 100644 index e0d186752f9e..000000000000 --- a/docs/changes/io.fits/18487.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug with heap which was not updated after a VLA column is modified. diff --git a/docs/changes/io.fits/18574.bugfix.rst b/docs/changes/io.fits/18574.bugfix.rst deleted file mode 100644 index 4f5561af8cfb..000000000000 --- a/docs/changes/io.fits/18574.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Make ``fitscheck`` verify all HDUs before listing errors. diff --git a/docs/changes/io.fits/18681.bugfix.rst b/docs/changes/io.fits/18681.bugfix.rst deleted file mode 100644 index 19eafaec4390..000000000000 --- a/docs/changes/io.fits/18681.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix calculation of DATASUM/CHECKSUM for heap data in ``BinTableHDU``. diff --git a/docs/changes/io.fits/18717.feature.rst b/docs/changes/io.fits/18717.feature.rst deleted file mode 100644 index 89bb584da63c..000000000000 --- a/docs/changes/io.fits/18717.feature.rst +++ /dev/null @@ -1 +0,0 @@ -It is now possible to check the existence of ``Columns`` in ``ColDefs`` by using the membership operator. diff --git a/docs/changes/io.fits/18882.bugfix.rst b/docs/changes/io.fits/18882.bugfix.rst deleted file mode 100644 index c457f82abbc3..000000000000 --- a/docs/changes/io.fits/18882.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed a bug in ``fitsdiff`` script where failing to read a single file could -crash the entire program. A warning is now printed instead, and such files -are simply ignored. diff --git a/docs/changes/io.misc/18267.feature.rst b/docs/changes/io.misc/18267.feature.rst deleted file mode 100644 index 25bda31887fd..000000000000 --- a/docs/changes/io.misc/18267.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -Added a new ECSV table reading module that supports different backend engines for the -CSV data parsing. In addition to the default "io.ascii" engine, this includes engines -that use the PyArrow and Pandas CSV readers. These can be up to 16 times faster and are -more memory efficient than the native astropy ECSV reader. To get help with this -interface run ``Table.read.help(format="ecsv")``. diff --git a/docs/changes/io.misc/18677.bugfix.rst b/docs/changes/io.misc/18677.bugfix.rst deleted file mode 100644 index 2f8e216197e7..000000000000 --- a/docs/changes/io.misc/18677.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed a bug where writing a table to ECSV fails if meta -contains a value that is a numpy string. diff --git a/docs/changes/io.misc/18712.feature.rst b/docs/changes/io.misc/18712.feature.rst deleted file mode 100644 index c5174e6bd238..000000000000 --- a/docs/changes/io.misc/18712.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Improve support for compressed file formats in the ECSV and the pyarrow CSV -Table readers. All formats supported by ``astropy.utils.data.get_readable_fileobj()`` -(currently gzip, bzip2, lzma (xz) or lzw (Z)) will now work with these readers. diff --git a/docs/changes/io.registry/17572.api.rst b/docs/changes/io.registry/17572.api.rst deleted file mode 100644 index d085f9b75521..000000000000 --- a/docs/changes/io.registry/17572.api.rst +++ /dev/null @@ -1,2 +0,0 @@ -``UnifiedInputRegistry`` and ``UnifiedOutputRegistry``'s ``delay_doc_updates`` -method's effect is disabled under Python's optimized mode (``-OO`` flag). diff --git a/docs/changes/io.registry/18470.feature.rst b/docs/changes/io.registry/18470.feature.rst deleted file mode 100644 index 7e36b3fd016d..000000000000 --- a/docs/changes/io.registry/18470.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Allow setting EXTNAME when writing a ``Table`` to a FITS file, e.g. -``tbl.write("filename.fits", name="CAT", append=True)``. diff --git a/docs/changes/io.votable/18151.feature.rst b/docs/changes/io.votable/18151.feature.rst deleted file mode 100644 index e7e99495102d..000000000000 --- a/docs/changes/io.votable/18151.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Enable color and suggestion-on-typos in ``volint`` CLI for Python 3.14 diff --git a/docs/changes/io.votable/18366.api.rst b/docs/changes/io.votable/18366.api.rst deleted file mode 100644 index cd4d73ebd515..000000000000 --- a/docs/changes/io.votable/18366.api.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added a ``config`` property to ``astropy.io.votable.tree.VOTableFile``. -This property can be passed to the ``config`` parameter of constructors that need to know the associated VOTable version, such as ``TimeSys`` and ``CooSys``. diff --git a/docs/changes/io.votable/18366.feature.rst b/docs/changes/io.votable/18366.feature.rst deleted file mode 100644 index 6116cb22e116..000000000000 --- a/docs/changes/io.votable/18366.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Modified the constructor for ``astropy.io.votable.tree.TableElement`` to use the version configuration information from the parent ``VOTableFile`` instance. This allows for better handling of version-specific features and ensures that the table element is created with the correct context regarding the VOTable version. diff --git a/docs/changes/io.votable/18483.bugfix.rst b/docs/changes/io.votable/18483.bugfix.rst deleted file mode 100644 index 556f15f38815..000000000000 --- a/docs/changes/io.votable/18483.bugfix.rst +++ /dev/null @@ -1,6 +0,0 @@ -Updated IVOA UCD1+ controlled vocabulary from version 1.5 to 1.6. This adds -support for new atmospheric observation terms including ``obs.atmos.wind``, -``obs.atmos.humidity``, ``obs.atmos.rain``, ``obs.atmos.turbulence``, -``obs.atmos.turbulence.isoplanatic``, ``obs.atmos.water``, and -``phys.temperature.dew`` which are now recognized when parsing UCDs with -``check_controlled_vocabulary=True``. diff --git a/docs/changes/io.votable/18771.bugfix.rst b/docs/changes/io.votable/18771.bugfix.rst deleted file mode 100644 index a72497f2da29..000000000000 --- a/docs/changes/io.votable/18771.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug in ``add_data_origin_info()`` where ``content`` is ignored for some INFO names. diff --git a/docs/changes/modeling/17304.feature.rst b/docs/changes/modeling/17304.feature.rst deleted file mode 100644 index 3cb0f8096d72..000000000000 --- a/docs/changes/modeling/17304.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Add support for unit change propagation through the ``|`` (model composition) operator, -using either `~astropy.modeling.compose_models_with_units` or by setting the -``unit_change_composition`` attribute on the model after composition. diff --git a/docs/changes/modeling/18958.bugfix.rst b/docs/changes/modeling/18958.bugfix.rst deleted file mode 100644 index babcd756541b..000000000000 --- a/docs/changes/modeling/18958.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug in ``modeling.tabular`` models when the ``lookup_table`` is a Quantity, where the result might lose its unit in some cases. diff --git a/docs/changes/nddata/18205.feature.rst b/docs/changes/nddata/18205.feature.rst deleted file mode 100644 index 81cbb0ab403d..000000000000 --- a/docs/changes/nddata/18205.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The ``interpret_bit_flags`` function now strips whitespace from flag names. diff --git a/docs/changes/nddata/18392.bugfix.rst b/docs/changes/nddata/18392.bugfix.rst deleted file mode 100644 index 667d8017290d..000000000000 --- a/docs/changes/nddata/18392.bugfix.rst +++ /dev/null @@ -1,7 +0,0 @@ -Fixed unexpected upcasting to 64 bits when doing arithmetic with Python scalars -on ``numpy`` 2. - -Don't upcast ``NDData`` unnecessarily when doing arithmetic involving a single -unit (consistent with the behaviour when there are no units). Upcasting still -occurs if an operand's unit gets converted to match the other, or where -required by the other operand's dtype. diff --git a/docs/changes/samp/18151.feature.rst b/docs/changes/samp/18151.feature.rst deleted file mode 100644 index f17d9398d05f..000000000000 --- a/docs/changes/samp/18151.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Enable color and suggestion-on-typos in ``samp_hub`` CLI for Python 3.14 diff --git a/docs/changes/samp/18169.bugfix.rst b/docs/changes/samp/18169.bugfix.rst deleted file mode 100644 index a63b7c6b1d8d..000000000000 --- a/docs/changes/samp/18169.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -``SAMPHubServer._call_and_wait`` raises a new ``SAMPProxyTimeoutError`` (derived from ``SAMPProxyError``) exception on timeout. -This allows client code to more easily distinguish timeouts from other kind of exceptions. diff --git a/docs/changes/stats/18676.bugfix.rst b/docs/changes/stats/18676.bugfix.rst deleted file mode 100644 index cde07db43b27..000000000000 --- a/docs/changes/stats/18676.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``poisson_conf_interval`` ``kraft-burrows-nousek`` no longer fails for large N. diff --git a/docs/changes/table/17631.api.rst b/docs/changes/table/17631.api.rst deleted file mode 100644 index cb1071846f82..000000000000 --- a/docs/changes/table/17631.api.rst +++ /dev/null @@ -1,2 +0,0 @@ -Add additional detail to the text of the ``ValueError`` that is raised when -``pprint`` cannot parse a column format string. diff --git a/docs/changes/table/18151.feature.rst b/docs/changes/table/18151.feature.rst deleted file mode 100644 index 010ad1a68d32..000000000000 --- a/docs/changes/table/18151.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Enable color and suggestion-on-typos in ``showtable`` CLI for Python 3.14 diff --git a/docs/changes/table/18435.feature.rst b/docs/changes/table/18435.feature.rst deleted file mode 100644 index 4df18223384d..000000000000 --- a/docs/changes/table/18435.feature.rst +++ /dev/null @@ -1,8 +0,0 @@ -Added generic ``from_df`` and ``to_df`` methods to ``astropy.Table`` using -``narwhals``. These methods provide a unified interface for converting between -Astropy Tables and various DataFrame formats (pandas, polars, pyarrow, etc.) -through the narwhals library. The ``to_df`` method converts an Astropy Table -to any supported DataFrame format, while ``from_df`` creates an Astropy Table -from any narwhals-compatible DataFrame. Narwhals is a lightweight compatibility -layer that provides a unified API across different DataFrame libraries, allowing -seamless interoperability without requiring all DataFrame libraries as dependencies. diff --git a/docs/changes/table/18641.feature.rst b/docs/changes/table/18641.feature.rst deleted file mode 100644 index 981f049aa238..000000000000 --- a/docs/changes/table/18641.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -Setting the ``units`` or ``descriptions`` of ``QTable`` and ``Table`` -has been made more flexible for tables with optional columns that may -or may not appear in the data. This applies to directly creating a table -as well as reading formatted data with the ``read()`` method. - -In both cases you can supply ``units`` and ``description`` arguments as a -``dict`` that specifies the units and descriptions for column names in -the table. Previously, if the input table did not contain a column that -was specified in the ``units`` or ``description`` dict, a ``ValueError`` -was raised. Now, such columns are simply ignored. diff --git a/docs/changes/table/18680.api.rst b/docs/changes/table/18680.api.rst deleted file mode 100644 index 692cdf9682fc..000000000000 --- a/docs/changes/table/18680.api.rst +++ /dev/null @@ -1,5 +0,0 @@ -Selecting a table index in the ``.loc``, ``.iloc``, or ``.loc_indices`` properties by -passing the index identifier as the first element of the item is deprecated and is -planned for removal in astropy 9.0. For example, if a table ``t`` has two indices on -columns ``"a"`` and ``"b"`` respectively, then ``t.loc["b", 2]`` (to find table rows -where ``t["b"] == 2``) is deprecated. This is replaced by ``t.loc.with_index("b")[2]``. diff --git a/docs/changes/table/18680.feature.rst b/docs/changes/table/18680.feature.rst deleted file mode 100644 index b2f38cdd5737..000000000000 --- a/docs/changes/table/18680.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -A new method has been added for accessing a table index for tables with multiple -indices. You can now select the index with the ``with_index(index_id)`` method of the -``.loc``, ``.iloc``, and ``.loc_indices`` properties. For example, for a table ``t`` -which has two indices on columns ``"a"`` and ``"b"`` respectively, -``t.loc.with_index("b")[2]`` will use index ``"b"`` to find all the table rows where -``t["b"] == 2``. Doing this query using the previous syntax ``t.loc["b", 2]`` is -deprecated and this functionality is planned for removal in astropy 9.0. - -In addition, support has been added for using ``.loc``, ``.iloc``, and ``.loc_indices`` -with an index based on two or more key columns. Previously this raised a ``ValueError``. diff --git a/docs/changes/table/18694.bugfix.rst b/docs/changes/table/18694.bugfix.rst deleted file mode 100644 index 01dc5cd2368a..000000000000 --- a/docs/changes/table/18694.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fix a bug when slicing a table that has a multi-column index. Previously, after slicing -the table then ``tbl.indices`` would show duplicates of the multi-column index, one for -each column in the index. The underlying indices on the index columns were incorrectly -distinct objects instead of the expected reference to a single index object. diff --git a/docs/changes/table/18725.bugfix.rst b/docs/changes/table/18725.bugfix.rst deleted file mode 100644 index 8ce0574de93a..000000000000 --- a/docs/changes/table/18725.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix bugs when indexing a ``QTable`` with a ``Quantity`` column. Previously, after adding -the index then indexed item access via with a ``Quantity`` or slicing was failing. diff --git a/docs/changes/table/18752.bugfix.rst b/docs/changes/table/18752.bugfix.rst deleted file mode 100644 index 6e07dbe517b6..000000000000 --- a/docs/changes/table/18752.bugfix.rst +++ /dev/null @@ -1,5 +0,0 @@ -Fix a bug where the ECSV writer was not correctly quoting column names if the first name -starts with the "#" character or any names contain leading/trailing whitespace. In this -situation, all column names are now surrounded by double quotes per the ECSV standard. -Likewise the ECSV reader was incorrectly stripping surrounding whitespace from column -names, leading to a consistency check failure when reading. diff --git a/docs/changes/table/18818.bugfix.rst b/docs/changes/table/18818.bugfix.rst deleted file mode 100644 index 545083fcea85..000000000000 --- a/docs/changes/table/18818.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug when writing ``Table`` to FITS files, if the table contained masked arrays of integers. diff --git a/docs/changes/tests/17874.api.rst b/docs/changes/tests/17874.api.rst deleted file mode 100644 index 017b9bb4b2e3..000000000000 --- a/docs/changes/tests/17874.api.rst +++ /dev/null @@ -1,10 +0,0 @@ -API changes towards a future deprecation of astropy test runner: - -* ``astropy.tests.runner.keyword`` is removed from public API. - It is used internally as a decorator within astropy test runner and - its exposure as public API was a mistake. In the future, it will be - removed without any deprecation. -* ``astropy.test``, ``astropy.tests.runner.TestRunnerBase``, and ``astropy.tests.runner.TestRunner`` - are now pending deprecation (``AstropyPendingDeprecationWarning``). - This will also affect downstream ``packagename.test`` generated using ``TestRunner``. - They may start to emit ``AstropyDeprecationWarning`` in v8.0 (but no earlier). diff --git a/docs/changes/time/18193.feature.rst b/docs/changes/time/18193.feature.rst deleted file mode 100644 index 802f223d077f..000000000000 --- a/docs/changes/time/18193.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Allow ``np.concatenate``, ``np.stack`` and similar numpy functions to -be applied on ``Time`` and ``TimeDelta`` instances. diff --git a/docs/changes/time/18330.feature.rst b/docs/changes/time/18330.feature.rst deleted file mode 100644 index 0f015ca0eaf3..000000000000 --- a/docs/changes/time/18330.feature.rst +++ /dev/null @@ -1,6 +0,0 @@ -Add a new time format ``galex`` for the GALEX satellite. - -In GALEX data, due to uncertainty in the spacecraft clock, the absolute time is only accurate to -about 1-10 seconds while the relative time within an observation is better than 0.005 s or so, -except on days with leap seconds, where relative times can be wrong by up to 1 s. -See question 101.2 in https://www.galex.caltech.edu/researcher/faq.html diff --git a/docs/changes/units/18500.bugfix.rst b/docs/changes/units/18500.bugfix.rst deleted file mode 100644 index ba8905400edd..000000000000 --- a/docs/changes/units/18500.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -The string representations of the liter with the different ``astropy`` unit -formatters are now more consistent with each other. -This change only affects converting units to strings, it has no effect on -parsing strings to units. diff --git a/docs/changes/units/18586.feature.rst b/docs/changes/units/18586.feature.rst deleted file mode 100644 index 4afcc897649d..000000000000 --- a/docs/changes/units/18586.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -Some unit formats have deprecated units and converting such units to strings -emits a warning. -The new ``deprecations`` parameter of the unit ``to_string()`` methods allows -automatically converting deprecated units (if possible), silencing the warnings -or raising them as errors instead. diff --git a/docs/changes/units/18723.bugfix.rst b/docs/changes/units/18723.bugfix.rst deleted file mode 100644 index 66ae4bab7fd6..000000000000 --- a/docs/changes/units/18723.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -So far only the ``"cds"`` unit format has been capable of parsing the string -``"as"`` as the attosecond, but now the other unit formats recognize that -string too. diff --git a/docs/changes/units/18776.bugfix.rst b/docs/changes/units/18776.bugfix.rst deleted file mode 100644 index 39f82a1f6ed1..000000000000 --- a/docs/changes/units/18776.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -The ``"ogip"`` unit formatter can now parse strings that include signed -fractions in the exponent, e.g. ``u.Unit("m**(-1/2)", format="ogip")``. diff --git a/docs/changes/utils/18053.api.rst b/docs/changes/utils/18053.api.rst deleted file mode 100644 index ecd2983a5ab1..000000000000 --- a/docs/changes/utils/18053.api.rst +++ /dev/null @@ -1,2 +0,0 @@ -The ``isiterable()`` utility is deprecated. -``numpy.iterable()`` can be used as a drop-in replacement. diff --git a/docs/changes/utils/18173.bugfix.rst b/docs/changes/utils/18173.bugfix.rst deleted file mode 100644 index 0644cf1c1d35..000000000000 --- a/docs/changes/utils/18173.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -If ``numpy.msort()`` is called with a ``Masked`` array then ``astropy`` no -longer erroneously hides the deprecation warning (with ``numpy`` versions -1.24-1.26). diff --git a/docs/changes/utils/18193.bugfix.rst b/docs/changes/utils/18193.bugfix.rst deleted file mode 100644 index e4b7824b043b..000000000000 --- a/docs/changes/utils/18193.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -For ``numpy < 2.0``, applying ``np.atleast_*d`` to iterables of most astropy -classes will now return a list of instances instead of a tuple, to match the -behaviour for arrays. For numpy >= 2.0, tuples continue to be returned. diff --git a/docs/changes/utils/18518.api.rst b/docs/changes/utils/18518.api.rst deleted file mode 100644 index c65008132210..000000000000 --- a/docs/changes/utils/18518.api.rst +++ /dev/null @@ -1,5 +0,0 @@ -``astropy.utils.metadata.MergeStrategy`` no longer modifies the ``merge()`` -methods of its subclasses at runtime to re-raise all exceptions as -``MergeConflictError``. -This does not affect the functionality of ``MergeStrategy`` subclasses within -the ``astropy`` metadata merging machinery. diff --git a/docs/changes/visualization/18151.feature.rst b/docs/changes/visualization/18151.feature.rst deleted file mode 100644 index 31825add207b..000000000000 --- a/docs/changes/visualization/18151.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Enable color and suggestion-on-typos in ``fits2bitmap`` CLI for Python 3.14 diff --git a/docs/changes/visualization/18312.feature.rst b/docs/changes/visualization/18312.feature.rst deleted file mode 100644 index 458b10d2ace7..000000000000 --- a/docs/changes/visualization/18312.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added ``show_decimal_unit`` to ``set_major_formatter`` to control whether -or not units are shown in decimal mode. diff --git a/docs/changes/visualization/18324.api.rst b/docs/changes/visualization/18324.api.rst deleted file mode 100644 index b2e26b83c16c..000000000000 --- a/docs/changes/visualization/18324.api.rst +++ /dev/null @@ -1,4 +0,0 @@ -A warning is now emitted for each axis name which is -invalid in ``set_ticklabel_position``, ``set_axislabel_position``, -and ``set_ticks_position``. This is a deprecation warning, -and in future invalid axis names will result in an error. diff --git a/docs/changes/visualization/18443.feature.rst b/docs/changes/visualization/18443.feature.rst deleted file mode 100644 index 2cc1a9fcf05f..000000000000 --- a/docs/changes/visualization/18443.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added the methods ``set_visible()`` and ``set_position()`` to control the visibility and position of ticks, tick labels, and axis labels in a single call. - -Also added ``get_ticks_visible()``, ``get_ticklabel_visible()``, and ``get_axislabel_visible()`` methods to get the visibility state of each coordinate element. diff --git a/docs/changes/visualization/18590.bugfix.rst b/docs/changes/visualization/18590.bugfix.rst deleted file mode 100644 index 8b33b322a2fe..000000000000 --- a/docs/changes/visualization/18590.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed an image-normalization bug where the interval on a ``ImageNormalize`` -instance could be ignored when plotting. diff --git a/docs/changes/visualization/18602.feature.rst b/docs/changes/visualization/18602.feature.rst deleted file mode 100644 index 30789a7eb150..000000000000 --- a/docs/changes/visualization/18602.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added an image interval option (``SymmetricInterval``) for specifying a -symmetric extent about a midpoint, and the extent that contains both the image -minimum and maximum can be automatically determined. diff --git a/docs/changes/visualization/18792.api.rst b/docs/changes/visualization/18792.api.rst deleted file mode 100644 index 1495c6e2eb46..000000000000 --- a/docs/changes/visualization/18792.api.rst +++ /dev/null @@ -1,3 +0,0 @@ -A warning is now emitted if arguments are given to the getter method -``get_axislabel_visibility_rule``. This is a deprecation warning, and in -future, giving arguments to this method will result in an error. diff --git a/docs/changes/wcs/18151.feature.rst b/docs/changes/wcs/18151.feature.rst deleted file mode 100644 index a0b4e7806733..000000000000 --- a/docs/changes/wcs/18151.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Enable color and suggestion-on-typos in all ``wcslint`` CLI for Python 3.14 diff --git a/docs/changes/wcs/18338.feature.rst b/docs/changes/wcs/18338.feature.rst deleted file mode 100644 index 1d69a33362da..000000000000 --- a/docs/changes/wcs/18338.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added a ``perserve_units`` keyword argument to ``WCS`` to optionally request -that units are not converted to SI (the default behavior is for celestial axes -to have units converted to degrees, and spectral axes to m or Hz). diff --git a/docs/changes/wcs/18730.bugfix.rst b/docs/changes/wcs/18730.bugfix.rst deleted file mode 100644 index ac008d1b53bb..000000000000 --- a/docs/changes/wcs/18730.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug that caused world_to_array_index to return lists instead of Numpy arrays. From 992b48d20dedd08fb412ae6dbe59919027fe6483 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 25 Nov 2025 14:29:21 +0000 Subject: [PATCH 025/199] Update stats in what's new --- docs/whatsnew/7.2.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/whatsnew/7.2.rst b/docs/whatsnew/7.2.rst index dc313319ac92..44c26caa3688 100644 --- a/docs/whatsnew/7.2.rst +++ b/docs/whatsnew/7.2.rst @@ -23,10 +23,10 @@ In addition to these major changes, Astropy v7.2 includes a large number of smaller improvements and bug fixes, which are described in the :ref:`changelog`. By the numbers: -* 830 commits have been added since 7.1 -* 140 issues have been closed since 7.1 -* 310 pull requests have been merged since 7.1 -* 49 people have contributed since 7.1 +* 858 commits have been added since 7.1 +* 166 issues have been closed since 7.1 +* 359 pull requests have been merged since 7.1 +* 50 people have contributed since 7.1 * 22 of which are new contributors .. _whatsnew-7.2-faster-ecsv-tab-rdr: From b010f7dcb4f74e517e18db58a34a79989c178659 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Thu, 27 Nov 2025 17:20:33 -0500 Subject: [PATCH 026/199] Backport PR #19001: Improve `SpectralCoord` relativistic Doppler shifts --- astropy/coordinates/spectral_coordinate.py | 32 ++++++++----------- .../tests/test_spectral_coordinate.py | 9 ++++++ docs/changes/coordinates/19001.bugfix.rst | 6 ++++ 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 docs/changes/coordinates/19001.bugfix.rst diff --git a/astropy/coordinates/spectral_coordinate.py b/astropy/coordinates/spectral_coordinate.py index c53463f74b77..f0f3e572cfd2 100644 --- a/astropy/coordinates/spectral_coordinate.py +++ b/astropy/coordinates/spectral_coordinate.py @@ -54,24 +54,20 @@ def _apply_relativistic_doppler_shift(scoord, velocity): # since we can't guarantee that their metadata would be correct/consistent. squantity = scoord.view(SpectralQuantity) - beta = velocity / c - doppler_factor = np.sqrt((1 + beta) / (1 - beta)) - - if squantity.unit.is_equivalent(u.m): # wavelength - return squantity * doppler_factor - elif ( - squantity.unit.is_equivalent(u.Hz) - or squantity.unit.is_equivalent(u.eV) - or squantity.unit.is_equivalent(1 / u.m) - ): - return squantity / doppler_factor - elif squantity.unit.is_equivalent(KMS): # velocity - return (squantity.to(u.Hz) / doppler_factor).to(squantity.unit) - else: # pragma: no cover - raise RuntimeError( - f"Unexpected units in velocity shift: {squantity.unit}. This should not" - " happen, so please report this in the astropy issue tracker!" - ) + beta = (velocity / c).to_value(u.dimensionless_unscaled) + doppler_factor = np.sqrt((1.0 + beta) / (1.0 - beta)) + + match squantity.unit.physical_type: + case u.physical.length: + return squantity * doppler_factor + case u.physical.frequency | u.physical.energy | u.physical.wavenumber: + return squantity / doppler_factor + case u.physical.velocity: + return (squantity.to(u.Hz) / doppler_factor).to(squantity.unit) + raise RuntimeError( # pragma: no cover + f"Unexpected units in velocity shift: {squantity.unit}. This should not" + " happen, so please report this in the astropy issue tracker!" + ) def update_differentials_to_match( diff --git a/astropy/coordinates/tests/test_spectral_coordinate.py b/astropy/coordinates/tests/test_spectral_coordinate.py index 78389412b5e6..b291e7c3b434 100644 --- a/astropy/coordinates/tests/test_spectral_coordinate.py +++ b/astropy/coordinates/tests/test_spectral_coordinate.py @@ -1119,5 +1119,14 @@ def test_spectralcoord_accuracy(specsys): ) +def test_spectralcoord_with_spectral_equivalency(): + # Regression test for #19001 - enabling the `u.spectral()` equivalency could cause + # the relativistic Doppler shift to be applied in the wrong direction. + sc = SpectralCoord(250 * u.MHz, radial_velocity=1000 * u.km / u.s) + assert_quantity_allclose(sc.to_rest(), 250.835306 * u.MHz) # sanity check + with u.set_enabled_equivalencies(u.spectral()): + assert_quantity_allclose(sc.to_rest(), 250.835306 * u.MHz) + + # TODO: add test when target is not ICRS # TODO: add test when SpectralCoord is in velocity to start with diff --git a/docs/changes/coordinates/19001.bugfix.rst b/docs/changes/coordinates/19001.bugfix.rst new file mode 100644 index 000000000000..c0975104fc27 --- /dev/null +++ b/docs/changes/coordinates/19001.bugfix.rst @@ -0,0 +1,6 @@ +Relativistic Doppler shifts are now applied to ``SpectralCoord`` in the correct +direction even if the ``astropy.units.spectral()`` equivalency is enabled. +Previously enabling the equivalency could cause wrong results with the +``SpectralCoord.to_rest()``, +``SpectralCoord.with_observer_stationary_relative_to()`` or +``SpectralCoord.with_radial_velocity_shift()`` methods. From b8e52e07a2536c2a0c7c7121d18fe17ba75744b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 1 Dec 2025 12:04:26 +0100 Subject: [PATCH 027/199] Backport PR #19009: Bump the actions group in /.github/workflows with 2 updates --- .github/workflows/CFF-test.yml | 2 +- .github/workflows/check_changelog.yml | 2 +- .github/workflows/ci_benchmark.yml | 2 +- .github/workflows/ci_cron_daily.yml | 2 +- .github/workflows/ci_cron_monthly.yml | 2 +- .github/workflows/ci_cron_weekly.yml | 4 ++-- .github/workflows/ci_workflows.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/update_astropy_iers_data_pin.yml | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/CFF-test.yml b/.github/workflows/CFF-test.yml index a79eb2fa805e..1607571360a6 100644 --- a/.github/workflows/CFF-test.yml +++ b/.github/workflows/CFF-test.yml @@ -19,7 +19,7 @@ jobs: cffconvert: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - uses: citation-file-format/cffconvert-github-action@4cf11baa70a673bfdf9dad0acc7ee33b3f4b6084 # 2.0.0 diff --git a/.github/workflows/check_changelog.yml b/.github/workflows/check_changelog.yml index c4b0ebdaa5b8..54553ffef3f3 100644 --- a/.github/workflows/check_changelog.yml +++ b/.github/workflows/check_changelog.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'astropy/astropy' steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - uses: scientific-python/action-towncrier-changelog@f9c7df9a9f8b55cb0c12d94d14b281e9bcd101c0 # v2.0.0 diff --git a/.github/workflows/ci_benchmark.yml b/.github/workflows/ci_benchmark.yml index a1af748d1809..2743b0b173ff 100644 --- a/.github/workflows/ci_benchmark.yml +++ b/.github/workflows/ci_benchmark.yml @@ -26,7 +26,7 @@ jobs: CCACHE_MAXSIZE: 400M steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/ci_cron_daily.yml b/.github/workflows/ci_cron_daily.yml index 73fde8cd9cc6..7d42affb1420 100644 --- a/.github/workflows/ci_cron_daily.yml +++ b/.github/workflows/ci_cron_daily.yml @@ -61,7 +61,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/ci_cron_monthly.yml b/.github/workflows/ci_cron_monthly.yml index 9945f17fb5e0..d039cd49aab0 100644 --- a/.github/workflows/ci_cron_monthly.yml +++ b/.github/workflows/ci_cron_monthly.yml @@ -59,7 +59,7 @@ jobs: - arch: s390x steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/ci_cron_weekly.yml b/.github/workflows/ci_cron_weekly.yml index 61c59178ba94..cac007dba543 100644 --- a/.github/workflows/ci_cron_weekly.yml +++ b/.github/workflows/ci_cron_weekly.yml @@ -83,7 +83,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false fetch-depth: 0 @@ -135,7 +135,7 @@ jobs: - arch: armv7 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index c739978136bc..50f72f9fcca3 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -165,7 +165,7 @@ jobs: name: Python 3.11 (build only, partial) with limited API (${{ matrix.os }}) needs: [initial_checks] steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d4c5c3add314..f66f1638a8db 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/update_astropy_iers_data_pin.yml b/.github/workflows/update_astropy_iers_data_pin.yml index 18a25b1a2a03..e3c36f7e8a3c 100644 --- a/.github/workflows/update_astropy_iers_data_pin.yml +++ b/.github/workflows/update_astropy_iers_data_pin.yml @@ -23,7 +23,7 @@ jobs: if: github.repository == 'astropy/astropy' steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Set up Python @@ -43,7 +43,7 @@ jobs: git commit -m "Update minimum required version of astropy-iers-data" fi - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: branch: update-astropy-iers-data-pin branch-suffix: timestamp From 6bb2481daeea8970d43b450821015acfc17d9757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Tue, 2 Dec 2025 09:52:52 +0100 Subject: [PATCH 028/199] Backport PR #19016: Update minimum required version of astropy-iers-data --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 14fae5f42ad7..943a3c43b25f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ keywords = [ dependencies = [ "numpy>=1.24", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires - "astropy-iers-data>=0.2025.10.27.0.39.10", + "astropy-iers-data>=0.2025.12.1.0.45.12", "PyYAML>=6.0.0", "packaging>=22.0.0", ] From ed0f59c8c2ece4b7420219e4e005ca6975d28dd8 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Sun, 7 Dec 2025 09:16:58 -0500 Subject: [PATCH 029/199] Backport PR #19030: Remove Windows line endings from a file --- docs/uncertainty/index.rst | 670 ++++++++++++++++++------------------- 1 file changed, 335 insertions(+), 335 deletions(-) diff --git a/docs/uncertainty/index.rst b/docs/uncertainty/index.rst index 6553965ac892..39c047b570f0 100644 --- a/docs/uncertainty/index.rst +++ b/docs/uncertainty/index.rst @@ -1,373 +1,373 @@ -.. _astropy-uncertainty: - +.. _astropy-uncertainty: + .. testsetup:: >>> import numpy as np >>> np.random.seed(12345) -******************************************************* -Uncertainties and Distributions (`astropy.uncertainty`) -******************************************************* - -Introduction -============ - -.. note:: This subpackage is still in development. - -``astropy`` provides a |Distribution| object to represent statistical -distributions in a form that acts as a drop-in replacement for a |Quantity| -object or a regular |ndarray|. Used in this manner, |Distribution| provides -uncertainty propagation at the cost of additional computation. It can also more -generally represent sampled distributions for Monte Carlo calculation -techniques, for instance. - -The core object for this feature is the |Distribution|. Currently, all -such distributions are Monte Carlo sampled. While this means each distribution -may take more memory, it allows arbitrarily complex operations to be performed -on distributions while maintaining their correlation structure. Some specific -well-behaved distributions (e.g., the normal distribution) have -analytic forms which may eventually enable a more compact and efficient -representation. In the future, these may provide a coherent uncertainty -propagation mechanism to work with `~astropy.nddata.NDData`. However, this is -not currently implemented. Hence, details of storing uncertainties for -`~astropy.nddata.NDData` objects can be found in the :ref:`astropy_nddata` -section. - -Getting Started -=============== - -To demonstrate a basic use case for distributions, consider the problem of -uncertainty propagation of normal distributions. Assume there are two -measurements you wish to add, each with normal uncertainties. We start -with some initial imports and setup:: - - >>> import numpy as np - >>> from astropy import units as u - >>> from astropy import uncertainty as unc - >>> rng = np.random.default_rng(12345) # ensures reproducible example numbers - -Now we create two |Distribution| objects to represent our distributions:: - - >>> a = unc.normal(1*u.kpc, std=30*u.pc, n_samples=10000) - >>> b = unc.normal(2*u.kpc, std=40*u.pc, n_samples=10000) - -For normal distributions, the centers should add as expected, and the standard -deviations add in quadrature. We can check these results (to the limits of our -Monte Carlo sampling) trivially with |Distribution| arithmetic and attributes:: - - >>> c = a + b - >>> c # doctest: +ELLIPSIS - - >>> c.pdf_mean() # doctest: +FLOAT_CMP +******************************************************* +Uncertainties and Distributions (`astropy.uncertainty`) +******************************************************* + +Introduction +============ + +.. note:: This subpackage is still in development. + +``astropy`` provides a |Distribution| object to represent statistical +distributions in a form that acts as a drop-in replacement for a |Quantity| +object or a regular |ndarray|. Used in this manner, |Distribution| provides +uncertainty propagation at the cost of additional computation. It can also more +generally represent sampled distributions for Monte Carlo calculation +techniques, for instance. + +The core object for this feature is the |Distribution|. Currently, all +such distributions are Monte Carlo sampled. While this means each distribution +may take more memory, it allows arbitrarily complex operations to be performed +on distributions while maintaining their correlation structure. Some specific +well-behaved distributions (e.g., the normal distribution) have +analytic forms which may eventually enable a more compact and efficient +representation. In the future, these may provide a coherent uncertainty +propagation mechanism to work with `~astropy.nddata.NDData`. However, this is +not currently implemented. Hence, details of storing uncertainties for +`~astropy.nddata.NDData` objects can be found in the :ref:`astropy_nddata` +section. + +Getting Started +=============== + +To demonstrate a basic use case for distributions, consider the problem of +uncertainty propagation of normal distributions. Assume there are two +measurements you wish to add, each with normal uncertainties. We start +with some initial imports and setup:: + + >>> import numpy as np + >>> from astropy import units as u + >>> from astropy import uncertainty as unc + >>> rng = np.random.default_rng(12345) # ensures reproducible example numbers + +Now we create two |Distribution| objects to represent our distributions:: + + >>> a = unc.normal(1*u.kpc, std=30*u.pc, n_samples=10000) + >>> b = unc.normal(2*u.kpc, std=40*u.pc, n_samples=10000) + +For normal distributions, the centers should add as expected, and the standard +deviations add in quadrature. We can check these results (to the limits of our +Monte Carlo sampling) trivially with |Distribution| arithmetic and attributes:: + + >>> c = a + b + >>> c # doctest: +ELLIPSIS + + >>> c.pdf_mean() # doctest: +FLOAT_CMP - >>> c.pdf_std().to(u.pc) # doctest: +FLOAT_CMP - - -Indeed these are close to the expectations. While this may seem unnecessary for -the basic Gaussian case, for more complex distributions or arithmetic -operations where error analysis becomes untenable, |Distribution| still powers -through:: - - >>> d = unc.uniform(center=3*u.kpc, width=800*u.pc, n_samples=10000) - >>> e = unc.Distribution(((rng.beta(2,5, 10000)-(2/7))/2 + 3)*u.kpc) - >>> f = (c * d * e) ** (1/3) - >>> f.pdf_mean() # doctest: +FLOAT_CMP + >>> c.pdf_std().to(u.pc) # doctest: +FLOAT_CMP + + +Indeed these are close to the expectations. While this may seem unnecessary for +the basic Gaussian case, for more complex distributions or arithmetic +operations where error analysis becomes untenable, |Distribution| still powers +through:: + + >>> d = unc.uniform(center=3*u.kpc, width=800*u.pc, n_samples=10000) + >>> e = unc.Distribution(((rng.beta(2,5, 10000)-(2/7))/2 + 3)*u.kpc) + >>> f = (c * d * e) ** (1/3) + >>> f.pdf_mean() # doctest: +FLOAT_CMP - >>> f.pdf_std() # doctest: +FLOAT_CMP + >>> f.pdf_std() # doctest: +FLOAT_CMP - >>> from matplotlib import pyplot as plt # doctest: +SKIP - >>> from astropy.visualization import quantity_support # doctest: +SKIP + >>> from matplotlib import pyplot as plt # doctest: +SKIP + >>> from astropy.visualization import quantity_support # doctest: +SKIP >>> with quantity_support(): ... fig, ax = plt.subplots() # doctest: +SKIP ... ax.hist(f.distribution, bins=50) # doctest: +SKIP - -.. plot:: - - import numpy as np - from astropy import units as u - from astropy import uncertainty as unc - from astropy.visualization import quantity_support - from matplotlib import pyplot as plt + +.. plot:: + + import numpy as np + from astropy import units as u + from astropy import uncertainty as unc + from astropy.visualization import quantity_support + from matplotlib import pyplot as plt rng = np.random.default_rng() - a = unc.normal(1*u.kpc, std=30*u.pc, n_samples=10000) - b = unc.normal(2*u.kpc, std=40*u.pc, n_samples=10000) - c = a + b - d = unc.uniform(center=3*u.kpc, width=800*u.pc, n_samples=10000) - e = unc.Distribution(((rng.beta(2,5, 10000)-(2/7))/2 + 3)*u.kpc) - f = (c * d * e) ** (1/3) - with quantity_support(): - fig, ax = plt.subplots() - ax.hist(f.distribution, bins=50) - - -Using `astropy.uncertainty` -=========================== - -Creating Distributions ----------------------- - -.. EXAMPLE START: Creating Distributions Using Arrays or Quantities - -The most direct way to create a distribution is to use an array or |Quantity| + a = unc.normal(1*u.kpc, std=30*u.pc, n_samples=10000) + b = unc.normal(2*u.kpc, std=40*u.pc, n_samples=10000) + c = a + b + d = unc.uniform(center=3*u.kpc, width=800*u.pc, n_samples=10000) + e = unc.Distribution(((rng.beta(2,5, 10000)-(2/7))/2 + 3)*u.kpc) + f = (c * d * e) ** (1/3) + with quantity_support(): + fig, ax = plt.subplots() + ax.hist(f.distribution, bins=50) + + +Using `astropy.uncertainty` +=========================== + +Creating Distributions +---------------------- + +.. EXAMPLE START: Creating Distributions Using Arrays or Quantities + +The most direct way to create a distribution is to use an array or |Quantity| that carries the samples in the *last* dimension:: - >>> import numpy as np - >>> from astropy import units as u - >>> from astropy import uncertainty as unc - >>> rng = np.random.default_rng(123456) # ensures "random" numbers match examples below + >>> import numpy as np + >>> from astropy import units as u + >>> from astropy import uncertainty as unc + >>> rng = np.random.default_rng(123456) # ensures "random" numbers match examples below >>> unc.Distribution(rng.poisson(12, (1000))) # doctest: +IGNORE_OUTPUT - NdarrayDistribution([..., 12,...]) with n_samples=1000 - >>> pq = rng.poisson([1, 5, 30, 400], (1000, 4)).T * u.ct # note the transpose, required to get the sampling on the *last* axis - >>> distr = unc.Distribution(pq) - >>> distr # doctest: +ELLIPSIS - - -Note the distinction for these two distributions: the first is built from an -array and therefore does not have |Quantity| attributes like ``unit``, while the -latter does have these attributes. This is reflected in how they interact with -other objects, for example, the ``NdarrayDistribution`` will not combine with -|Quantity| objects containing units. - -.. EXAMPLE END - -.. EXAMPLE START: Creating Distributions Using Helper Functions - -For commonly used distributions, helper functions exist to make creating them -more convenient. The examples below demonstrate several equivalent ways to -create a normal/Gaussian distribution:: - - >>> center = [1, 5, 30, 400] - >>> n_distr = unc.normal(center*u.kpc, std=[0.2, 1.5, 4, 1]*u.kpc, n_samples=1000) - >>> n_distr = unc.normal(center*u.kpc, var=[0.04, 2.25, 16, 1]*u.kpc**2, n_samples=1000) - >>> n_distr = unc.normal(center*u.kpc, ivar=[25, 0.44444444, 0.625, 1]*u.kpc**-2, n_samples=1000) - >>> n_distr.distribution.shape - (4, 1000) - >>> unc.normal(center*u.kpc, std=[0.2, 1.5, 4, 1]*u.kpc, n_samples=100).distribution.shape - (4, 100) - >>> unc.normal(center*u.kpc, std=[0.2, 1.5, 4, 1]*u.kpc, n_samples=20000).distribution.shape - (4, 20000) - -Additionally, Poisson and uniform |Distribution| creation functions exist:: - - >>> unc.poisson(center*u.count, n_samples=1000) # doctest: +ELLIPSIS - - >>> uwidth = [10, 20, 10, 55]*u.pc - >>> unc.uniform(center=center*u.kpc, width=uwidth, n_samples=1000) # doctest: +ELLIPSIS - - >>> unc.uniform(lower=center*u.kpc - uwidth/2, upper=center*u.kpc + uwidth/2, n_samples=1000) # doctest: +ELLIPSIS - - -.. EXAMPLE END - -Users are free to create their own distribution classes following similar -patterns. - -Using Distributions -------------------- - -.. EXAMPLE START: Accessing Properties of Distributions - -This object now acts much like a |Quantity| or |ndarray| for all but the -non-sampled dimension, but with additional statistical operations that work on -the sampled distributions:: - - >>> distr.shape - (4,) - >>> distr.size - 4 - >>> distr.unit - Unit("ct") - >>> distr.n_samples - 1000 - >>> distr.pdf_mean() # doctest: +FLOAT_CMP + NdarrayDistribution([..., 12,...]) with n_samples=1000 + >>> pq = rng.poisson([1, 5, 30, 400], (1000, 4)).T * u.ct # note the transpose, required to get the sampling on the *last* axis + >>> distr = unc.Distribution(pq) + >>> distr # doctest: +ELLIPSIS + + +Note the distinction for these two distributions: the first is built from an +array and therefore does not have |Quantity| attributes like ``unit``, while the +latter does have these attributes. This is reflected in how they interact with +other objects, for example, the ``NdarrayDistribution`` will not combine with +|Quantity| objects containing units. + +.. EXAMPLE END + +.. EXAMPLE START: Creating Distributions Using Helper Functions + +For commonly used distributions, helper functions exist to make creating them +more convenient. The examples below demonstrate several equivalent ways to +create a normal/Gaussian distribution:: + + >>> center = [1, 5, 30, 400] + >>> n_distr = unc.normal(center*u.kpc, std=[0.2, 1.5, 4, 1]*u.kpc, n_samples=1000) + >>> n_distr = unc.normal(center*u.kpc, var=[0.04, 2.25, 16, 1]*u.kpc**2, n_samples=1000) + >>> n_distr = unc.normal(center*u.kpc, ivar=[25, 0.44444444, 0.625, 1]*u.kpc**-2, n_samples=1000) + >>> n_distr.distribution.shape + (4, 1000) + >>> unc.normal(center*u.kpc, std=[0.2, 1.5, 4, 1]*u.kpc, n_samples=100).distribution.shape + (4, 100) + >>> unc.normal(center*u.kpc, std=[0.2, 1.5, 4, 1]*u.kpc, n_samples=20000).distribution.shape + (4, 20000) + +Additionally, Poisson and uniform |Distribution| creation functions exist:: + + >>> unc.poisson(center*u.count, n_samples=1000) # doctest: +ELLIPSIS + + >>> uwidth = [10, 20, 10, 55]*u.pc + >>> unc.uniform(center=center*u.kpc, width=uwidth, n_samples=1000) # doctest: +ELLIPSIS + + >>> unc.uniform(lower=center*u.kpc - uwidth/2, upper=center*u.kpc + uwidth/2, n_samples=1000) # doctest: +ELLIPSIS + + +.. EXAMPLE END + +Users are free to create their own distribution classes following similar +patterns. + +Using Distributions +------------------- + +.. EXAMPLE START: Accessing Properties of Distributions + +This object now acts much like a |Quantity| or |ndarray| for all but the +non-sampled dimension, but with additional statistical operations that work on +the sampled distributions:: + + >>> distr.shape + (4,) + >>> distr.size + 4 + >>> distr.unit + Unit("ct") + >>> distr.n_samples + 1000 + >>> distr.pdf_mean() # doctest: +FLOAT_CMP - >>> distr.pdf_std() # doctest: +FLOAT_CMP + >>> distr.pdf_std() # doctest: +FLOAT_CMP - >>> distr.pdf_var() # doctest: +FLOAT_CMP + >>> distr.pdf_var() # doctest: +FLOAT_CMP - >>> distr.pdf_median() - - >>> distr.pdf_mad() # Median absolute deviation # doctest: +FLOAT_CMP + >>> distr.pdf_median() + + >>> distr.pdf_mad() # Median absolute deviation # doctest: +FLOAT_CMP - >>> distr.pdf_smad() # Median absolute deviation, rescaled to match std for normal # doctest: +FLOAT_CMP + >>> distr.pdf_smad() # Median absolute deviation, rescaled to match std for normal # doctest: +FLOAT_CMP - >>> distr.pdf_percentiles([10, 50, 90]) + >>> distr.pdf_percentiles([10, 50, 90]) - >>> distr.pdf_percentiles([.1, .5, .9]*u.dimensionless_unscaled) + >>> distr.pdf_percentiles([.1, .5, .9]*u.dimensionless_unscaled) - -If need be, the underlying array can then be accessed from the ``distribution`` -attribute:: - + +If need be, the underlying array can then be accessed from the ``distribution`` +attribute:: + >>> distr.distribution - >>> distr.distribution.shape - (4, 1000) - -.. EXAMPLE END - -.. EXAMPLE START: Interaction Between Quantity Objects and Distributions - -A |Quantity| distribution interacts naturally with non-|Distribution| -|Quantity| objects, assuming the |Quantity| is a Dirac delta distribution:: - - >>> distr_in_kpc = distr * u.kpc/u.count # for the sake of round numbers in examples - >>> distrplus = distr_in_kpc + [2000,0,0,500]*u.pc - >>> distrplus.pdf_median() - - >>> distrplus.pdf_var() # doctest: +FLOAT_CMP + >>> distr.distribution.shape + (4, 1000) + +.. EXAMPLE END + +.. EXAMPLE START: Interaction Between Quantity Objects and Distributions + +A |Quantity| distribution interacts naturally with non-|Distribution| +|Quantity| objects, assuming the |Quantity| is a Dirac delta distribution:: + + >>> distr_in_kpc = distr * u.kpc/u.count # for the sake of round numbers in examples + >>> distrplus = distr_in_kpc + [2000,0,0,500]*u.pc + >>> distrplus.pdf_median() + + >>> distrplus.pdf_var() # doctest: +FLOAT_CMP - -It also operates as expected with other distributions (but see below for a -discussion of covariances):: - + +It also operates as expected with other distributions (but see below for a +discussion of covariances):: + >>> means = [2000, 0, 0, 500] >>> sigmas = [1000, .01, 3000, 10] >>> another_distr = unc.Distribution((rng.normal(means, sigmas, (1000,4))).T * u.pc) - >>> combined_distr = distr_in_kpc + another_distr - >>> combined_distr.pdf_median() # doctest: +FLOAT_CMP + >>> combined_distr = distr_in_kpc + another_distr + >>> combined_distr.pdf_median() # doctest: +FLOAT_CMP - >>> combined_distr.pdf_var() # doctest: +FLOAT_CMP + >>> combined_distr.pdf_var() # doctest: +FLOAT_CMP - -.. EXAMPLE END - -Covariance in Distributions and Discrete Sampling Effects ---------------------------------------------------------- - -One of the main applications for distributions is uncertainty propagation, which -critically requires proper treatment of covariance. This comes naturally in the -Monte Carlo sampling approach used by the |Distribution| class, as long as -proper care is taken with sampling error. - -.. EXAMPLE START: Covariance in Distributions - -To start with a basic example, two un-correlated distributions should produce -an un-correlated joint distribution plot: - -.. plot:: - :context: close-figs - :include-source: - - >>> import numpy as np - >>> from astropy import units as u - >>> from astropy import uncertainty as unc - >>> from matplotlib import pyplot as plt # doctest: +SKIP - >>> n1 = unc.normal(center=0., std=1, n_samples=10000) - >>> n2 = unc.normal(center=0., std=2, n_samples=10000) - >>> fig, ax = plt.subplots() # doctest: +SKIP - >>> ax.scatter(n1.distribution, n2.distribution, s=2, lw=0, alpha=.5) # doctest: +SKIP - >>> ax.set(xlim=(-4, 4), ylim=(-4, 4)) # doctest: +SKIP - -Indeed, the distributions are independent. If we instead construct a covariant -pair of Gaussians, it is immediately apparent: - -.. plot:: - :context: close-figs - :include-source: - + +.. EXAMPLE END + +Covariance in Distributions and Discrete Sampling Effects +--------------------------------------------------------- + +One of the main applications for distributions is uncertainty propagation, which +critically requires proper treatment of covariance. This comes naturally in the +Monte Carlo sampling approach used by the |Distribution| class, as long as +proper care is taken with sampling error. + +.. EXAMPLE START: Covariance in Distributions + +To start with a basic example, two un-correlated distributions should produce +an un-correlated joint distribution plot: + +.. plot:: + :context: close-figs + :include-source: + + >>> import numpy as np + >>> from astropy import units as u + >>> from astropy import uncertainty as unc + >>> from matplotlib import pyplot as plt # doctest: +SKIP + >>> n1 = unc.normal(center=0., std=1, n_samples=10000) + >>> n2 = unc.normal(center=0., std=2, n_samples=10000) + >>> fig, ax = plt.subplots() # doctest: +SKIP + >>> ax.scatter(n1.distribution, n2.distribution, s=2, lw=0, alpha=.5) # doctest: +SKIP + >>> ax.set(xlim=(-4, 4), ylim=(-4, 4)) # doctest: +SKIP + +Indeed, the distributions are independent. If we instead construct a covariant +pair of Gaussians, it is immediately apparent: + +.. plot:: + :context: close-figs + :include-source: + >>> rng = np.random.default_rng(357) - >>> ncov = rng.multivariate_normal([0, 0], [[1, .5], [.5, 2]], size=10000) - >>> n1 = unc.Distribution(ncov[:, 0]) - >>> n2 = unc.Distribution(ncov[:, 1]) - >>> plt.scatter(n1.distribution, n2.distribution, s=2, lw=0, alpha=.5) # doctest: +SKIP - >>> plt.xlim(-4, 4) # doctest: +SKIP - >>> plt.ylim(-4, 4) # doctest: +SKIP - -Most importantly, the proper correlated structure is preserved or generated as -expected by appropriate arithmetic operations. For example, ratios of -uncorrelated normal distribution gain covariances if the axes are not -independent, as in this simulation of iron, hydrogen, and oxygen abundances in -a hypothetical collection of stars: - -.. plot:: - :context: close-figs - :include-source: - - >>> fe_abund = unc.normal(center=-2, std=.25, n_samples=10000) - >>> o_abund = unc.normal(center=-6., std=.5, n_samples=10000) - >>> h_abund = unc.normal(center=-0.7, std=.1, n_samples=10000) - >>> feh = fe_abund - h_abund - >>> ofe = o_abund - fe_abund - >>> plt.scatter(ofe.distribution, feh.distribution, s=2, lw=0, alpha=.5) # doctest: +SKIP - >>> plt.xlabel('[Fe/H]') # doctest: +SKIP - >>> plt.ylabel('[O/Fe]') # doctest: +SKIP - -This demonstrates that the correlations naturally arise from the variables, but -there is no need to explicitly account for it: the sampling process naturally -recovers correlations that are present. - -.. EXAMPLE END - -.. EXAMPLE START: Preserving Covariance in Distributions - -An important note of warning, however, is that the covariance is only preserved -if the sampling axes are exactly matched sample by sample. If they are not, all -covariance information is (silently) lost: - -.. plot:: - :context: close-figs - :include-source: - - >>> n2_wrong = unc.Distribution(ncov[::-1, 1]) #reverse the sampling axis order - >>> plt.scatter(n1.distribution, n2_wrong.distribution, s=2, lw=0, alpha=.5) # doctest: +SKIP - >>> plt.xlim(-4, 4) # doctest: +SKIP - >>> plt.ylim(-4, 4) # doctest: +SKIP - -Moreover, an insufficiently sampled distribution may give poor estimates or -hide correlations. The example below is the same as the covariant Gaussian -example above, but with 200x fewer samples: - - -.. plot:: - :context: close-figs - :include-source: - - >>> ncov = rng.multivariate_normal([0, 0], [[1, .5], [.5, 2]], size=50) - >>> n1 = unc.Distribution(ncov[:, 0]) - >>> n2 = unc.Distribution(ncov[:, 1]) - >>> plt.scatter(n1.distribution, n2.distribution, s=5, lw=0) # doctest: +SKIP - >>> plt.xlim(-4, 4) # doctest: +SKIP - >>> plt.ylim(-4, 4) # doctest: +SKIP - >>> np.cov(n1.distribution, n2.distribution) # doctest: +FLOAT_CMP + >>> ncov = rng.multivariate_normal([0, 0], [[1, .5], [.5, 2]], size=10000) + >>> n1 = unc.Distribution(ncov[:, 0]) + >>> n2 = unc.Distribution(ncov[:, 1]) + >>> plt.scatter(n1.distribution, n2.distribution, s=2, lw=0, alpha=.5) # doctest: +SKIP + >>> plt.xlim(-4, 4) # doctest: +SKIP + >>> plt.ylim(-4, 4) # doctest: +SKIP + +Most importantly, the proper correlated structure is preserved or generated as +expected by appropriate arithmetic operations. For example, ratios of +uncorrelated normal distribution gain covariances if the axes are not +independent, as in this simulation of iron, hydrogen, and oxygen abundances in +a hypothetical collection of stars: + +.. plot:: + :context: close-figs + :include-source: + + >>> fe_abund = unc.normal(center=-2, std=.25, n_samples=10000) + >>> o_abund = unc.normal(center=-6., std=.5, n_samples=10000) + >>> h_abund = unc.normal(center=-0.7, std=.1, n_samples=10000) + >>> feh = fe_abund - h_abund + >>> ofe = o_abund - fe_abund + >>> plt.scatter(ofe.distribution, feh.distribution, s=2, lw=0, alpha=.5) # doctest: +SKIP + >>> plt.xlabel('[Fe/H]') # doctest: +SKIP + >>> plt.ylabel('[O/Fe]') # doctest: +SKIP + +This demonstrates that the correlations naturally arise from the variables, but +there is no need to explicitly account for it: the sampling process naturally +recovers correlations that are present. + +.. EXAMPLE END + +.. EXAMPLE START: Preserving Covariance in Distributions + +An important note of warning, however, is that the covariance is only preserved +if the sampling axes are exactly matched sample by sample. If they are not, all +covariance information is (silently) lost: + +.. plot:: + :context: close-figs + :include-source: + + >>> n2_wrong = unc.Distribution(ncov[::-1, 1]) #reverse the sampling axis order + >>> plt.scatter(n1.distribution, n2_wrong.distribution, s=2, lw=0, alpha=.5) # doctest: +SKIP + >>> plt.xlim(-4, 4) # doctest: +SKIP + >>> plt.ylim(-4, 4) # doctest: +SKIP + +Moreover, an insufficiently sampled distribution may give poor estimates or +hide correlations. The example below is the same as the covariant Gaussian +example above, but with 200x fewer samples: + + +.. plot:: + :context: close-figs + :include-source: + + >>> ncov = rng.multivariate_normal([0, 0], [[1, .5], [.5, 2]], size=50) + >>> n1 = unc.Distribution(ncov[:, 0]) + >>> n2 = unc.Distribution(ncov[:, 1]) + >>> plt.scatter(n1.distribution, n2.distribution, s=5, lw=0) # doctest: +SKIP + >>> plt.xlim(-4, 4) # doctest: +SKIP + >>> plt.ylim(-4, 4) # doctest: +SKIP + >>> np.cov(n1.distribution, n2.distribution) # doctest: +FLOAT_CMP array([[0.95534365, 0.35220031], [0.35220031, 1.99511743]]) - -The covariance structure is much less apparent by eye, and this is reflected -in significant discrepancies between the input and output covariance matrix. -In general this is an intrinsic trade-off using sampled distributions: a smaller -number of samples is computationally more efficient, but leads to larger -uncertainties in any of the relevant quantities. These tend to be of order -:math:`\sqrt{n_{\rm samples}}` in any derived quantity, but that depends on the -complexity of the distribution in question. - -.. EXAMPLE END - -.. note that if this section gets too long, it should be moved to a separate - doc page - see the top of performance.inc.rst for the instructions on how to do - that -.. include:: performance.inc.rst - -Reference/API -============= - -.. automodapi:: astropy.uncertainty + +The covariance structure is much less apparent by eye, and this is reflected +in significant discrepancies between the input and output covariance matrix. +In general this is an intrinsic trade-off using sampled distributions: a smaller +number of samples is computationally more efficient, but leads to larger +uncertainties in any of the relevant quantities. These tend to be of order +:math:`\sqrt{n_{\rm samples}}` in any derived quantity, but that depends on the +complexity of the distribution in question. + +.. EXAMPLE END + +.. note that if this section gets too long, it should be moved to a separate + doc page - see the top of performance.inc.rst for the instructions on how to do + that +.. include:: performance.inc.rst + +Reference/API +============= + +.. automodapi:: astropy.uncertainty From 9baed20dace645b378608fe49b3d312a0de25a13 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:30:04 -0500 Subject: [PATCH 030/199] Backport PR #19036: Fix sign errors in `Galactocentric` description --- docs/coordinates/galactocentric.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/coordinates/galactocentric.rst b/docs/coordinates/galactocentric.rst index 54ccf2d2f6ca..27741df32153 100644 --- a/docs/coordinates/galactocentric.rst +++ b/docs/coordinates/galactocentric.rst @@ -100,9 +100,9 @@ The full rotation matrix thus far is: \begin{gathered} \boldsymbol{R} = \boldsymbol{R}_3 \boldsymbol{R}_1 \boldsymbol{R}_2 = \\ \begin{bmatrix} - \cos\alpha_{\rm GC}\cos\delta_{\rm GC}& \cos\delta_{\rm GC}\sin\alpha_{\rm GC}& -\sin\delta_{\rm GC}\\ - \cos\alpha_{\rm GC}\sin\delta_{\rm GC}\sin\eta - \sin\alpha_{\rm GC}\cos\eta & \sin\alpha_{\rm GC}\sin\delta_{\rm GC}\sin\eta + \cos\alpha_{\rm GC}\cos\eta & \cos\delta_{\rm GC}\sin\eta\\ - \cos\alpha_{\rm GC}\sin\delta_{\rm GC}\cos\eta + \sin\alpha_{\rm GC}\sin\eta & \sin\alpha_{\rm GC}\sin\delta_{\rm GC}\cos\eta - \cos\alpha_{\rm GC}\sin\eta & \cos\delta_{\rm GC}\cos\eta + \cos\alpha_{\rm GC}\cos\delta_{\rm GC}& \cos\delta_{\rm GC}\sin\alpha_{\rm GC}& \sin\delta_{\rm GC}\\ + -\cos\alpha_{\rm GC}\sin\delta_{\rm GC}\sin\eta - \sin\alpha_{\rm GC}\cos\eta & -\sin\alpha_{\rm GC}\sin\delta_{\rm GC}\sin\eta + \cos\alpha_{\rm GC}\cos\eta & \cos\delta_{\rm GC}\sin\eta\\ + -\cos\alpha_{\rm GC}\sin\delta_{\rm GC}\cos\eta + \sin\alpha_{\rm GC}\sin\eta & -\sin\alpha_{\rm GC}\sin\delta_{\rm GC}\cos\eta - \cos\alpha_{\rm GC}\sin\eta & \cos\delta_{\rm GC}\cos\eta \end{bmatrix}\end{gathered} With the rotated position vector From 93c5c72fed9139481f800623c7c2b5e3fa153bfd Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:48:51 -0500 Subject: [PATCH 031/199] Backport PR #19058: DOC: Replace URL for stylemanual1989.pdf --- astropy/units/format/latex.py | 4 ++-- docs/units/format.rst | 4 ++-- docs/units/index.rst | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/astropy/units/format/latex.py b/astropy/units/format/latex.py index 248bd3546faf..eb959c1e8be2 100644 --- a/astropy/units/format/latex.py +++ b/astropy/units/format/latex.py @@ -19,7 +19,7 @@ class Latex(console.Console): Output LaTeX to display the unit based on IAU style guidelines. Attempts to follow the `IAU Style Manual - `_. + `_. """ _space: ClassVar[str] = r"\," @@ -75,7 +75,7 @@ class LatexInline(Latex): powers. Attempts to follow the `IAU Style Manual - `_ and the + `_ and the `ApJ and AJ style guide `_. """ diff --git a/docs/units/format.rst b/docs/units/format.rst index 356916754800..5f64facc02c9 100644 --- a/docs/units/format.rst +++ b/docs/units/format.rst @@ -209,7 +209,7 @@ following formats: - ``"latex"``: Writes units out using LaTeX math syntax using the `IAU Style Manual - `_ + `_ recommendations for unit presentation. This format is automatically used when printing a unit in the |IPython| notebook:: @@ -224,7 +224,7 @@ following formats: - ``"latex_inline"``: Writes units out using LaTeX math syntax using the `IAU Style Manual - `_ + `_ recommendations for unit presentation, using negative powers instead of fractions, as required by some journals (e.g., `Apj and AJ `_). diff --git a/docs/units/index.rst b/docs/units/index.rst index 3e6b04cde614..5d008f165f0c 100644 --- a/docs/units/index.rst +++ b/docs/units/index.rst @@ -215,7 +215,7 @@ See Also `_. - `IAU Style Manual - `_. + `_. - `A table of astronomical unit equivalencies `_. From 560ec0ebcc6dd56dbda0bf53f1699b5628fa9d91 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:14:44 -0500 Subject: [PATCH 032/199] Backport PR #19052: Ensure `pyarrow.csv` is available in tests (if installed) --- astropy/io/misc/pyarrow/tests/test_csv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astropy/io/misc/pyarrow/tests/test_csv.py b/astropy/io/misc/pyarrow/tests/test_csv.py index 23f794b64d61..c9373b0b0d08 100644 --- a/astropy/io/misc/pyarrow/tests/test_csv.py +++ b/astropy/io/misc/pyarrow/tests/test_csv.py @@ -16,6 +16,7 @@ if HAS_PYARROW: import pyarrow as pa + import pyarrow.csv else: pytest.skip("pyarrow is not available", allow_module_level=True) From 6da0bb7aa34b9a5aa99dd675cab30e8f4039911c Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:35:19 -0500 Subject: [PATCH 033/199] Backport PR #18687: Allow refreshing `EarthLocation` site registry cache --- astropy/coordinates/earth.py | 36 +++++------ astropy/coordinates/sites.py | 17 ----- astropy/coordinates/tests/test_sites.py | 79 +++++++++++++---------- docs/changes/coordinates/18687.bugfix.rst | 2 + docs/coordinates/index.rst | 2 +- 5 files changed, 64 insertions(+), 72 deletions(-) create mode 100644 docs/changes/coordinates/18687.bugfix.rst diff --git a/astropy/coordinates/earth.py b/astropy/coordinates/earth.py index 8b469acc0ad2..a54281b9f762 100644 --- a/astropy/coordinates/earth.py +++ b/astropy/coordinates/earth.py @@ -14,6 +14,7 @@ from astropy.units.quantity import QuantityInfoBase from astropy.utils import data from astropy.utils.compat import COPY_IF_NEEDED +from astropy.utils.data import get_pkg_data_contents from .angles import Angle, Latitude, Longitude from .errors import UnknownSiteException @@ -383,7 +384,7 @@ class = EarthLocation -------- get_site_names : the list of sites that this function can access """ - registry = cls._get_site_registry(force_download=refresh_cache) + registry = cls._get_site_registry(refresh_cache) try: el = registry[site_name] except UnknownSiteException as e: @@ -531,31 +532,28 @@ def get_site_names(cls, *, refresh_cache=False): of_site : Gets the actual location object for one of the sites names this returns. """ - return cls._get_site_registry(force_download=refresh_cache).names + return cls._get_site_registry(refresh_cache).names @classmethod - def _get_site_registry(cls, force_download=False): + def _get_site_registry(cls, refresh_cache=False): """ Gets the site registry. The first time this tries to download the data. Subsequent calls will use the cached version unless explicitly overridden. - - Parameters - ---------- - force_download : bool or str - If not False, force replacement of the cached registry with a - downloaded version. If a str, that will be used as the URL to - download from (if just True, the default URL will be used). - - Returns - ------- - reg : astropy.coordinates.sites.SiteRegistry """ # need to do this here at the bottom to avoid circular dependencies - from .sites import get_downloaded_sites - - if force_download or not cls._site_registry: - cls._site_registry = get_downloaded_sites( - force_download if isinstance(force_download, str) else None + from .sites import SiteRegistry + + if refresh_cache or not cls._site_registry: + # we explicitly set the encoding because the default is to leave it set by + # the users' locale, which may fail if it's not matched to the sites.json + cls._site_registry = SiteRegistry.from_json( + json.loads( + get_pkg_data_contents( + "coordinates/sites.json", + encoding="UTF-8", + cache="update" if refresh_cache else True, + ) + ) ) return cls._site_registry diff --git a/astropy/coordinates/sites.py b/astropy/coordinates/sites.py index afaab3fa3ebc..0c1455b1871f 100644 --- a/astropy/coordinates/sites.py +++ b/astropy/coordinates/sites.py @@ -10,13 +10,11 @@ updating the ``location.json`` file. """ -import json from collections.abc import Iterator, Mapping from difflib import get_close_matches from typing import Any, Final, Self from astropy import units as u -from astropy.utils.data import get_file_contents, get_pkg_data_contents from .earth import EarthLocation from .errors import UnknownSiteException @@ -125,18 +123,3 @@ def from_json(cls, jsondb: Mapping[str, dict[str, Any]]) -> Self: reg.add_site([site] + aliases, location) return reg - - -def get_downloaded_sites(jsonurl: str | None = None) -> SiteRegistry: - """ - Load observatory database from data.astropy.org and parse into a SiteRegistry. - """ - # we explicitly set the encoding because the default is to leave it set by - # the users' locale, which may fail if it's not matched to the sites.json - if jsonurl is None: - content = get_pkg_data_contents("coordinates/sites.json", encoding="UTF-8") - else: - content = get_file_contents(jsonurl, encoding="UTF-8") - - jsondb = json.loads(content) - return SiteRegistry.from_json(jsondb) diff --git a/astropy/coordinates/tests/test_sites.py b/astropy/coordinates/tests/test_sites.py index 06c7aac981a1..066cf72aa4a2 100644 --- a/astropy/coordinates/tests/test_sites.py +++ b/astropy/coordinates/tests/test_sites.py @@ -1,3 +1,4 @@ +import shutil from pathlib import Path from threading import Lock @@ -6,52 +7,36 @@ from astropy import units as u from astropy.config import set_temp_cache from astropy.coordinates import EarthLocation, Latitude, Longitude, UnknownSiteException -from astropy.coordinates.sites import SiteRegistry, get_downloaded_sites +from astropy.coordinates.sites import SiteRegistry from astropy.tests.helper import assert_quantity_allclose SITE_DATA_LOCK = Lock() +TEST_SITE_DATA_DIR = Path(__file__).with_name("data") / "sites" @pytest.fixture(scope="function") def local_site_data(monkeypatch): with SITE_DATA_LOCK: monkeypatch.setattr(EarthLocation, "_site_registry", None) - with set_temp_cache(Path(__file__).with_name("data") / "sites"): + with set_temp_cache(TEST_SITE_DATA_DIR): yield -@pytest.mark.remote_data(source="astropy") -def test_online_sites(): - reg = get_downloaded_sites() - - keck = reg["keck"] - lon, lat, el = keck.to_geodetic() - assert_quantity_allclose( - lon, -Longitude("155:28.7", unit=u.deg), atol=0.001 * u.deg - ) - assert_quantity_allclose(lat, Latitude("19:49.7", unit=u.deg), atol=0.001 * u.deg) - assert_quantity_allclose(el, 4160 * u.m, atol=1 * u.m) - - names = reg.names - assert "keck" in names - assert "ctio" in names +@pytest.fixture(scope="function") +def remote_site_data(monkeypatch, tmp_path): + with SITE_DATA_LOCK: + monkeypatch.setattr(EarthLocation, "_site_registry", None) + with set_temp_cache(tmp_path): + yield - # The JSON file contains `name` and `aliases` for each site, and astropy - # should use names from both, but not empty strings [#12721]. - assert "" not in names - assert "Royal Observatory Greenwich" in names - with pytest.raises( - KeyError, - match="Site 'nonexistent' not in database. Use the 'names' attribute to see", - ): - reg["nonexistent"] - - with pytest.raises( - KeyError, - match="Site 'kec' not in database. Use the 'names' attribute to see available", - ): - reg["kec"] +@pytest.fixture(scope="function") +def refreshable_site_data(monkeypatch, tmp_path): + with SITE_DATA_LOCK: + monkeypatch.setattr(EarthLocation, "_site_registry", None) + shutil.copytree(TEST_SITE_DATA_DIR, tmp_path, dirs_exist_ok=True) + with set_temp_cache(tmp_path): + yield @pytest.mark.usefixtures("local_site_data") @@ -91,23 +76,47 @@ def test_Earthlocation_refresh_cache_is_mandatory_kwarg(class_method, args): class_method(*args, False) -@pytest.mark.xfail(reason="regression test for #18572", raises=AssertionError) +@pytest.mark.remote_data(source="astropy") +@pytest.mark.usefixtures("remote_site_data") +def test_Earthlocation_get_site_names_empty_cache(): + assert len(EarthLocation.get_site_names()) > 3 + + @pytest.mark.usefixtures("local_site_data") +def test_Earthlocation_get_site_names_no_refresh_cache(): + assert EarthLocation.get_site_names() == [ + "Royal Observatory Greenwich", + "example_site", + "greenwich", + ] + + +@pytest.mark.remote_data(source="astropy") +@pytest.mark.usefixtures("refreshable_site_data") def test_Earthlocation_get_site_names_refresh_cache(): assert len(EarthLocation.get_site_names()) == 3 # Regression test for #18572 - refresh_cache=True had no effect assert len(EarthLocation.get_site_names(refresh_cache=True)) > 3 +@pytest.mark.remote_data(source="astropy") +@pytest.mark.usefixtures("remote_site_data") +def test_Earthlocation_of_site_empty_cache(): + keck = EarthLocation.of_site("keck") + assert_quantity_allclose(keck.lon, -155.47833333 * u.deg) + + @pytest.mark.usefixtures("local_site_data") def test_Earthlocation_of_site_no_refresh_cache(): with pytest.raises(UnknownSiteException, match="^Site 'keck' not in database"): EarthLocation.of_site("keck") -@pytest.mark.xfail(reason="regression test for #18572", raises=UnknownSiteException) -@pytest.mark.usefixtures("local_site_data") +@pytest.mark.remote_data(source="astropy") +@pytest.mark.usefixtures("refreshable_site_data") def test_Earthlocation_of_site_refresh_cache(): + with pytest.raises(UnknownSiteException, match="^Site 'keck' not in database"): + EarthLocation.of_site("keck") # Regression test for #18572 - refresh_cache=True had no effect keck = EarthLocation.of_site("keck", refresh_cache=True) assert_quantity_allclose(keck.lon, -155.47833333 * u.deg) diff --git a/docs/changes/coordinates/18687.bugfix.rst b/docs/changes/coordinates/18687.bugfix.rst new file mode 100644 index 000000000000..97ed51c5743d --- /dev/null +++ b/docs/changes/coordinates/18687.bugfix.rst @@ -0,0 +1,2 @@ +The ``refresh_cache`` parameter of ``EarthLocation.of_site()`` and +``EarthLocation.get_site_names()`` methods now works. diff --git a/docs/coordinates/index.rst b/docs/coordinates/index.rst index a0ad276ef128..9d9c14be18eb 100644 --- a/docs/coordinates/index.rst +++ b/docs/coordinates/index.rst @@ -6,7 +6,7 @@ .. testsetup:: >>> from astropy.coordinates import EarthLocation - >>> EarthLocation._get_site_registry(force_download=True) #doctest: +REMOTE_DATA +IGNORE_OUTPUT + >>> EarthLocation._get_site_registry(refresh_cache=True) #doctest: +REMOTE_DATA +IGNORE_OUTPUT .. _astropy-coordinates: From 8dc995ece07921f703e2f09b9b15a1c43e02927a Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:30:15 -0500 Subject: [PATCH 034/199] Backport PR #19075: Hide original exception when failing to match a site name --- astropy/coordinates/earth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astropy/coordinates/earth.py b/astropy/coordinates/earth.py index a54281b9f762..77069810e24d 100644 --- a/astropy/coordinates/earth.py +++ b/astropy/coordinates/earth.py @@ -390,7 +390,7 @@ class = EarthLocation except UnknownSiteException as e: raise UnknownSiteException( e.site, "EarthLocation.get_site_names", close_names=e.close_names - ) from e + ) from None if cls is el.__class__: return el From 9ff52daf6fb23c93950f918a1096e8de7b75c957 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 22 Dec 2025 12:43:25 -0500 Subject: [PATCH 035/199] Backport PR #19085: DOC: Add nitpick-exceptions for numpy 2.4 inherited docs --- docs/nitpick-exceptions | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/nitpick-exceptions b/docs/nitpick-exceptions index b05dfb61d34a..2658d709051d 100644 --- a/docs/nitpick-exceptions +++ b/docs/nitpick-exceptions @@ -56,8 +56,10 @@ py:class astropy.wcs.wcsapi.fitswcs.custom_ctype_to_ucd_mapping py:obj dtype py:obj a py:obj n +py:obj v py:obj ndarray py:obj args +py:obj numpy._typing.ArrayLike # other classes and functions that cannot be linked to py:class xmlrpc.client.Error From 4528454eb7728181678645e61e950b04608da207 Mon Sep 17 00:00:00 2001 From: Pey Lian Lim <2090236+pllim@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:26:03 -0500 Subject: [PATCH 036/199] TST: Pin pyparsing for mpl380 and oldestdeps jobs --- pyproject.toml | 1 + tox.ini | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 943a3c43b25f..1b694d6597ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ dependencies = [ recommended = [ "scipy>=1.9.2", "matplotlib>=3.8.0", + "pyparsing>=2.4.0", "narwhals>=1.42.0", # keep in sync with dependency-groups.dataframe ] # Optional IPython-related behavior is in many places in Astropy. IPython is a complex diff --git a/tox.ini b/tox.ini index a795523bdbbb..c6fdb2e12335 100644 --- a/tox.ini +++ b/tox.ini @@ -66,7 +66,9 @@ deps = numpy124: numpy==1.24.* numpy125: numpy==1.25.* + # matplotlib 3.8 + pyparsing 3.3 = DeprecationWarning mpl380: matplotlib==3.8.0 + mpl380: pyparsing==3.2.5 image: latex image, devinfra: scipy From 6ef6500a544a810a091a9144ac4389f10e89a3bb Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Fri, 26 Dec 2025 12:38:54 -0500 Subject: [PATCH 037/199] Backport PR #19094: DOC: fix duplicated 'the the' typos --- astropy/io/misc/pyarrow/csv.py | 2 +- astropy/io/votable/data/VOTable.v1.5.xsd | 2 +- docs/io/fits/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/astropy/io/misc/pyarrow/csv.py b/astropy/io/misc/pyarrow/csv.py index 4bbf949fd4e2..910b37d26ca0 100644 --- a/astropy/io/misc/pyarrow/csv.py +++ b/astropy/io/misc/pyarrow/csv.py @@ -46,7 +46,7 @@ def read_csv( This function allows highly performant reading of text CSV files into an astropy ``Table`` using `PyArrow `_. The best performance is achieved for files with only numeric data types, but even for - files with mixed data types, the performance is still better than the the standard + files with mixed data types, the performance is still better than the standard ``astropy.io.ascii`` fast CSV reader. By default, empty values (zero-length string "") in the CSV file are read as masked diff --git a/astropy/io/votable/data/VOTable.v1.5.xsd b/astropy/io/votable/data/VOTable.v1.5.xsd index 84a7cba03712..f59d8b9e76e1 100644 --- a/astropy/io/votable/data/VOTable.v1.5.xsd +++ b/astropy/io/votable/data/VOTable.v1.5.xsd @@ -255,7 +255,7 @@ This is a time origin of a time coordinate, given as a - Julian Date for the the time scale and reference point + Julian Date for the time scale and reference point defined. It is usually given as a floating point literal; for convenience, the magic strings “MJD-origin” (standing for 2400000.5) and “JD-origin” (standing for 0) diff --git a/docs/io/fits/index.rst b/docs/io/fits/index.rst index e139dd4d2dfb..b6ac1ee6966c 100644 --- a/docs/io/fits/index.rst +++ b/docs/io/fits/index.rst @@ -410,7 +410,7 @@ Working with Image Data This section describes reading and writing image data in the FITS format using the `~astropy.io.fits` package directly. For CCD image data with a unit, you should consider using the :ref:`Unified Image Data` interface with the - :ref:`CCDData class `. This provides the the capability to load data, + :ref:`CCDData class `. This provides the capability to load data, uncertainty and mask from a multi-extension FITS (MEF) file. If an HDU's data is an image, the data attribute of the HDU object will return From 379c10f08a947b5b2b8757d7e05935d0bcd2a403 Mon Sep 17 00:00:00 2001 From: Kyle Oman Date: Sat, 27 Dec 2025 19:39:40 +0000 Subject: [PATCH 038/199] Backport PR #19055: Handle `np.average` with different shape weights and input array and returning sum of weights --- .../units/quantity_helper/function_helpers.py | 32 ++++++++++++++++++- .../units/tests/test_quantity_non_ufuncs.py | 18 +++++++++++ docs/changes/units/19055.bugfix.rst | 3 ++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 docs/changes/units/19055.bugfix.rst diff --git a/astropy/units/quantity_helper/function_helpers.py b/astropy/units/quantity_helper/function_helpers.py index 29d958cfd5be..08e7f31e4f2c 100644 --- a/astropy/units/quantity_helper/function_helpers.py +++ b/astropy/units/quantity_helper/function_helpers.py @@ -103,7 +103,7 @@ np.round, np.around, np.fix, np.angle, np.i0, np.clip, np.isposinf, np.isneginf, np.isreal, np.iscomplex, - np.average, np.mean, np.std, np.var, np.trace, + np.mean, np.std, np.var, np.trace, np.nanmax, np.nanmin, np.nanargmin, np.nanargmax, np.nanmean, np.nansum, np.nancumsum, np.nanprod, np.nancumprod, np.einsum_path, np.linspace, @@ -361,6 +361,7 @@ def putmask_impl(a, /, mask, values): @function_helper def putmask(a, /, mask, values): return putmask_impl(a, mask=mask, values=values) + else: @function_helper @@ -470,6 +471,31 @@ def _quantities2arrays(*args, unit_from_first=False): return arrays, q.unit +@function_helper +def average(a, axis=None, weights=None, returned=False, *, keepdims=np._NoValue): + # This override should no longer be needed once + # https://github.com/numpy/numpy/pull/30522 is merged (likely for + # "not NUMPY_LT_2_4_1"). + + a_value, a_unit = (_a := _as_quantity(a)).value, _a.unit + w_value, w_unit = ( + (None, dimensionless_unscaled) + if weights is None + else ((_w := _as_quantity(weights)).value, _w.unit) + ) + return ( + (a_value,), + { + "axis": axis, + "weights": w_value, + "returned": returned, + "keepdims": keepdims, + }, + ((a_unit, w_unit) if returned else a_unit), + None, + ) + + def _iterable_helper(*args, out=None, **kwargs): """Convert arguments to Quantity, and treat possible 'out'.""" if out is not None: @@ -489,6 +515,7 @@ def concatenate(arrays, axis=0, out=None, **kwargs): *arrays, out=out, axis=axis, **kwargs ) return (arrays,), kwargs, unit, out + else: @function_helper @@ -630,6 +657,7 @@ def arange_impl(start_or_stop, /, *, stop, step, dtype, device=None): @function_helper(helps={np.empty, np.ones, np.zeros}) def creation_helper(shape, dtype=None, order="C"): return (shape, dtype, order), {}, UNIT_FROM_LIKE_ARG, None + else: @function_helper(helps={np.empty, np.ones, np.zeros}) @@ -642,6 +670,7 @@ def creation_helper(shape, dtype=None, order="C", *, device=None): @function_helper def full(shape, fill_value, dtype=None, order="C"): return full_impl(shape, fill_value, dtype, order) + else: @function_helper @@ -679,6 +708,7 @@ def array( ndmin=ndmin, ndmax=ndmax, ) + else: @function_helper diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index 785663b843ab..5452bb21d2c7 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -1118,6 +1118,24 @@ def test_average(self): expected = np.average(q1.value, weights=q2.value) * u.m assert np.all(o == expected) + def test_average_no_weights(self): + q1 = np.arange(9.0).reshape(3, 3) * u.m + o = np.average(q1) + expected = np.average(q1.value) * u.m + assert np.all(o == expected) + + def test_average_a_and_weights_different_shapes_and_returned(self): + q1 = np.arange(9.0).reshape(3, 3) * u.m + q2 = np.ones(3) / u.s + avg, wsum = np.average(q1, weights=q2, axis=1, returned=True) + expected_avg, expected_wsum = np.average( + q1.value, weights=q2.value, axis=1, returned=True + ) + expected_avg *= u.m + expected_wsum /= u.s + assert np.all(avg == expected_avg) + assert np.all(wsum == expected_wsum) + def test_mean(self): self.check(np.mean) diff --git a/docs/changes/units/19055.bugfix.rst b/docs/changes/units/19055.bugfix.rst new file mode 100644 index 000000000000..c658508f0e62 --- /dev/null +++ b/docs/changes/units/19055.bugfix.rst @@ -0,0 +1,3 @@ +Fixed a bug in the ``np.average`` function when weights with units and a +different shape to the input array were passed and the optionally-returned sum +of the weights was requested. The sum of the weights now has correct units. From 8cbce2ef037c8b159b05d62a98929599c53f5bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 29 Dec 2025 19:48:54 +0100 Subject: [PATCH 039/199] Backport PR #19103: Fix redundant asarray helps and update for NumPy 2.0+ signatures --- astropy/units/quantity_helper/function_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astropy/units/quantity_helper/function_helpers.py b/astropy/units/quantity_helper/function_helpers.py index 08e7f31e4f2c..395acb641f09 100644 --- a/astropy/units/quantity_helper/function_helpers.py +++ b/astropy/units/quantity_helper/function_helpers.py @@ -730,12 +730,12 @@ def array_impl(object, *, dtype, copy, order, subok, ndmin, ndmax=0): if NUMPY_LT_2_0: asarray_impl_1_helps = {np.asarray, np.asanyarray} - asarray_impl_2_helps = {} + asarray_impl_2_helps = set() elif NUMPY_LT_2_1: asarray_impl_1_helps = {np.asanyarray} asarray_impl_2_helps = {np.asarray} else: - asarray_impl_1_helps = {} + asarray_impl_1_helps = set() asarray_impl_2_helps = {np.asarray, np.asanyarray} From fa32459da2bc7a366771bafa8f8fbb4676744fd8 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:40:34 -0500 Subject: [PATCH 040/199] Backport PR #19098: MAINT: move np.average to subclass safe for numpy>=2.4.1 --- .../units/quantity_helper/function_helpers.py | 47 ++++++++++--------- astropy/utils/compat/numpycompat.py | 2 + 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/astropy/units/quantity_helper/function_helpers.py b/astropy/units/quantity_helper/function_helpers.py index 395acb641f09..cb4296c542ef 100644 --- a/astropy/units/quantity_helper/function_helpers.py +++ b/astropy/units/quantity_helper/function_helpers.py @@ -48,6 +48,7 @@ NUMPY_LT_2_1, NUMPY_LT_2_2, NUMPY_LT_2_4, + NUMPY_LT_2_4_1, ) if NUMPY_LT_2_0: @@ -146,6 +147,8 @@ SUBCLASS_SAFE_FUNCTIONS |= {np.trapezoid} if not NUMPY_LT_2_1: SUBCLASS_SAFE_FUNCTIONS |= {np.unstack, np.cumulative_prod, np.cumulative_sum} +if not NUMPY_LT_2_4_1: + SUBCLASS_SAFE_FUNCTIONS |= {np.average} # Implemented as methods on Quantity: # np.ediff1d is from setops, but we support it anyway; the others @@ -471,29 +474,27 @@ def _quantities2arrays(*args, unit_from_first=False): return arrays, q.unit -@function_helper -def average(a, axis=None, weights=None, returned=False, *, keepdims=np._NoValue): - # This override should no longer be needed once - # https://github.com/numpy/numpy/pull/30522 is merged (likely for - # "not NUMPY_LT_2_4_1"). - - a_value, a_unit = (_a := _as_quantity(a)).value, _a.unit - w_value, w_unit = ( - (None, dimensionless_unscaled) - if weights is None - else ((_w := _as_quantity(weights)).value, _w.unit) - ) - return ( - (a_value,), - { - "axis": axis, - "weights": w_value, - "returned": returned, - "keepdims": keepdims, - }, - ((a_unit, w_unit) if returned else a_unit), - None, - ) +if NUMPY_LT_2_4_1: + + @function_helper + def average(a, axis=None, weights=None, returned=False, *, keepdims=np._NoValue): + a_value, a_unit = (_a := _as_quantity(a)).value, _a.unit + w_value, w_unit = ( + (None, dimensionless_unscaled) + if weights is None + else ((_w := _as_quantity(weights)).value, _w.unit) + ) + return ( + (a_value,), + { + "axis": axis, + "weights": w_value, + "returned": returned, + "keepdims": keepdims, + }, + ((a_unit, w_unit) if returned else a_unit), + None, + ) def _iterable_helper(*args, out=None, **kwargs): diff --git a/astropy/utils/compat/numpycompat.py b/astropy/utils/compat/numpycompat.py index f089ec39589d..770f3efe9389 100644 --- a/astropy/utils/compat/numpycompat.py +++ b/astropy/utils/compat/numpycompat.py @@ -17,6 +17,7 @@ "NUMPY_LT_2_2", "NUMPY_LT_2_3", "NUMPY_LT_2_4", + "NUMPY_LT_2_4_1", ] # TODO: It might also be nice to have aliases to these named for specific @@ -29,6 +30,7 @@ NUMPY_LT_2_2 = not minversion(np, "2.2.0.dev0") NUMPY_LT_2_3 = not minversion(np, "2.3.0.dev0") NUMPY_LT_2_4 = not minversion(np, "2.4.0.dev0") +NUMPY_LT_2_4_1 = not minversion(np, "2.4.1.dev0") COPY_IF_NEEDED = False if NUMPY_LT_2_0 else None From 01be612ab0222df74de0fe0f84ef4e4cf6022e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Fri, 2 Jan 2026 09:47:20 +0100 Subject: [PATCH 041/199] Backport PR #19117: Update minimum required version of astropy-iers-data --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b694d6597ba..c78fb7468482 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ keywords = [ dependencies = [ "numpy>=1.24", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires - "astropy-iers-data>=0.2025.12.1.0.45.12", + "astropy-iers-data>=0.2025.12.29.0.42.34", "PyYAML>=6.0.0", "packaging>=22.0.0", ] From c71b2fcce017ebd1976c9d63fd14b6edefc047d8 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Fri, 2 Jan 2026 13:45:26 -0500 Subject: [PATCH 042/199] Backport PR #19109: TST: Remove self from test function --- astropy/io/fits/tests/test_connect.py | 29 ++++++++++++++++++++++----- astropy/io/misc/tests/test_hdf5.py | 14 ++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/astropy/io/fits/tests/test_connect.py b/astropy/io/fits/tests/test_connect.py index ee7d3ae22777..af08b8a43dcc 100644 --- a/astropy/io/fits/tests/test_connect.py +++ b/astropy/io/fits/tests/test_connect.py @@ -1036,13 +1036,32 @@ def test_fits_mixins_per_column(table_cls, name_col, tmp_path): @pytest.mark.parametrize("name_col", unsupported_cols.items()) -@pytest.mark.xfail(reason="column type unsupported") -def test_fits_unsupported_mixin(self, name_col, tmp_path): - # Check that we actually fail in writing unsupported columns defined - # on top. +def test_fits_unsupported_mixin(name_col, tmp_path): + """Check that we actually fail in writing unsupported columns defined + on top. + """ filename = tmp_path / "test_simple.fits" name, col = name_col - Table([col], names=[name]).write(filename, format="fits") + t = Table([col], names=[name]) + if isinstance(col, Time): + # Time with different locations can in principle be supported, but + # not if there is another time column which has yet another location. + t["t2"] = Time(col.jd, format="jd", location=None) + ctx = pytest.warns( + AstropyUserWarning, match="Time Column .* has no specified location" + ) + elif name == "obj": + ctx = pytest.raises( + TypeError, match="Column .* contains unsupported object types" + ) + else: + ctx = pytest.warns( + AstropyUserWarning, + match="The unit .* could not be saved in native FITS format", + ) + + with ctx: + t.write(filename, format="fits", overwrite=True) def test_info_attributes_with_no_mixins(tmp_path): diff --git a/astropy/io/misc/tests/test_hdf5.py b/astropy/io/misc/tests/test_hdf5.py index 1a73f2cbfb49..629510b53a34 100644 --- a/astropy/io/misc/tests/test_hdf5.py +++ b/astropy/io/misc/tests/test_hdf5.py @@ -801,16 +801,20 @@ def test_hdf5_mixins_per_column(table_cls, name_col, tmp_path): assert t2[name]._time.jd2.__class__ is np.ndarray +@pytest.mark.skipif(not HAS_H5PY, reason="requires h5py") @pytest.mark.parametrize("name_col", unsupported_cols.items()) -@pytest.mark.xfail(reason="column type unsupported") -def test_fits_unsupported_mixin(self, name_col, tmp_path): +def test_fits_unsupported_mixin(name_col, tmp_path): # Check that we actually fail in writing unsupported columns defined # on top. filename = tmp_path / "test_simple.fits" name, col = name_col - Table([col], names=[name]).write( - filename, format="hdf5", path="root", serialize_meta=True - ) + t = Table([col], names=[name]) + with pytest.raises( + TypeError, match="Object dtype .* has no native HDF5 equivalent" + ): + t.write( + filename, format="hdf5", path="root", serialize_meta=True, overwrite=True + ) @pytest.mark.skipif(not HAS_H5PY, reason="requires h5py") From ce927ace4d85a08e06ed0507ef35a409d718e247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 8 Jan 2026 20:47:57 +0100 Subject: [PATCH 043/199] Backport PR #19136: DOC: Remove unnecessary doctest-skip --- docs/modeling/new-fitter.rst | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/modeling/new-fitter.rst b/docs/modeling/new-fitter.rst index 0b21f5794276..9e26b95234c0 100644 --- a/docs/modeling/new-fitter.rst +++ b/docs/modeling/new-fitter.rst @@ -64,18 +64,14 @@ entry points can be found in `setuptools' documentation Date: Tue, 13 Jan 2026 15:44:44 -0500 Subject: [PATCH 044/199] Backport PR #19129: BUG: avoid mutation of shape attibutes (modeling) --- astropy/modeling/core.py | 7 ++++--- astropy/modeling/projections.py | 4 ++-- astropy/modeling/rotations.py | 6 ++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/astropy/modeling/core.py b/astropy/modeling/core.py index e11f0f0ad6e6..3894fb1747c8 100644 --- a/astropy/modeling/core.py +++ b/astropy/modeling/core.py @@ -2742,9 +2742,10 @@ def _array_to_parameters(self): param_metrics = self._param_metrics for name in self.param_names: param = getattr(self, name) - value = self._parameters[param_metrics[name]["slice"]] - value.shape = param_metrics[name]["shape"] - param.value = value + param.value = np.reshape( + self._parameters[param_metrics[name]["slice"]], + param_metrics[name]["shape"], + ) def _check_param_broadcast(self, max_ndim): """ diff --git a/astropy/modeling/projections.py b/astropy/modeling/projections.py index fb9a57a05ef1..c660362db741 100644 --- a/astropy/modeling/projections.py +++ b/astropy/modeling/projections.py @@ -1627,8 +1627,8 @@ def evaluate(cls, x, y, matrix, translation): augmented_matrix = cls._create_augmented_matrix(matrix, translation) result = np.dot(augmented_matrix, inarr) - x, y = result[0], result[1] - x.shape = y.shape = shape + # Project by taking just x and y of the result. + x, y, _ = np.reshape(result, (3, *shape)) return x, y diff --git a/astropy/modeling/rotations.py b/astropy/modeling/rotations.py index 316b893ba5de..3b9755820eb1 100644 --- a/astropy/modeling/rotations.py +++ b/astropy/modeling/rotations.py @@ -132,8 +132,7 @@ def evaluate(self, x, y, z, angles): orig_shape = x.shape or (1,) inarr = np.array([x.ravel(), y.ravel(), z.ravel()]) result = np.dot(_create_matrix(angles[0], self.axes_order), inarr) - x, y, z = result[0], result[1], result[2] - x.shape = y.shape = z.shape = orig_shape + x, y, z = np.reshape(result, (3, *orig_shape)) return x, y, z @@ -544,8 +543,7 @@ def evaluate(cls, x, y, angle): if isinstance(angle, u.Quantity): angle = angle.to_value(u.rad) result = np.dot(cls._compute_matrix(angle), inarr) - x, y = result[0], result[1] - x.shape = y.shape = orig_shape + x, y = np.reshape(result, (2, *orig_shape)) if has_units: return u.Quantity(x, unit=x_unit, subok=True), u.Quantity( y, unit=y_unit, subok=True From 19b2a09db9421c508b33817951cb6a5bf2896713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Wed, 14 Jan 2026 15:40:51 +0100 Subject: [PATCH 045/199] Merge pull request #19131 from neutrinoceros/uncertainty/19126 (cherry picked from commit a99d1985cdb8b64810e0700d4af0b38d1955083d) --- astropy/uncertainty/core.py | 6 +++++- astropy/utils/compat/numpycompat.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/astropy/uncertainty/core.py b/astropy/uncertainty/core.py index 7f73b6226cf9..6d95dd26c128 100644 --- a/astropy/uncertainty/core.py +++ b/astropy/uncertainty/core.py @@ -10,6 +10,7 @@ from astropy import stats from astropy import units as u +from astropy.utils.compat import NUMPY_LT_2_5 from astropy.utils.compat.numpycompat import NUMPY_LT_2_0 if NUMPY_LT_2_0: @@ -102,7 +103,10 @@ def __new__(cls, samples): # Set our new structured dtype. structured.dtype = new_dtype # Get rid of trailing dimension of 1. - structured.shape = samples.shape[:-1] + if NUMPY_LT_2_5: + structured.shape = samples.shape[:-1] + else: + structured._set_shape(samples.shape[:-1]) # Now view as the Distribution subclass, and finalize based on the # original samples (e.g., to set the unit for QuantityDistribution). diff --git a/astropy/utils/compat/numpycompat.py b/astropy/utils/compat/numpycompat.py index 770f3efe9389..91cbc3805225 100644 --- a/astropy/utils/compat/numpycompat.py +++ b/astropy/utils/compat/numpycompat.py @@ -18,6 +18,7 @@ "NUMPY_LT_2_3", "NUMPY_LT_2_4", "NUMPY_LT_2_4_1", + "NUMPY_LT_2_5", ] # TODO: It might also be nice to have aliases to these named for specific @@ -31,6 +32,7 @@ NUMPY_LT_2_3 = not minversion(np, "2.3.0.dev0") NUMPY_LT_2_4 = not minversion(np, "2.4.0.dev0") NUMPY_LT_2_4_1 = not minversion(np, "2.4.1.dev0") +NUMPY_LT_2_5 = not minversion(np, "2.5.0.dev0") COPY_IF_NEEDED = False if NUMPY_LT_2_0 else None From c7f59e590585650b68b0a0fbf4323f16fc82afe0 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:27:42 -0500 Subject: [PATCH 046/199] Backport PR #19152: BUG: avoid mutation of shape attibutes (modeling, 2/2) --- astropy/modeling/parameters.py | 2 +- astropy/modeling/tests/test_constraints.py | 6 ++---- astropy/modeling/tests/test_models.py | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/astropy/modeling/parameters.py b/astropy/modeling/parameters.py index be70fee0fc45..396d9e65df5e 100644 --- a/astropy/modeling/parameters.py +++ b/astropy/modeling/parameters.py @@ -465,7 +465,7 @@ def shape(self, value): if value not in ((), (1,)): raise ValueError("Cannot assign this shape to a scalar quantity") else: - self.value.shape = value + self.value = np.reshape(self.value, value) @property def size(self): diff --git a/astropy/modeling/tests/test_constraints.py b/astropy/modeling/tests/test_constraints.py index 867482295195..d576430ba53c 100644 --- a/astropy/modeling/tests/test_constraints.py +++ b/astropy/modeling/tests/test_constraints.py @@ -586,12 +586,10 @@ def test_2d_model(fitter): Y = np.linspace(-1, 7, 200) x, y = np.meshgrid(X, Y) z = gauss2d(x, y) - w = np.ones(x.size) - w.shape = x.shape + w = np.ones(x.shape) with NumpyRNGContext(1234567890): - n = np.random.randn(x.size) - n.shape = x.shape + n = np.random.randn(*x.shape) m = fitter(gauss2d, x, y, z + 2 * n, weights=w) assert_allclose(m.parameters, gauss2d.parameters, rtol=0.05) m = fitter(gauss2d, x, y, z + 2 * n, weights=None) diff --git a/astropy/modeling/tests/test_models.py b/astropy/modeling/tests/test_models.py index 40567f829c13..427f4a304b54 100644 --- a/astropy/modeling/tests/test_models.py +++ b/astropy/modeling/tests/test_models.py @@ -118,8 +118,8 @@ def test_inconsistent_input_shapes(): # check scalar input broadcasting works assert np.abs(g(x, 0) - g(x, 0 * x)).sum() == 0 # and that array broadcasting works - x.shape = (10, 1) - y.shape = (1, 10) + x = x.reshape((10, 1)) + y = y.reshape((1, 10)) result = g(x, y) assert result.shape == (10, 10) # incompatible shapes do _not_ work From 80556a34c627042665f2b4fa78f2a6532da8b698 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:24:43 -0500 Subject: [PATCH 047/199] Backport PR #19128: BUG: avoid deprecated interface for mutating shape attibutes (io.fits) --- astropy/io/fits/tests/test_image.py | 15 ++++++++++++++- astropy/io/fits/util.py | 6 +++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/astropy/io/fits/tests/test_image.py b/astropy/io/fits/tests/test_image.py index 0d7b2d868035..2e6c1b31e310 100644 --- a/astropy/io/fits/tests/test_image.py +++ b/astropy/io/fits/tests/test_image.py @@ -9,6 +9,7 @@ from numpy.testing import assert_equal from astropy.io import fits +from astropy.utils.compat import NUMPY_LT_2_5 from astropy.utils.data import get_pkg_data_filename from astropy.utils.exceptions import AstropyUserWarning @@ -977,7 +978,12 @@ def test_open_scaled_in_update_mode(self): # Try reshaping the data, then closing and reopening the file; let's # see if all the changes are preserved properly - hdul[0].data.shape = (42, 10) + if NUMPY_LT_2_5: + hdul[0].data.shape = (42, 10) + else: + # ndarray._set_shape is semi-private, but the only + # non deprecated, strict semantic equivalent in Numpy 2.5+ + hdul[0].data._set_shape((42, 10)) hdul.close() hdul = fits.open(testfile) @@ -988,6 +994,13 @@ def test_open_scaled_in_update_mode(self): assert "BSCALE" not in hdul[0].header hdul.close() + with fits.open(testfile, mode="update") as hdul: + # Try reshaping the data again, this time using np.reshape + hdul[0].data = np.reshape(hdul[0].data, (21, 20)) + + with fits.open(testfile) as hdul: + assert hdul[0].shape == (21, 20) + def test_scale_back(self): """A simple test for https://aeon.stsci.edu/ssb/trac/pyfits/ticket/120 diff --git a/astropy/io/fits/util.py b/astropy/io/fits/util.py index 14b353e78d72..ad74e20ab6bc 100644 --- a/astropy/io/fits/util.py +++ b/astropy/io/fits/util.py @@ -20,6 +20,7 @@ from packaging.version import Version from astropy.utils import data +from astropy.utils.compat import NUMPY_LT_2_5 from astropy.utils.compat.optional_deps import HAS_DASK from astropy.utils.exceptions import AstropyUserWarning @@ -870,7 +871,10 @@ def _rstrip_inplace(array): # Note: the code will work if this fails; the chunks will just be larger. if b.ndim > 2: try: - b.shape = -1, b.shape[-1] + if NUMPY_LT_2_5: + b.shape = -1, b.shape[-1] + else: + b._set_shape((-1, b.shape[-1])) except AttributeError: # can occur for non-contiguous arrays pass for j in range(0, b.shape[0], bufsize): From c45154bf68072f1cd5189a2ff7152f950e839449 Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:54:11 +0100 Subject: [PATCH 048/199] Backport PR #19153 on branch v7.2.x (Fix testing pickle protocol versions in table unit tests) (#19155) * Backport PR #19153: Fix testing pickle protocol versions in table unit tests * Delete docs/changes/table/19153.bugfix.rst --------- Co-authored-by: Evan Chen Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- astropy/table/tests/test_pickle.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/astropy/table/tests/test_pickle.py b/astropy/table/tests/test_pickle.py index 7335be6ae6d3..6e31e6de3f8e 100644 --- a/astropy/table/tests/test_pickle.py +++ b/astropy/table/tests/test_pickle.py @@ -18,7 +18,7 @@ def test_pickle_column(protocol): unit="cm", meta={"a": 1}, ) - cs = pickle.dumps(c) + cs = pickle.dumps(c, protocol=protocol) cp = pickle.loads(cs) assert np.all(cp == c) assert cp.attrs_equal(c) @@ -38,7 +38,7 @@ def test_pickle_masked_column(protocol): c.mask[1] = True c.fill_value = -99 - cs = pickle.dumps(c) + cs = pickle.dumps(c, protocol=protocol) cp = pickle.loads(cs) assert np.all(cp._data == c._data) @@ -54,7 +54,7 @@ def test_pickle_multidimensional_column(protocol): a = np.zeros((3, 2)) c = Column(a, name="a") - cs = pickle.dumps(c) + cs = pickle.dumps(c, protocol=protocol) cp = pickle.loads(cs) assert np.all(c == cp) @@ -87,7 +87,7 @@ def test_pickle_table(protocol): t["d"] = Time(["2001-01-02T12:34:56", "2001-02-03T00:01:02"]) t["e"] = SkyCoord([125.0, 180.0] * deg, [-45.0, 36.5] * deg) - ts = pickle.dumps(t) + ts = pickle.dumps(t, protocol=protocol) tp = pickle.loads(ts) assert tp.__class__ is table_class @@ -129,7 +129,7 @@ def test_pickle_masked_table(protocol): t["a"].mask[1] = True t["a"].fill_value = -99 - ts = pickle.dumps(t) + ts = pickle.dumps(t, protocol=protocol) tp = pickle.loads(ts) for colname in ("a", "b"): @@ -148,7 +148,7 @@ def test_pickle_indexed_table(protocol): t = simple_table() t.add_index("a") t.add_index(["a", "b"]) - ts = pickle.dumps(t) + ts = pickle.dumps(t, protocol=protocol) tp = pickle.loads(ts) assert len(t.indices) == len(tp.indices) From a261254f82400da44cb07feba5efd599061e89d1 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Thu, 15 Jan 2026 16:14:09 -0500 Subject: [PATCH 049/199] Backport PR #19142: Fix `PicklingError` when pickling `Masked` subclasses with initialized `info` attribute --- astropy/table/tests/test_pickle.py | 25 ++++++++++++- astropy/utils/masked/core.py | 15 +++++--- .../masked/tests/test_dynamic_subclasses.py | 10 ++++- astropy/utils/masked/tests/test_pickle.py | 37 +++++++++++++++++++ docs/changes/utils/19142.bugfix.rst | 1 + 5 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 astropy/utils/masked/tests/test_pickle.py create mode 100644 docs/changes/utils/19142.bugfix.rst diff --git a/astropy/table/tests/test_pickle.py b/astropy/table/tests/test_pickle.py index 6e31e6de3f8e..8c670ff66101 100644 --- a/astropy/table/tests/test_pickle.py +++ b/astropy/table/tests/test_pickle.py @@ -2,7 +2,7 @@ import numpy as np -from astropy.coordinates import SkyCoord +from astropy.coordinates import Angle, SkyCoord from astropy.table import Column, MaskedColumn, QTable, Table from astropy.table.table_helpers import simple_table from astropy.time import Time @@ -141,6 +141,29 @@ def test_pickle_masked_table(protocol): assert tp.meta == t.meta +def test_pickle_masked_qtable(protocol): + t = QTable(meta={"a": 1}, masked=True) + t["a"] = Quantity([1, 2], unit="m") + t["b"] = Angle([1, 2], unit=deg) + + t["a"].mask[0] = True + t["b"].mask[1] = True + + ts = pickle.dumps(t, protocol=protocol) + tp = pickle.loads(ts) + + assert tp.__class__ is QTable + + assert np.all(tp["a"].mask == t["a"].mask) + assert np.all(tp["a"].unmasked == t["a"].unmasked) + assert np.all(tp["b"].mask == t["b"].mask) + assert np.all(tp["b"].unmasked == t["b"].unmasked) + assert type(tp["a"]) is type(t["a"]) # nopep8 + assert type(tp["b"]) is type(t["b"]) # nopep8 + assert tp.meta == t.meta + assert type(tp) is type(t) + + def test_pickle_indexed_table(protocol): """ Ensure that any indices that have been added will survive pickling. diff --git a/astropy/utils/masked/core.py b/astropy/utils/masked/core.py index b87ac39ce429..4ad48e26fc8c 100644 --- a/astropy/utils/masked/core.py +++ b/astropy/utils/masked/core.py @@ -618,10 +618,13 @@ def __new__(newcls, *args, mask=None, **kwargs): if "info" not in cls.__dict__ and hasattr(cls._data_cls, "info"): data_info = cls._data_cls.info attr_names = data_info.attr_names | {"serialize_method"} + # Ensure the new info class uses the class's module. + # Without this, the DataInfoMeta metaclass incorrectly sets + # __module__ to its own. new_info = type( cls.__name__ + "Info", (MaskedArraySubclassInfo, data_info.__class__), - dict(attr_names=attr_names), + dict(attr_names=attr_names, __module__=cls.__module__), ) cls.info = new_info() @@ -1430,13 +1433,14 @@ def __repr__(self): def __getattr__(key): - """Make commonly used Masked subclasses importable for ASDF support. + """Make commonly used Masked subclasses and their info classes importable for ASDF support. Registered types associated with ASDF converters must be importable by their fully qualified name. Masked classes are dynamically created and have apparent names like ``astropy.utils.masked.core.MaskedQuantity`` although they aren't actually attributes of this module. Customize module attribute - lookup so that certain commonly used Masked classes are importable. + lookup so that certain commonly used Masked classes are importable. The same + is done for their dynamically created info classes. See: - https://asdf.readthedocs.io/en/latest/asdf/extending/converters.html#entry-point-performance-considerations @@ -1450,13 +1454,14 @@ def __getattr__(key): base_class_name = key[len(Masked.__name__) :] for base_class_qualname in __construct_mixin_classes: module, _, name = base_class_qualname.rpartition(".") - if name == base_class_name: + is_info = name + "Info" == base_class_name + if name == base_class_name or is_info: base_class = getattr(importlib.import_module(module), name) # Try creating the masked class masked_class = Masked(base_class) # But only return it if it is a standard one, not one # where we just used the ndarray fallback. if base_class in Masked._masked_classes: - return masked_class + return masked_class.info.__class__ if is_info else masked_class raise AttributeError(f"module '{__name__}' has no attribute '{key}'") diff --git a/astropy/utils/masked/tests/test_dynamic_subclasses.py b/astropy/utils/masked/tests/test_dynamic_subclasses.py index f05a632ec9c3..972b09ac9177 100644 --- a/astropy/utils/masked/tests/test_dynamic_subclasses.py +++ b/astropy/utils/masked/tests/test_dynamic_subclasses.py @@ -1,5 +1,5 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst -"""Test importability of common Masked classes.""" +"""Test importability of common Masked classes and their info classes.""" import pytest @@ -11,3 +11,11 @@ @pytest.mark.parametrize("base_class", [Quantity, Angle, Latitude, Longitude]) def test_importable(base_class): assert getattr(core, f"Masked{base_class.__name__}") is core.Masked(base_class) + + +@pytest.mark.parametrize("base_class", [Quantity, Angle, Latitude, Longitude]) +def test_importable_info(base_class): + assert ( + getattr(core, f"Masked{base_class.__name__}Info") + is core.Masked(base_class).info.__class__ + ) diff --git a/astropy/utils/masked/tests/test_pickle.py b/astropy/utils/masked/tests/test_pickle.py new file mode 100644 index 000000000000..1429a91fbd43 --- /dev/null +++ b/astropy/utils/masked/tests/test_pickle.py @@ -0,0 +1,37 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +import pickle + +import numpy as np +import pytest + +import astropy.units as u +from astropy.coordinates import Angle, Latitude, Longitude +from astropy.units import Quantity +from astropy.utils.masked import Masked + + +@pytest.mark.parametrize( + "data", + [ + Quantity([1, 2, 3], u.m), + Angle([1, 2, 3], u.deg), + Latitude([1, 2, 3], u.deg), + Longitude([1, 2, 3], u.deg), + ], +) +def test_masked_pickle(data): + mask = [True, False, False] + m = Masked(data, mask=mask) + + # Force creation of the info object (see #19142) + assert m.info is not None + + m2 = pickle.loads(pickle.dumps(m)) + + np.testing.assert_array_equal(m.unmasked, m2.unmasked) + np.testing.assert_array_equal(m.mask, m2.mask) + + assert m.unit == m2.unit + assert type(m) is type(m2) + assert type(m.info) is type(m2.info) diff --git a/docs/changes/utils/19142.bugfix.rst b/docs/changes/utils/19142.bugfix.rst new file mode 100644 index 000000000000..ddbd2191c3cc --- /dev/null +++ b/docs/changes/utils/19142.bugfix.rst @@ -0,0 +1 @@ +Pickling ``Masked`` subclasses with an initialized ``info`` attribute no longer fails. From 7edc2bb31936e676750519b5297a2de07e49dc65 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Fri, 16 Jan 2026 09:23:06 -0500 Subject: [PATCH 050/199] Backport PR #19127: BUG: avoid deprecated shape mutating APIs (coordinates) --- .../builtin_frames/fk4_fk5_transforms.py | 2 +- astropy/coordinates/representation/base.py | 23 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/astropy/coordinates/builtin_frames/fk4_fk5_transforms.py b/astropy/coordinates/builtin_frames/fk4_fk5_transforms.py index f87737bd0d21..1fd2d86fd023 100644 --- a/astropy/coordinates/builtin_frames/fk4_fk5_transforms.py +++ b/astropy/coordinates/builtin_frames/fk4_fk5_transforms.py @@ -42,7 +42,7 @@ def _fk4_B_matrix(obstime): T = (obstime.jyear - 1950.0) / 100.0 if getattr(T, "shape", ()): # Ensure we broadcast possibly arrays of times properly. - T.shape += (1, 1) + T = np.reshape(T, (*T.shape, 1, 1)) return _B1950_TO_J2000_M + _FK4_CORR * T diff --git a/astropy/coordinates/representation/base.py b/astropy/coordinates/representation/base.py index 043f65eda251..3ed4bea53387 100644 --- a/astropy/coordinates/representation/base.py +++ b/astropy/coordinates/representation/base.py @@ -12,6 +12,7 @@ import astropy.units as u from astropy.coordinates.angles import Angle from astropy.utils import classproperty +from astropy.utils.compat import NUMPY_LT_2_5 from astropy.utils.data_info import MixinInfo from astropy.utils.decorators import deprecated from astropy.utils.exceptions import DuplicateRepresentationWarning @@ -461,15 +462,23 @@ def shape(self, shape): oldshape = self.shape for component in self.components: val = getattr(self, component) - if val.size > 1: - try: + if val.size <= 1: + continue + + try: + if NUMPY_LT_2_5: val.shape = shape - except Exception: - for val2 in reshaped: - val2.shape = oldshape - raise else: - reshaped.append(val) + val._set_shape(shape) + except Exception: + for val2 in reshaped: + if NUMPY_LT_2_5: + val2.shape = oldshape + else: + val2._set_shape(oldshape) + raise + else: + reshaped.append(val) @property def masked(self): From 9216eb295e33fd5419a1deb199aa10771c273bc3 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Fri, 16 Jan 2026 10:38:57 -0500 Subject: [PATCH 051/199] Backport PR #19163: TST: selectively ignore deprecation warnings for numpy.fix (deprecated in 2.5dev) --- astropy/units/tests/test_quantity_non_ufuncs.py | 1 + astropy/utils/masked/tests/test_function_helpers.py | 1 + 2 files changed, 2 insertions(+) diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index 5452bb21d2c7..d25fe49d5ef4 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -943,6 +943,7 @@ def test_round_(self): def test_around(self): self.check(np.around) + @pytest.mark.filterwarnings("ignore:numpy.fix is deprecated:DeprecationWarning") def test_fix(self): self.check(np.fix) diff --git a/astropy/utils/masked/tests/test_function_helpers.py b/astropy/utils/masked/tests/test_function_helpers.py index acec5f225a5e..04aca6ee30bb 100644 --- a/astropy/utils/masked/tests/test_function_helpers.py +++ b/astropy/utils/masked/tests/test_function_helpers.py @@ -739,6 +739,7 @@ def test_var(self): class TestUfuncLike(InvariantMaskTestSetup): + @pytest.mark.filterwarnings("ignore:numpy.fix is deprecated:DeprecationWarning") def test_fix(self): self.check(np.fix) # Check np.fix with out argument for completeness From d8ee750ae05d0e6e4ccd28e8cc842ee8700ad0b9 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Mon, 19 Jan 2026 09:47:11 -0500 Subject: [PATCH 052/199] Merge pull request #19158 from neutrinoceros/utils.masked/19126 BUG/TST: avoid (or accept) NumPy deprecation warnings for mutating shape attributes in utils.masked (cherry picked from commit 7cc5a2c0c7bb0fd0ecf89bbf12315184c89a4d94) --- astropy/utils/masked/function_helpers.py | 6 +++++- astropy/utils/masked/tests/test_masked.py | 20 +++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/astropy/utils/masked/function_helpers.py b/astropy/utils/masked/function_helpers.py index c8aebdc3f833..786d50b8a0c5 100644 --- a/astropy/utils/masked/function_helpers.py +++ b/astropy/utils/masked/function_helpers.py @@ -24,6 +24,7 @@ import numpy._core as np_core from numpy.lib._function_base_impl import _quantile_is_valid, _ureduce +from astropy.utils.compat import NUMPY_LT_2_5 # This module should not really be imported, but we define __all__ # such that sphinx can typeset the functions with docstrings. @@ -1563,7 +1564,10 @@ def in1d(ar1, ar2, assume_unique=False, invert=False, *, kind=None): def isin(element, test_elements, assume_unique=False, invert=False, *, kind=None): element = np.asanyarray(element) result = _in1d(element, test_elements, assume_unique, invert, kind=kind) - result.shape = element.shape + if NUMPY_LT_2_5: + result.shape = element.shape + else: + result._set_shape(element.shape) return result, _copy_of_mask(element), None diff --git a/astropy/utils/masked/tests/test_masked.py b/astropy/utils/masked/tests/test_masked.py index f06cba7192eb..f2123cd2ccf6 100644 --- a/astropy/utils/masked/tests/test_masked.py +++ b/astropy/utils/masked/tests/test_masked.py @@ -14,7 +14,7 @@ from astropy import units as u from astropy.coordinates import Longitude from astropy.units import Quantity -from astropy.utils.compat import NUMPY_LT_2_0, NUMPY_LT_2_2, NUMPY_LT_2_3 +from astropy.utils.compat import NUMPY_LT_2_0, NUMPY_LT_2_1, NUMPY_LT_2_2, NUMPY_LT_2_3 from astropy.utils.compat.optional_deps import HAS_PLT from astropy.utils.masked import Masked, MaskedNDArray @@ -304,8 +304,11 @@ def test_viewing(self): def test_viewing_independent_shape(self): mms = Masked(self.a, mask=self.m) - mms2 = mms.view() - mms2.shape = mms2.shape[::-1] + if NUMPY_LT_2_1: + mms2 = mms.view() + mms2.shape = mms2.shape[::-1] + else: + mms2 = np.reshape(mms, mms.shape[::-1], copy=False) assert mms2.shape == mms.shape[::-1] assert mms2.mask.shape == mms.shape[::-1] # This should not affect the original array! @@ -563,14 +566,21 @@ def test_reshape(self): assert_array_equal(ma_reshape.mask, expected_mask) def test_shape_setting(self): - ma_reshape = self.ma.copy() - ma_reshape.shape = (6,) + if NUMPY_LT_2_1: + ma_reshape = self.ma.copy() + ma_reshape.shape = (6,) + else: + ma_reshape = np.reshape(self.ma, (6,), copy=True) + expected_data = self.a.reshape((6,)) expected_mask = self.mask_a.reshape((6,)) assert ma_reshape.shape == expected_data.shape assert_array_equal(ma_reshape.unmasked, expected_data) assert_array_equal(ma_reshape.mask, expected_mask) + @pytest.mark.filterwarnings( + "default:Setting the shape on a NumPy array has been deprecated in NumPy 2.5:DeprecationWarning" + ) def test_shape_setting_failure(self): ma = self.ma.copy() with pytest.raises(ValueError, match="cannot reshape"): From 24bd57452fbcfe5e5aa6eae31f659a40614d6883 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:24:48 -0500 Subject: [PATCH 053/199] Backport PR #19189: Bugfix for Parameter.value erroring if internal value state is None. --- astropy/modeling/parameters.py | 2 +- astropy/modeling/tests/test_parameters.py | 14 ++++++++++++++ docs/changes/modeling/19189.bugfix.rst | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 docs/changes/modeling/19189.bugfix.rst diff --git a/astropy/modeling/parameters.py b/astropy/modeling/parameters.py index 396d9e65df5e..4637dcfd05c5 100644 --- a/astropy/modeling/parameters.py +++ b/astropy/modeling/parameters.py @@ -354,7 +354,7 @@ def value(self): else: value = self._getter(self._internal_value) - if value.size == 1: + if value is not None and value.size == 1: # return scalar number as np.float64 object return np.float64(value.item()) diff --git a/astropy/modeling/tests/test_parameters.py b/astropy/modeling/tests/test_parameters.py index 4077586cee98..4231d22dcc6f 100644 --- a/astropy/modeling/tests/test_parameters.py +++ b/astropy/modeling/tests/test_parameters.py @@ -1248,3 +1248,17 @@ def test_setter(): for x, y in pars: np.testing.assert_almost_equal(model(x, y), (x + 1) ** 2 + (y - np.pi * 3) ** 2) + + +def test_none_parameter(): + """ + When Parameter internal value is None, the value property should also be + NaN. + + See issue #19188 + """ + param = Parameter() + # Internal value is None + assert param._value is None + + assert np.isnan(param.value) diff --git a/docs/changes/modeling/19189.bugfix.rst b/docs/changes/modeling/19189.bugfix.rst new file mode 100644 index 000000000000..2983132014bf --- /dev/null +++ b/docs/changes/modeling/19189.bugfix.rst @@ -0,0 +1 @@ +Bugfix for ``Parameter.value`` accessor when the parameter value has not been set yet. From 9770833dab0d1b6ea5dc1fd3daf63543b19f7dce Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:01:15 -0500 Subject: [PATCH 054/199] Backport PR #19193: DOC: Pin sphinx<9 to fix RTD build --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c78fb7468482..3f0d99d27ed9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,7 @@ typing = [ ] docs = [ "astropy[recommended]", # installs the [recommended] dependencies - "sphinx>=8.2.0", # keep in sync with docs/conf.py + "sphinx>=8.2.0,<9", # keep in sync with docs/conf.py "sphinx-astropy[confv2]>=1.9.1", "pytest>=8.0.0", "sphinx-changelog>=1.2.0", From 0e4b8fae6730c01c12e369d2fe3cc699efb93562 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:39:16 -0500 Subject: [PATCH 055/199] Backport PR #19195: TST: Update mpldev hashes --- .../figures/py311-test-image-mpldev-cov.json | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/astropy/tests/figures/py311-test-image-mpldev-cov.json b/astropy/tests/figures/py311-test-image-mpldev-cov.json index 5ef0af9c9369..886491a2af98 100644 --- a/astropy/tests/figures/py311-test-image-mpldev-cov.json +++ b/astropy/tests/figures/py311-test-image-mpldev-cov.json @@ -1,45 +1,45 @@ { - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "d79ed179997672de0a929aedee3c2ac590e04b544558808ac9bace8937a60275", - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_rectangular": "30e13643c770a26b2707745143f50a735daa6f37a6a8258e733ae35338a4a1bb", - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_nonrectangular": "37de34740cef2897effc9de5e6726ef3955a3449d0fa6a929350301500557b8e", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "0c259f5278851bda21016cce3d5479e1d6202762587ad8d8ff9982fa5a9ce11a", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_rectangular": "d9ad505b7455d6f3a1305273547b177e58d1f36418f5983d0faad29ddd5134a2", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_nonrectangular": "0c1528f0ac4c30b66b0f427851dd82ae6fca2982db5c86dd20166318e2271c5d", "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_change_wcs": "c68e961f0a21cc0dcc43c523cee1c564d94bd96c795f976d51e5198fc52f83cc", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tight_layout": "80f167d1d14c8c22f1ffc2f403951e60397a4dcf35b9ecfd3ebba297d3fe0ff7", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tight_layout": "ab936ab9b81dc3b35acea5257e108ac0071aec2225cefffa096b1873e650f2f8", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_image_plot": "4fe4c89d089e8f1f9584584826f25d3c884d4914f76a863e1a624f9ef2295b07", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axes_off": "3580258468fc0ff0fa6d140299b79f1f05c16c3e222447de38228e01983fbdd1", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[True]": "5364e39cab14f28b08073b31cce3dae8501c0431b1a49985810b764902747c8e", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[False]": "ee1e46856a19ba472ae9ea4588c614fbbd1b3a4a0787ef40e33f991a8f79d6c0", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[line]": "516c3882bbaa31a0d241eec2c70aca45b27b44929662371718b10025735db772", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contour_overlay": "8679305e8a8b5b3f5b896343f4e16dfc65e9289c31505e3b35ef773d7837c8ea", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contourf_overlay": "fa928ab57291e8a128009320323ffb8955cd75d28b2a589074defb7b0889b3eb", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_overlay_features_image": "30d5562cb7a2484db0b898bc886253829de884fbf92bc0e6575f091438ec5f32", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_curvilinear_grid_patches_image": "ad0a985a324a73b5bea839a231f7537075306c874c279c6a6ea2cfe16529d815", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image": "822d96a93eb4ab59b1a0095384bb315b025b481a18821b3c558e4a937c77f489", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image_lonlat": "224f7e2f7d106ab028584bdc51c73296e277a211b7f3ed36a0452eb4842afa5b", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_coord": "9352d254af8add2918f98fb8a8d6d220bb95ab8096a1c56584d6c80ff3d5de39", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scatter_coord": "9352d254af8add2918f98fb8a8d6d220bb95ab8096a1c56584d6c80ff3d5de39", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_text_coord": "c6e7575becb6381f45fe549ea7415bd038778406e43b44e8a72f8c6bce82e466", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_line": "74ce73bcbb1dd170ae36d38a1ad4a62c626517a84e05922dab0f989a12a27758", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_changed_axis_units": "056ec7123c67fa7ee6269bec47641bfe4932630a79d02029e71164b522fadacd", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_minor_ticks": "5224f8fa725901ff74a4558987ceaae0363f98894d2c6a12d8c557db500ff7fe", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_labels": "f4944296c37a6e8ea9d30126709da3567a25908c1665e8cad10e894dd87cd54d", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contour_overlay": "3df6a162520b4ac6ec4727bb58bad168852c5b037b9202af0d85ae4997c8954e", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contourf_overlay": "5a68cd27e0cace22f2501d5a3c784f71b18e5f9f409e04ef8738761b0845c2d7", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_overlay_features_image": "1e0c92a5d43d935e7d497ae79b54681bb58b480782b412c5a521da66e928bca7", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_curvilinear_grid_patches_image": "32b60fc009c2acdfb11d2d522d883d8893943d0458f513684038aca28e64e406", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image": "f740f21fd0af4e502753934add6abcbc68ff3b73428b3d8eec1132d34086595b", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image_lonlat": "96b021d7f89817c3ca3f0f2c0528658c9df095c113e0b4a2330ef5e321f03b9b", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_coord": "142be2288f9a08595e022627e8f2b00596123c7cd7304c6bde9e09b5b3212f44", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scatter_coord": "142be2288f9a08595e022627e8f2b00596123c7cd7304c6bde9e09b5b3212f44", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_text_coord": "4e81a420d4e4e4ed080c61027d0aa5198bf015ea17455a874f3fe78e4b268672", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_line": "c0163e76c738b41274185756370d1502bf968f500a283dc76122d258426fe96a", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_changed_axis_units": "75a85cc15104030366272835729af049bdd54ac49575159cbed30c15d249e065", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_minor_ticks": "e35de8268a042d86016cc684f441c96ab0ba374af78c44c99c4ddfdfb9aa4a25", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_labels": "a494be95719c60530307a288b109e9a8d4da96d295e8b9891f8f3e162a3ce64d", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_no_ticks": "5836f6d36ce6ce89156b251055e5b00b6063a35bff4b7ca1b2da725da14a67ad", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_rcparams": "1af0c66ba343df4f03efc18e707f5b0c2a54dbce7351e0f9024154937a30b371", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_rcparams": "1437e6206edf7bc906382558097c2da346812d4ba184157d3b1c8f5b9c5e1262", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles": "11ef218080920301ada1ef9cea558599ca110ba49e3dc4e9dc547c013b87fcc7", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles_non_square_axes": "2ffe1157db233bc80e0eda08a30b916cc56a8ff0a7f34c3a0a1b1037695fe1a5", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_set_coord_type": "ee5e873fd467292f7e76f0890051f33762b399fa3936357797e7338f80e914eb", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_regression": "d295b08d88c6f8cd0eb56f9aa9c1892b6e72e6819a0f5b6265f3a29acb0a1545", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axislabels_regression": "431b65a14f107dd47c3ea56240b92e246ba25be682f6487c8e8565b78f1fc0c8", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_noncelestial_angular": "cafe2fe3ca40e358ad8e92040c698c1e7f2913968f226a12b635fc47c0acc063", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_regression": "c06679408a13b816854a17b962cf4ec2afcef2a917c62fc4c0e68894967be754", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axislabels_regression": "d34dac4a594e8c4597f8cebb19921d815e1384c41a53d2455a94fb9664b9fcc2", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_noncelestial_angular": "4e20c0fe1dcc7c89aa0774512e4782be7e5cc42493dbaa8917555fcf7e82da57", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_patches_distortion": "704af668b56837628f11b3dedbba60c0d11fda2e896ddbba9dee0e8acb5b99d7", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_quadrangle": "0223dba7d207f37c644c5c0860f6ef18b11b8013438030c89d88d700ea2ed437", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_args": "5bb6436777ab08013e747807f7db670b068229d6fb2b6b250787e44eaac36dab", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_header": "d7309b91db792bd42fdccd53ebe0738611c7cec56adb2972f0d684d83c8c75f6", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scalebar": "29931954f6c7c5131a63bdfabbe4c220ee255d2efde756e31977d13a3c69af98", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_args": "59a354f169ba1084b590aacddc6fc57f41223fab43b2f3c71f53872b83673be9", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_header": "11383bf77fda831fae02beae555038a6f7cff027119c547bac1644c36fc038f6", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scalebar": "034ae9301a5ad448d803d7b28435c4fb98b5e45731ccd3375798a0b2896ca8a3", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_elliptical_frame": "3aee6e8bcb0b1283993d6743683f13077aab179a85540e3dcb301fef8c66aaf7", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_hms_labels": "1047e944e3cb798a39702be44c9612cb27a26e4fd246a5cf0abe047a51985d5c", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_latex_labels": "22c900ff73a60d58fbc29ee61485cad76aa483e3c8efa89bffbdf6d660d0bf9f", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_latex_labels": "2bfbb32d3ac53f781ca901bb3c4630f9ae99ae54ad3cac2b7ffe8863980e7e2e", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_params": "78bdce5072b3e9ac87b7e76832f3fc889c115aef3f894004a50b7eeba0d32ebd", - "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs": "190a14699654f7eae01556013f4fee1bbcddd3b34e62e9e3d92a92f7ce9a7695", + "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs": "f8976c6c777c720a5de21f76fa5a9c39d24ff70e7f11ce75a2a5e56daba4bd30", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs_format_unit": "e0eea94f0ca0e37e96d903c3f441ecf0eb20b6870bfbd8a3426ad3e8305d1cee", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs_get_transform": "73e65438281efd42cda9e459467b18f7695aa50589782d0ce0cfbd298b69cb14", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_2d_wcs_correlated": "c2fd3acaa4739844288e1ce23dcb0a70b71aaf190d85f4a9164e7ab90836a76b", @@ -48,15 +48,15 @@ "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices0-hpln]": "60c967eef03cc2f038d7915f1b988dfa60852fa7b950b83c6d3f49054862d5d1", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices1-hplt]": "0a14e5153eb91e2a3a96501f02a21581effc7d6b7ae31e29a402a3abf30d5b12", "astropy.visualization.wcsaxes.tests.test_images.test_allsky_labels_wrap": "8908818b526d4a506505da890eff47121b23ee41656dc15dff3e19f7d03094af", - "astropy.visualization.wcsaxes.tests.test_images.test_tickable_gridlines": "9d91bcf571af6dcc1425b6dfd76fb92e3e0180fb9fd0c852f9eba3fb06e079fc", - "astropy.visualization.wcsaxes.tests.test_images.test_overlay_nondegree_unit": "aa9b85520da54dc61d4db2988883ff184fb59cd27c52a04899cb33599fcab4b7", - "astropy.visualization.wcsaxes.tests.test_images.test_nosimplify": "565b8bf068323147ae0fcc72263f258c552285aadcf2fc67786818fccc0b812d", - "astropy.visualization.wcsaxes.tests.test_images.test_custom_formatter": "e04eb07365c6bbc4774a20150c13dadcf72b9c5a664805aff2fffd106fdecd87", - "astropy.visualization.wcsaxes.tests.test_images.test_equatorial_arcsec": "2e2ad683a3acdda19dd4353654fa6d399f4b5cd528cd41591a7bcba8776a67ce", - "astropy.visualization.wcsaxes.tests.test_images.test_wcs_preserve_units": "464a0503099d3f7381479ddbba0e417011adb6e24301692056b124d608b1067b", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay": "0a87473ff8e5b5610f4adac1518c608afcd2178b25584fb42f77bcc594c4f47a", + "astropy.visualization.wcsaxes.tests.test_images.test_tickable_gridlines": "b01476e68e63a8e0b80ffd533968081518c5eda059274a104e73658eb0cdf2db", + "astropy.visualization.wcsaxes.tests.test_images.test_overlay_nondegree_unit": "2bbb8832852f6c5ea95839cdeed778dc1b1b41cc6d6d2efa7266dadc3edab146", + "astropy.visualization.wcsaxes.tests.test_images.test_nosimplify": "f88c0868cc16d98ee93baca3681d68ebb5a4c0a0ab82e75e9977cec1e92f4a73", + "astropy.visualization.wcsaxes.tests.test_images.test_custom_formatter": "00aa0ada490271baa37ce7f23d169a5871d7afba6647257fe212fe931c425d05", + "astropy.visualization.wcsaxes.tests.test_images.test_equatorial_arcsec": "d30d80194f04319d38f65239d57cc300dc42c0678b433f71ffb74445c000dbbf", + "astropy.visualization.wcsaxes.tests.test_images.test_wcs_preserve_units": "60a979ca74d39b22e641dbc547c6f7d52777702412aac6824f4f6ec176aba960", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay": "1f025403a8df3208ace222556e78fd67a74a5bbcb7d8bcd70ad0913c52a52a27", "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay_auto_coord_meta": "2f737bb70fb1a5452cb0efa79010376614dc559e9aff607f638f044ac6b04448", "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_direct_init": "1f24c5243bfdf0f30e88afc4f2f5d66db85954cea50a2910af94975ff8765b45", - "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_5d_with_names": "c90ae6f3b0f9f407ca7a866fa648dc3ef4bea78369315292bc82f16104ed5736", - "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_2d_celestial_arcsec": "4b743d645a85d7516decbcf4a831c127af5a1800072597c2a1299d17fb186adb" + "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_5d_with_names": "135f6e8530ab0de32fe1a5907ba9a4f9aad43335e5a4e029962c2611e4334723", + "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_2d_celestial_arcsec": "b9d44e1b54ff14cc214aac132372bdebdd6893a8c5faceed2e09620674bc7e5e" } From c8b3d5f084625fbde15a5c296ee09d11d073e01a Mon Sep 17 00:00:00 2001 From: Surya-k-eng Date: Wed, 14 Jan 2026 21:45:12 +0530 Subject: [PATCH 056/199] Backport PR #19145: DOC: Rearrange badges in README as suggested in #19073 --- README.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 79a3c3dd48be..6e668e88cd7e 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,8 @@ |Astropy Logo| ----- - -|Actions Status| |CircleCI Status| |Coverage Status| |PyPI Status| |Supported Python Versions| |Documentation Status| |Pre-Commit| |Ruff| |Zenodo| - ----- +:Versions: |Zenodo| |PyPI Status| |Supported Python Versions| +:Status: |Coverage Status| |Actions Status| |CircleCI Status| |Documentation Status| +:Tools: |Pre-Commit| |Ruff| The Astropy Project is a community effort to develop a single core package for astronomy in Python and foster interoperability between From 74cb67b92cd7309abc85ed944c5e3687c70e3dcc Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:37:16 -0500 Subject: [PATCH 057/199] Backport PR #19065: DOC: Add pyOpenSci badge --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 6e668e88cd7e..f54057ae8ee3 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,7 @@ :Versions: |Zenodo| |PyPI Status| |Supported Python Versions| :Status: |Coverage Status| |Actions Status| |CircleCI Status| |Documentation Status| :Tools: |Pre-Commit| |Ruff| +:Community: |pyOpenSci Peer-Reviewed| The Astropy Project is a community effort to develop a single core package for astronomy in Python and foster interoperability between @@ -133,3 +134,7 @@ Astropy is licensed under a 3-clause BSD style license - see the .. |Codespaces| image:: https://github.com/codespaces/badge.svg :target: https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=2081289 :alt: Open in GitHub Codespaces + +.. |pyOpenSci Peer-Reviewed| image:: https://pyopensci.org/badges/peer-reviewed.svg + :target: https://github.com/pyOpenSci/software-review/issues/251 + :alt: pyOpenSci Peer-Reviewed From 8fabbebb329ae4b4446852bca4e04c39830a83f4 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:05:47 -0500 Subject: [PATCH 058/199] Backport PR #19187: BUG: avoid deprecated shape mutation APIs in coordinates (2) --- astropy/coordinates/solar_system.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/astropy/coordinates/solar_system.py b/astropy/coordinates/solar_system.py index c1b50889b5fc..f2c0e1d663a6 100644 --- a/astropy/coordinates/solar_system.py +++ b/astropy/coordinates/solar_system.py @@ -319,7 +319,9 @@ def _get_body_barycentric_posvel(body, time, ephemeris=None, get_velocity=True): ): body_p_or_v += p_or_v - body_posvel_bary.shape = body_posvel_bary.shape[:2] + jd1_shape + body_posvel_bary = np.reshape( + body_posvel_bary, body_posvel_bary.shape[:2] + jd1_shape + ) body_pos_bary = CartesianRepresentation( body_posvel_bary[0], unit=u.km, copy=False ) From b19e0e59b04afcdaaecbdcb74a2c950dde1ee691 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Thu, 22 Jan 2026 14:23:38 -0500 Subject: [PATCH 059/199] Merge pull request #19182 from neutrinoceros/time/19126 BUG: avoid deprecated shape mutation APIs in time (cherry picked from commit 081b25b00ffc41cd4ee728f8011dcaef5cd56d2e) --- astropy/time/core.py | 23 +++++++++++++++-------- astropy/time/tests/test_methods.py | 23 +++++++++++++++++------ astropy/utils/masked/core.py | 20 ++++++++++++++++---- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/astropy/time/core.py b/astropy/time/core.py index 52d476d222da..5037db2b59e4 100644 --- a/astropy/time/core.py +++ b/astropy/time/core.py @@ -27,7 +27,7 @@ from astropy.extern import _strptime from astropy.units import UnitConversionError from astropy.utils import lazyproperty -from astropy.utils.compat import COPY_IF_NEEDED +from astropy.utils.compat import COPY_IF_NEEDED, NUMPY_LT_2_5 from astropy.utils.data_info import MixinInfo, data_info_factory from astropy.utils.decorators import deprecated from astropy.utils.exceptions import AstropyDeprecationWarning, AstropyWarning @@ -926,15 +926,22 @@ def shape(self, shape): (self, "location"), ): val = getattr(obj, attr, None) - if val is not None and val.size > 1: - try: + if val is None or val.size <= 1: + continue + try: + if NUMPY_LT_2_5: val.shape = shape - except Exception: - for val2 in reshaped: - val2.shape = oldshape - raise else: - reshaped.append(val) + val._set_shape(shape) + except Exception: + for val2 in reshaped: + if NUMPY_LT_2_5: + val2.shape = oldshape + else: + val2._set_shape(oldshape) + raise + else: + reshaped.append(val) def _shaped_like_input(self, value): if self.masked: diff --git a/astropy/time/tests/test_methods.py b/astropy/time/tests/test_methods.py index f295a4978b96..8039974b3578 100644 --- a/astropy/time/tests/test_methods.py +++ b/astropy/time/tests/test_methods.py @@ -13,6 +13,7 @@ from astropy.time.utils import day_frac from astropy.units.quantity_helper.function_helpers import ARRAY_FUNCTION_ENABLED from astropy.utils import iers +from astropy.utils.compat import NUMPY_LT_2_5 from astropy.utils.exceptions import AstropyDeprecationWarning needs_array_function = pytest.mark.xfail( @@ -320,7 +321,7 @@ def test_shape_setting(self, use_mask): t0_reshape = self.t0.copy() mjd = t0_reshape.mjd # Creates a cache of the mjd attribute - t0_reshape.shape = (5, 2, 5) + t0_reshape = np.reshape(t0_reshape, (5, 2, 5)) assert t0_reshape.shape == (5, 2, 5) assert mjd.shape != t0_reshape.mjd.shape # Cache got cleared assert np.all(t0_reshape.jd1 == self.t0._time.jd1.reshape(5, 2, 5)) @@ -328,16 +329,26 @@ def test_shape_setting(self, use_mask): assert t0_reshape.location is None # But if the shape doesn't work, one should get an error. t0_reshape_t = t0_reshape.T - with pytest.raises(ValueError): - t0_reshape_t.shape = (12,) # Wrong number of elements. - with pytest.raises(AttributeError): - t0_reshape_t.shape = (10, 5) # Cannot be done without copy. + + if NUMPY_LT_2_5: + depr_warning_action = "error" + else: + depr_warning_action = "ignore" + with warnings.catch_warnings(): + warnings.simplefilter(depr_warning_action, DeprecationWarning) + with pytest.raises(ValueError): + t0_reshape_t.shape = (12,) # Wrong number of elements. + with pytest.raises((AttributeError, ValueError)): + # the exact exception type isn't really in our control, + # as it ultimately comes from numpy but depends on astropy's + # execution flow + t0_reshape_t.shape = (10, 5) # Cannot be done without copy. # check no shape was changed. assert t0_reshape_t.shape == t0_reshape.T.shape assert t0_reshape_t.jd1.shape == t0_reshape.T.shape assert t0_reshape_t.jd2.shape == t0_reshape.T.shape t1_reshape = self.t1.copy() - t1_reshape.shape = (2, 5, 5) + t1_reshape = np.reshape(t1_reshape, (2, 5, 5)) assert t1_reshape.shape == (2, 5, 5) assert np.all(t1_reshape.jd1 == self.t1.jd1.reshape(2, 5, 5)) # location is a single element, so its shape should not change. diff --git a/astropy/utils/masked/core.py b/astropy/utils/masked/core.py index 4ad48e26fc8c..8d69d2fde72f 100644 --- a/astropy/utils/masked/core.py +++ b/astropy/utils/masked/core.py @@ -22,7 +22,7 @@ import numpy as np -from astropy.utils.compat import COPY_IF_NEEDED, NUMPY_LT_2_0 +from astropy.utils.compat import COPY_IF_NEEDED, NUMPY_LT_2_0, NUMPY_LT_2_5 from astropy.utils.data_info import ParentDtypeInfo from astropy.utils.shapes import NDArrayShapeMethods, ShapedLikeNDArray @@ -744,14 +744,26 @@ def shape(self): @shape.setter def shape(self, shape): + return self._set_shape(shape) + + def _set_shape(self, shape): old_shape = self.shape - self._mask.shape = shape + if NUMPY_LT_2_5: + self._mask.shape = shape + else: + self._mask._set_shape(shape) # Reshape array proper in try/except just in case some broadcasting # or so causes it to fail. try: - super(MaskedNDArray, type(self)).shape.__set__(self, shape) + if NUMPY_LT_2_5: + super(MaskedNDArray, type(self)).shape.__set__(self, shape) + else: + super()._set_shape(shape) except Exception as exc: - self._mask.shape = old_shape + if NUMPY_LT_2_5: + self._mask.shape = old_shape + else: + self._mask._set_shape(old_shape) # Given that the mask reshaping succeeded, the only logical # reason for an exception is something like a broadcast error in # in __array_finalize__, or a different memory ordering between From bb6d39b4bd0ca2cf55c9f2c941bd32e4ae04452f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 26 Jan 2026 15:43:41 +0100 Subject: [PATCH 060/199] Backport PR #19218: BUG: a avoid deprecation warning from numpy.char.chararray (table) --- astropy/table/column.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astropy/table/column.py b/astropy/table/column.py index 78da49a126ba..01616468eca1 100644 --- a/astropy/table/column.py +++ b/astropy/table/column.py @@ -1153,7 +1153,7 @@ def _encode_str(value): def tolist(self): if self.dtype.kind == "S": - return np.char.chararray.decode(self, encoding="utf-8").tolist() + return np.char.decode(self, encoding="utf-8").tolist() else: return super().tolist() From faa571ab1741a0cab53590ce750f9be4793d3c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 26 Jan 2026 18:46:20 +0100 Subject: [PATCH 061/199] Backport PR #19219: TST/CLN: clean up a 10 y.o. comment in a test --- astropy/io/fits/tests/test_table.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/astropy/io/fits/tests/test_table.py b/astropy/io/fits/tests/test_table.py index 429868aee7b8..f3bae3f523a4 100644 --- a/astropy/io/fits/tests/test_table.py +++ b/astropy/io/fits/tests/test_table.py @@ -2803,13 +2803,7 @@ def test_update_string_column_inplace(self): with fits.open(self.temp("test2.fits"), mode="update") as hdul: assert hdul[1].header["TDIM1"] == "(3,3,2)" - # Note: Previously I wrote data['a'][0][1, 1] to address - # the single row. However, this is broken for chararray because - # data['a'][0] does *not* return a view of the original array--this - # is a bug in chararray though and not a bug in any FITS-specific - # code so we'll roll with it for now... - # (by the way the bug in question is fixed in newer Numpy versions) - hdul[1].data["a"][0, 1, 1] = "XYZ" + hdul[1].data["a"][0][1, 1] = "XYZ" assert np.all(hdul[1].data["a"][0] == expected) with fits.open(self.temp("test2.fits")) as hdul: From c4c20b29eea45c33922027185f125876b12f1f7c Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Mon, 26 Jan 2026 17:29:53 -0500 Subject: [PATCH 062/199] Merge pull request #19199 from mhvk/table-user-dtypes API,BUG: support user dtypes in table columns (cherry picked from commit 66a32acd10b97c1bac63b0e1abd826f692246216) --- astropy/table/operations.py | 8 +- astropy/table/tests/test_operations.py | 16 ++- astropy/utils/compat/optional_deps.py | 1 + astropy/utils/data_info.py | 2 +- astropy/utils/metadata/merge.py | 4 +- astropy/utils/metadata/tests/test_metadata.py | 117 ++++++++++++++++-- astropy/utils/metadata/utils.py | 51 ++++---- docs/changes/table/19199.bugfix.rst | 2 + pyproject.toml | 1 + tox.ini | 1 + 10 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 docs/changes/table/19199.bugfix.rst diff --git a/astropy/table/operations.py b/astropy/table/operations.py index 43a4409f3fdf..4846145df26e 100644 --- a/astropy/table/operations.py +++ b/astropy/table/operations.py @@ -1008,7 +1008,7 @@ def get_descrs(arrays, col_name_map): # Output dtype is the superset of all dtypes in in_arrays try: - dtype = common_dtype(in_cols) + dtype = result_type(in_cols) except TableMergeError as tme: # Beautify the error message when we are trying to merge columns with incompatible # types by including the name of the columns that originated the error. @@ -1030,7 +1030,7 @@ def get_descrs(arrays, col_name_map): return out_descrs -def common_dtype(cols): +def result_type(cols): """ Use numpy to find the common dtype for a list of columns. @@ -1038,7 +1038,7 @@ def common_dtype(cols): np.bool_, np.object_, np.number, np.character, np.void """ try: - return metadata.common_dtype(cols) + return metadata.utils.result_type(cols) except metadata.MergeConflictError as err: tme = TableMergeError(f"Columns have incompatible types {err._incompat_types}") tme._incompat_types = err._incompat_types @@ -1085,7 +1085,7 @@ def _get_join_sort_idxs(keys, left, right): sort_right[sort_key] = right_sort_col # Build up dtypes for the structured array that gets sorted. - dtype_str = common_dtype([left_sort_col, right_sort_col]) + dtype_str = result_type([left_sort_col, right_sort_col]) sort_keys_dtypes.append((sort_key, dtype_str)) ii += 1 diff --git a/astropy/table/tests/test_operations.py b/astropy/table/tests/test_operations.py index 505afc7929c0..5c0038ccb218 100644 --- a/astropy/table/tests/test_operations.py +++ b/astropy/table/tests/test_operations.py @@ -6,6 +6,7 @@ import numpy as np import pytest +from numpy.testing import assert_array_equal from astropy import table from astropy import units as u @@ -33,7 +34,7 @@ from astropy.units.quantity import Quantity from astropy.utils import metadata from astropy.utils.compat import NUMPY_LT_2_0 -from astropy.utils.compat.optional_deps import HAS_SCIPY +from astropy.utils.compat.optional_deps import HAS_NUMPY_QUADDTYPE, HAS_SCIPY from astropy.utils.masked import Masked from astropy.utils.metadata import MergeConflictError @@ -2625,3 +2626,16 @@ def test_empty_skycoord_vstack(): table2 = table.vstack([table1, table1]) # Used to fail. assert len(table2) == 0 assert isinstance(table2["foo"], SkyCoord) + + +@pytest.mark.skipif(not HAS_NUMPY_QUADDTYPE, reason="Tests QuadDtype") +def test_user_dtype_vstack(): + # Regression test for gh-19197 + from numpy_quaddtype import QuadPrecDType + + c = Column([1, 2], dtype=QuadPrecDType()) + 1e-25 + t = Table([c], names=["c"]) + t2 = table.vstack([t, t]) # This used to fail + assert t2["c"].dtype == c.dtype + assert_array_equal(t2[:2]["c"], c) + assert_array_equal(t2[2:]["c"], c) diff --git a/astropy/utils/compat/optional_deps.py b/astropy/utils/compat/optional_deps.py index 808a9eba20c2..505944640d94 100644 --- a/astropy/utils/compat/optional_deps.py +++ b/astropy/utils/compat/optional_deps.py @@ -32,6 +32,7 @@ "matplotlib", "mpmath", "narwhals", + "numpy_quaddtype", "pandas", "PIL", "polars", diff --git a/astropy/utils/data_info.py b/astropy/utils/data_info.py index 9aa85037bfa6..bf36975b93f9 100644 --- a/astropy/utils/data_info.py +++ b/astropy/utils/data_info.py @@ -738,7 +738,7 @@ def getattrs(col): ) # Output dtype is the superset of all dtypes in in_cols - out["dtype"] = metadata.common_dtype(cols) + out["dtype"] = metadata.utils.result_type(cols) # Make sure all input shapes are the same uniq_shapes = {col.shape[1:] for col in cols} diff --git a/astropy/utils/metadata/merge.py b/astropy/utils/metadata/merge.py index c7ee740fb861..ec21cada0347 100644 --- a/astropy/utils/metadata/merge.py +++ b/astropy/utils/metadata/merge.py @@ -8,7 +8,7 @@ import numpy as np from .exceptions import MergeConflictError, MergeConflictWarning -from .utils import common_dtype +from .utils import result_type __all__ = [ "MERGE_STRATEGIES", @@ -130,7 +130,7 @@ class MergeNpConcatenate(MergeStrategy): @classmethod def merge(cls, left, right): left, right = np.asanyarray(left), np.asanyarray(right) - common_dtype([left, right]) # Ensure left and right have compatible dtype + result_type([left, right]) # Ensure left and right have compatible dtype return np.concatenate([left, right]) diff --git a/astropy/utils/metadata/tests/test_metadata.py b/astropy/utils/metadata/tests/test_metadata.py index 900e109635df..436c2f602586 100644 --- a/astropy/utils/metadata/tests/test_metadata.py +++ b/astropy/utils/metadata/tests/test_metadata.py @@ -13,6 +13,7 @@ enable_merge_strategies, merge, ) +from astropy.utils.metadata.utils import result_type class OrderedDictSubclass(OrderedDict): @@ -244,23 +245,123 @@ class MergeConcatStrings(metadata.MergePlus): metadata.MERGE_STRATEGIES = original_merge_strategies -def test_common_dtype_string(): +def test_result_type_string(): u3 = np.array(["123"]) u4 = np.array(["1234"]) b3 = np.array([b"123"]) b5 = np.array([b"12345"]) - assert common_dtype([u3, u4]).endswith("U4") - assert common_dtype([b5, u4]).endswith("U5") - assert common_dtype([b3, b5]).endswith("S5") + assert result_type([u3, u4]) == np.dtype("U4") + assert result_type([b5, u4]) == np.dtype("U5") + assert result_type([b3, b5]) == np.dtype("S5") -def test_common_dtype_basic(): +def test_result_type_basic(): i8 = np.array(1, dtype=np.int64) f8 = np.array(1, dtype=np.float64) u3 = np.array("123") with pytest.raises(MergeConflictError): - common_dtype([i8, u3]) + result_type([i8, u3]) - assert common_dtype([i8, i8]).endswith("i8") - assert common_dtype([i8, f8]).endswith("f8") + assert result_type([i8, i8]) == np.dtype("i8") + assert result_type([i8, f8]) == np.dtype("f8") + + +@pytest.mark.parametrize("function", [result_type, common_dtype]) +def test_finding_common_type_exhaustive(function): + """ + Test that allowed combinations are those expected. + """ + dtype = [ + ("int", int), + ("uint8", np.uint8), + ("float32", np.float32), + ("float64", np.float64), + ("str", "S2"), + ("uni", "U2"), + ("bool", bool), + ("object", np.object_), + ] + arr = np.empty(1, dtype=dtype) + fail = set() + succeed = set() + for name1, _ in dtype: + for name2, _ in dtype: + try: + function([arr[name1], arr[name2]]) + succeed.add(f"{name1} {name2}") + except MergeConflictError: + fail.add(f"{name1} {name2}") + + # known bad combinations + bad = { + "str int", + "str bool", + "uint8 bool", + "uint8 str", + "object float32", + "bool object", + "uni uint8", + "int str", + "bool str", + "bool float64", + "bool uni", + "str float32", + "uni float64", + "uni object", + "bool uint8", + "object float64", + "float32 bool", + "str uint8", + "uni bool", + "float64 bool", + "float64 object", + "int bool", + "uni int", + "uint8 object", + "int uni", + "uint8 uni", + "float32 uni", + "object uni", + "bool float32", + "uni float32", + "object str", + "int object", + "str float64", + "object int", + "float64 uni", + "bool int", + "object bool", + "object uint8", + "float32 object", + "str object", + "float64 str", + "float32 str", + } + assert fail == bad + + good = { + "float64 int", + "int int", + "uint8 float64", + "uint8 int", + "str uni", + "float32 float32", + "float64 float64", + "float64 uint8", + "float64 float32", + "int uint8", + "int float32", + "uni str", + "int float64", + "uint8 float32", + "float32 int", + "float32 uint8", + "bool bool", + "uint8 uint8", + "str str", + "float32 float64", + "object object", + "uni uni", + } + assert succeed == good diff --git a/astropy/utils/metadata/utils.py b/astropy/utils/metadata/utils.py index 8f4f16b93732..19573d6462a6 100644 --- a/astropy/utils/metadata/utils.py +++ b/astropy/utils/metadata/utils.py @@ -3,8 +3,6 @@ import numpy as np -from astropy.utils.misc import dtype_bytes_or_chars - from .exceptions import MergeConflictError __all__ = ["common_dtype"] @@ -31,32 +29,41 @@ def common_dtype(arrs): dtype_str : str String representation of dytpe (dtype ``str`` attribute) """ + dt = result_type(arrs) + return dt.str if dt.names is None else dt.descr + + +def result_type(arrs): + """ + Use numpy to find the common type for a list of ndarray. + + The difference with `numpy.result_type` is that all arrays should + share the same fundamental numpy data type, one of: + ``np.bool_``, ``np.object_``, ``np.number``, ``np.character``, ``np.void`` + Hence, a mix like integer and string will raise instead of resulting + in a string type. + + Parameters + ---------- + arrs : list of ndarray + Array likes for which to find the common dtype. Anything not + an array (e.g, |Time|), will be considered an object array. + + Returns + ------- + dtype : ~numpy.dtype + The common type. + """ + dtypes = [dtype(arr) for arr in arrs] np_types = (np.bool_, np.object_, np.number, np.character, np.void) uniq_types = { - tuple(issubclass(dtype(arr).type, np_type) for np_type in np_types) - for arr in arrs + tuple(issubclass(dt.type, np_type) for np_type in np_types) for dt in dtypes } if len(uniq_types) > 1: # Embed into the exception the actual list of incompatible types. - incompat_types = [dtype(arr).name for arr in arrs] + incompat_types = [dt.name for dt in dtypes] tme = MergeConflictError(f"Arrays have incompatible types {incompat_types}") tme._incompat_types = incompat_types raise tme - arrs = [np.empty(1, dtype=dtype(arr)) for arr in arrs] - - # For string-type arrays need to explicitly fill in non-zero - # values or the final arr_common = .. step is unpredictable. - for i, arr in enumerate(arrs): - if arr.dtype.kind in ("S", "U"): - arrs[i] = [ - ("0" if arr.dtype.kind == "U" else b"0") - * dtype_bytes_or_chars(arr.dtype) - ] - - arr_common = np.array([arr[0] for arr in arrs]) - return ( - arr_common.dtype.str - if arr_common.dtype.names is None - else arr_common.dtype.descr - ) + return np.result_type(*dtypes) diff --git a/docs/changes/table/19199.bugfix.rst b/docs/changes/table/19199.bugfix.rst new file mode 100644 index 000000000000..140582a5f856 --- /dev/null +++ b/docs/changes/table/19199.bugfix.rst @@ -0,0 +1,2 @@ +Tables can now be joined and stacked also if they contain columns with +user-defined data types such as ``QuadPrecDtype``. diff --git a/pyproject.toml b/pyproject.toml index 3f0d99d27ed9..44b03403455f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,7 @@ test_all = [ "sgp4>=2.3", "array-api-strict>=1.0", "array-api-strict<2.4;python_version<'3.12'", # PYTHON_LT_3_12 + "numpy-quaddtype>=1.0", ] typing = [ "pandas-stubs>=2.0.0", diff --git a/tox.ini b/tox.ini index c6fdb2e12335..bf4d7856afda 100644 --- a/tox.ini +++ b/tox.ini @@ -105,6 +105,7 @@ deps = devdeps-!noscipy: sgp4>=2.3 devdeps-!noscipy: array-api-strict>=1.0 py311-devdeps-!noscipy: array-api-strict<2.4 + devdeps-!noscipy: numpy-quaddtype>=1.0 # The following indicates which [project.optional-dependencies] from pyproject.toml will be installed. # test_all does not work here due to upstream bug https://github.com/tox-dev/tox/issues/3433 From 1c76f918fbd2bc9d12bd412be9c02fa4e46cd0e1 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Sat, 31 Jan 2026 14:00:03 -0500 Subject: [PATCH 063/199] Backport PR #19240: DOC: fix spelling mistakes in comments --- astropy/utils/data.py | 2 +- astropy/wcs/tests/test_wcs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/astropy/utils/data.py b/astropy/utils/data.py index 3b437cf04d56..0c51448f2b5c 100644 --- a/astropy/utils/data.py +++ b/astropy/utils/data.py @@ -1235,7 +1235,7 @@ def _try_url_open( # Always try first with a secure connection # _build_urlopener uses lru_cache, so the ssl_context argument must be - # converted to a hashshable type (a set of 2-tuples) + # converted to a hashable type (a set of 2-tuples) ssl_context = frozenset(ssl_context.items() if ssl_context else []) urlopener = _build_urlopener( ftp_tls=ftp_tls, ssl_context=ssl_context, allow_insecure=False diff --git a/astropy/wcs/tests/test_wcs.py b/astropy/wcs/tests/test_wcs.py index 8eddd5d9863c..240c3ccc5955 100644 --- a/astropy/wcs/tests/test_wcs.py +++ b/astropy/wcs/tests/test_wcs.py @@ -2417,7 +2417,7 @@ def test_unit_scaling(self): def test_no_scaling_if_no_unit_changes(self): # This checks that if a WCS doesn't actually have units changing in # WCSLIB, that we aren't using the unit scaling machinery with scales - # all set to 1 since this would have an unecessary impact on performance. + # all set to 1 since this would have an unnecessary impact on performance. simple_wcs = wcs.WCS(naxis=3, preserve_units=True) simple_wcs.wcs.ctype = "RA---TAN", "DEC--TAN", "FREQ" From f125bfbc9051dd8ffdbd6af5b7c161884a1bf09b Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:56:41 -0500 Subject: [PATCH 064/199] Backport of #19248 (Update minimum required version of astropy-iers-data) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 44b03403455f..d9c7e948ff19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ keywords = [ dependencies = [ "numpy>=1.24", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires - "astropy-iers-data>=0.2025.12.29.0.42.34", + "astropy-iers-data>=0.2026.1.26.0.43.56", "PyYAML>=6.0.0", "packaging>=22.0.0", ] From abc44918876324807d787bd26393a197a03ac153 Mon Sep 17 00:00:00 2001 From: Vishwas Date: Tue, 3 Feb 2026 07:50:18 +0530 Subject: [PATCH 065/199] Backport PR #19245: FIX : spelling mistakes in comments and tests --- astropy/visualization/wcsaxes/formatter_locator.py | 2 +- astropy/visualization/wcsaxes/tests/test_wcsapi.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/astropy/visualization/wcsaxes/formatter_locator.py b/astropy/visualization/wcsaxes/formatter_locator.py index 01af93a9db8c..53197ca4c8c2 100644 --- a/astropy/visualization/wcsaxes/formatter_locator.py +++ b/astropy/visualization/wcsaxes/formatter_locator.py @@ -424,7 +424,7 @@ def formatter(self, values, spacing, format="auto"): # from the end. We do this rather than just trusting e.g. # str() because str(15.) == 15.0. We format using 10 decimal # places by default before stripping the zeros since this - # corresponds to a resolution of less than a microarcecond, + # corresponds to a resolution of less than a microarcsecond, # which should be sufficient. spacing = spacing.to_value(unit) fields = 0 diff --git a/astropy/visualization/wcsaxes/tests/test_wcsapi.py b/astropy/visualization/wcsaxes/tests/test_wcsapi.py index 1c6fb7adf23d..7c2106e7ab81 100644 --- a/astropy/visualization/wcsaxes/tests/test_wcsapi.py +++ b/astropy/visualization/wcsaxes/tests/test_wcsapi.py @@ -644,7 +644,7 @@ def world_axis_object_classes(self): def test_edge_axes(): - # Check that axes on the edge of a spherical projection are shown properley + # Check that axes on the edge of a spherical projection are shown properly # (see https://github.com/astropy/astropy/issues/10441) shape = [180, 360] data = np.random.rand(*shape) From 426c74898fc7163965960bfa8cfd3323e65ae748 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:10:36 -0500 Subject: [PATCH 066/199] Backport PR #19252: [internal] Fix minor typos in test comments --- astropy/convolution/tests/test_convolve.py | 2 +- astropy/nddata/mixins/tests/test_ndarithmetic.py | 2 +- astropy/stats/tests/test_funcs.py | 2 +- astropy/stats/tests/test_sigma_clipping.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/astropy/convolution/tests/test_convolve.py b/astropy/convolution/tests/test_convolve.py index 4009e80a465e..1d939ce270fb 100644 --- a/astropy/convolution/tests/test_convolve.py +++ b/astropy/convolution/tests/test_convolve.py @@ -1247,7 +1247,7 @@ def test_uninterpolated_nan_regions(boundary, normalize_kernel): # Test case: kernel.shape > NaN_region.shape nan_centroid = np.full( (kernel.shape[0] - 1, kernel.shape[1] - 1), np.nan - ) # 1 smaller than kerenel + ) # 1 smaller than kernel image = np.pad( nan_centroid, pad_width=kernel.shape[0] * 2, mode="constant", constant_values=1 ) diff --git a/astropy/nddata/mixins/tests/test_ndarithmetic.py b/astropy/nddata/mixins/tests/test_ndarithmetic.py index f99e61dc052d..29453dbcde9d 100644 --- a/astropy/nddata/mixins/tests/test_ndarithmetic.py +++ b/astropy/nddata/mixins/tests/test_ndarithmetic.py @@ -1272,7 +1272,7 @@ def test_two_argument_useage_non_nddata_first_arg(meth): # Call add on the class (not the instance) ndd3 = getattr(NDDataArithmetic, meth)(data1, data2) - # Compare it with the instance-useage and two identical NDData-like + # Compare it with the instance-usage and two identical NDData-like # classes: ndd1 = NDDataArithmetic(data1) ndd2 = NDDataArithmetic(data2) diff --git a/astropy/stats/tests/test_funcs.py b/astropy/stats/tests/test_funcs.py index 47a2fc4605d3..1ac322e77d90 100644 --- a/astropy/stats/tests/test_funcs.py +++ b/astropy/stats/tests/test_funcs.py @@ -60,7 +60,7 @@ def test_median_absolute_deviation(): def test_median_absolute_deviation_masked(): - # Based on the changes introduces in #4658 + # Based on the changes introduced in #4658 # normal masked arrays without masked values are handled like normal # numpy arrays diff --git a/astropy/stats/tests/test_sigma_clipping.py b/astropy/stats/tests/test_sigma_clipping.py index b69a576315b0..501273e07299 100644 --- a/astropy/stats/tests/test_sigma_clipping.py +++ b/astropy/stats/tests/test_sigma_clipping.py @@ -670,7 +670,7 @@ def test_mad_std(): # We now check this again but with the axis= keyword set since at the time # of writing this test this relies on a fast C implementation in which we - # have re-inplemented mad_std. + # have re-implemented mad_std. result_std = sigma_clip( array, From e44f30e0b34c9a416eda6a319670548539488832 Mon Sep 17 00:00:00 2001 From: Vishwas Date: Tue, 3 Feb 2026 23:36:03 +0530 Subject: [PATCH 067/199] Backport PR #19253: [internal] fix: minor spelling typos in comments --- astropy/utils/data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/astropy/utils/data.py b/astropy/utils/data.py index 0c51448f2b5c..debc15181a6c 100644 --- a/astropy/utils/data.py +++ b/astropy/utils/data.py @@ -86,7 +86,7 @@ class _NonClosingBufferedReader(io.BufferedReader): def __del__(self): try: # NOTE: self.raw will not be closed, but left in the state - # it was in at detactment + # it was in at detachment self.detach() except Exception: pass @@ -96,7 +96,7 @@ class _NonClosingTextIOWrapper(io.TextIOWrapper): def __del__(self): try: # NOTE: self.stream will not be closed, but left in the state - # it was in at detactment + # it was in at detachment self.detach() except Exception: pass @@ -370,7 +370,7 @@ def get_readable_fileobj( # Check if the file object supports random access, and if not, # then wrap it in a BytesIO buffer. It would be nicer to use a - # BufferedReader to avoid reading loading the whole file first, + # BufferedReader to avoid loading the whole file first, # but that might not be compatible with all possible I/O classes. if not hasattr(fileobj, "seek"): try: From 9ae300bdf0f86ff0ad95171754cd2942c6ff4581 Mon Sep 17 00:00:00 2001 From: Raphael Erik Hviding Date: Thu, 5 Feb 2026 18:44:53 +0100 Subject: [PATCH 068/199] Backport PR #19268: Vendored private API from narwhals --- astropy/table/_dataframes.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/astropy/table/_dataframes.py b/astropy/table/_dataframes.py index 15eeb1b784cc..66948a473ae1 100644 --- a/astropy/table/_dataframes.py +++ b/astropy/table/_dataframes.py @@ -186,8 +186,14 @@ def to_df( backend_impl = _get_backend_impl(backend) - # Using private API while narwhals-dev/narwhals#3150 is not merged - if not nw._utils.is_eager_allowed(backend_impl): + # Ensure eager-compatible backend + if backend_impl not in { + nw.Implementation.CUDF, + nw.Implementation.MODIN, + nw.Implementation.PANDAS, + nw.Implementation.POLARS, + nw.Implementation.PYARROW, + }: raise ValueError("Must export to eager compatible DataFrame") # Handle index argument From 63e1f9e9c23a79b6e673884ae2ec725a6029a3db Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:14:15 -0500 Subject: [PATCH 069/199] Backport PR #19269: Add more link targets --- docs/development/maintainers/index.rst | 2 ++ docs/index.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/development/maintainers/index.rst b/docs/development/maintainers/index.rst index f576ac6b21c2..ce1de3c3bbce 100644 --- a/docs/development/maintainers/index.rst +++ b/docs/development/maintainers/index.rst @@ -1,3 +1,5 @@ +.. _maintainers-index: + Maintaining astropy and affiliated packages ------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index f733c5dc1d69..2170241d9424 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,8 @@ :tocdepth: 3 +.. _astropy-docs-index: + ################################################# astropy: A Community Python Library for Astronomy ################################################# From d46f1277f3dae7435c24823cd37d85d7e3c4686f Mon Sep 17 00:00:00 2001 From: Jett Higgins Date: Fri, 6 Feb 2026 11:42:32 -0500 Subject: [PATCH 070/199] Backport PR #19238: Updating bold titles to headings --- docs/table/modify_table.rst | 80 +++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/docs/table/modify_table.rst b/docs/table/modify_table.rst index cd4f80ca6bfe..2f5d46ccf9c0 100644 --- a/docs/table/modify_table.rst +++ b/docs/table/modify_table.rst @@ -20,7 +20,10 @@ Examples .. EXAMPLE START: Making a Table and Modifying Data -**Make a table** +.. _table-mod-make-a-table: + +Make a table +^^^^^^^^^^^^ :: >>> from astropy.table import Table @@ -28,7 +31,10 @@ Examples >>> arr = np.arange(15).reshape(5, 3) >>> t = Table(arr, names=('a', 'b', 'c'), meta={'keywords': {'key1': 'val1'}}) -**Modify data values** +.. _table-mod-modify-data-values: + +Modify data values +^^^^^^^^^^^^^^^^^^ :: >>> t['a'][:] = [1, -2, 3, -4, 5] # Set all values of column 'a' @@ -73,7 +79,10 @@ the conventions of `~astropy.units.Quantity` by using the .. EXAMPLE END -**Add a column or columns** +.. _table-mod-add-a-column-or-columns: + +Add a column or columns +^^^^^^^^^^^^^^^^^^^^^^^ .. EXAMPLE START: Adding Columns to Tables @@ -115,7 +124,10 @@ convenient to add a |Quantity| to a |QTable| instead, see .. EXAMPLE END -**Remove columns** +.. _table-mod-remove-columns: + +Remove columns +^^^^^^^^^^^^^^ .. EXAMPLE START: Removing Columns from Tables @@ -129,7 +141,10 @@ To remove a column from a table:: .. EXAMPLE END -**Replace a column** +.. _table-mod-replace-a-column: + +Replace a column +^^^^^^^^^^^^^^^^ .. EXAMPLE START: Replacing Columns in Tables @@ -148,7 +163,10 @@ will be done, for example:: .. EXAMPLE END -**Perform a dictionary-style update** +.. _table-mod-perform-a-dictionary-style-update: + +Perform a dictionary-style update +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ It is possible to perform a dictionary-style update, which adds new columns to the table and replaces existing ones:: @@ -204,7 +222,10 @@ you need them to be references you can use the :meth:`~astropy.table.Table.update` method with ``copy=False``, see :ref:`copy_versus_reference` for details. -**Ensure the existence of a column** +.. _table-mod-ensure-the-existence-of-a-column: + +Ensure the existence of a column +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |Table| has a :meth:`~astropy.table.Table.setdefault` method, which is analogous to :meth:`dict.setdefault`. @@ -238,7 +259,10 @@ Either way the (possibly just inserted) column in the table is returned:: Ham False Spam False -**Rename columns** +.. _table-mod-rename-columns: + +Rename columns +^^^^^^^^^^^^^^ .. EXAMPLE START: Renaming Columns in Tables @@ -253,7 +277,10 @@ To rename multiple columns at once:: .. EXAMPLE END -**Add a row of data** +.. _table-mod-add-a-row-of-data: + +Add a row of data +^^^^^^^^^^^^^^^^^ .. EXAMPLE START: Adding a Row of Data to a Table @@ -263,7 +290,10 @@ To add a row:: .. EXAMPLE END -**Remove rows** +.. _table-mod-remove-rows: + +Remove rows +^^^^^^^^^^^ .. EXAMPLE START: Removing Rows of Data from Tables @@ -275,7 +305,10 @@ To remove a row:: .. EXAMPLE END -**Sort by one or more columns** +.. _table-mod-sort-by-one-or-more-columns: + +Sort by one or more columns +^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. EXAMPLE START: Sorting Columns in Tables @@ -286,7 +319,10 @@ To sort columns:: .. EXAMPLE END -**Reverse table rows** +.. _table-mod-reverse-table-rows: + +Reverse table rows +^^^^^^^^^^^^^^^^^^ .. EXAMPLE START: Reversing Table Rows @@ -296,7 +332,10 @@ To reverse the order of table rows:: .. EXAMPLE END -**Modify metadata** +.. _table-mod-modify-metadata: + +Modify metadata +^^^^^^^^^^^^^^^ .. EXAMPLE START: Modifying Metadata in Tables @@ -306,7 +345,10 @@ To modify metadata:: .. EXAMPLE END -**Select or reorder columns** +.. _table-mod-select-or-reorder-columns: + +Select or reorder columns +^^^^^^^^^^^^^^^^^^^^^^^^^ .. EXAMPLE START: Selecting or Reordering Columns in Tables @@ -375,13 +417,19 @@ with float values by internally calling ``t.replace_column('a', [10.5, 20.5, 30.5])``. In general this behavior is more consistent with Python and `pandas `_ behavior. -**Forcing in-place update** +.. _table-mod-forcing-in-place-update: + +Forcing in-place update +----------------------- It is possible to force an in-place update of a column as follows:: t[colname][:] = value -**Finding the source of problems** +.. _table-mod-finding-the-source-of-problems: + +Finding the source of problems +------------------------------ In order to find potential problems related to replacing columns, there is the option `astropy.table.conf.replace_warnings From 6f3e3b5d0552cee5e3f6fee56ad5bcc715b464de Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Sun, 8 Feb 2026 14:09:57 -0500 Subject: [PATCH 071/199] Merge pull request #19279 from neutrinoceros/dep/adjust-numpy-quaddtype DEP: relax lower bound on numpy-quaddtype from `>=1.0.0` to `>=0.2.2` (cherry picked from commit a6ee2dbe38903bfb44c6f765368fb4a0b4ea0cce) --- pyproject.toml | 9 ++++++++- tox.ini | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d9c7e948ff19..429dc547236d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,14 @@ test_all = [ "sgp4>=2.3", "array-api-strict>=1.0", "array-api-strict<2.4;python_version<'3.12'", # PYTHON_LT_3_12 - "numpy-quaddtype>=1.0", + + # 0.2.2 works on *some* platforms we support, though not all + # 1.0.0 would be a more universal lower bound, but it also + # requires numpy>=2.4. Because dependency trees are resolved + # with *all* extras and groups (whether they are to be installed or not), + # and we want to preserve the ability to test against NUMPY_LT_2_4, + # we effectively cannot upgrade this lower bound until we drop NUMPY_LT_2_4 + "numpy-quaddtype>=0.2.2", ] typing = [ "pandas-stubs>=2.0.0", diff --git a/tox.ini b/tox.ini index bf4d7856afda..09f2ec46d351 100644 --- a/tox.ini +++ b/tox.ini @@ -105,7 +105,7 @@ deps = devdeps-!noscipy: sgp4>=2.3 devdeps-!noscipy: array-api-strict>=1.0 py311-devdeps-!noscipy: array-api-strict<2.4 - devdeps-!noscipy: numpy-quaddtype>=1.0 + devdeps-!noscipy: numpy-quaddtype>=0.2.2 # The following indicates which [project.optional-dependencies] from pyproject.toml will be installed. # test_all does not work here due to upstream bug https://github.com/tox-dev/tox/issues/3433 From 42d89fed9fd302b17d1ba5fefac5c7335633cbdd Mon Sep 17 00:00:00 2001 From: yaochengchen <51320658+yaochengchen@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:31:38 +0800 Subject: [PATCH 072/199] Backport PR #19301: DOC: Fix typos in data.py and hdulist.py [ci skip] --- astropy/io/fits/hdu/hdulist.py | 2 +- astropy/utils/data.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/astropy/io/fits/hdu/hdulist.py b/astropy/io/fits/hdu/hdulist.py index 9d4cbe5a4fd6..b803fdbab953 100644 --- a/astropy/io/fits/hdu/hdulist.py +++ b/astropy/io/fits/hdu/hdulist.py @@ -266,7 +266,7 @@ def __init__(self, hdus=[], file=None): self._in_read_next_hdu = False # If we have read all the HDUs from the file or not - # The assumes that all HDUs have been written when we first opened the + # This assumes that all HDUs have been written when we first opened the # file; we do not currently support loading additional HDUs from a file # while it is being streamed to. In the future that might be supported # but for now this is only used for the purpose of lazy-loading of diff --git a/astropy/utils/data.py b/astropy/utils/data.py index debc15181a6c..cfa73fc7e5ec 100644 --- a/astropy/utils/data.py +++ b/astropy/utils/data.py @@ -177,7 +177,7 @@ def is_url(string): return url.scheme.lower() in ["http", "https", "ftp", "sftp", "ssh", "file"] -# Backward compatibility because some downstream packages allegedly uses it. +# Backward compatibility because some downstream packages allegedly use it. _is_url = is_url From 586bfd4135a5391cc4227941ae6de756dbff4f85 Mon Sep 17 00:00:00 2001 From: yaochengchen Date: Sat, 21 Feb 2026 02:08:33 +0800 Subject: [PATCH 073/199] Backport PR #19308: BUG: Fix broadcasting bug in uniform() for ndim >= 2 inputs --- astropy/uncertainty/distributions.py | 7 ++--- .../uncertainty/tests/test_distribution.py | 26 +++++++++++++++++++ docs/changes/coordinates/19308.bugfix.rst | 2 ++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 docs/changes/coordinates/19308.bugfix.rst diff --git a/astropy/uncertainty/distributions.py b/astropy/uncertainty/distributions.py index 91f617e78c6c..d4687b95bf91 100644 --- a/astropy/uncertainty/distributions.py +++ b/astropy/uncertainty/distributions.py @@ -199,11 +199,8 @@ def uniform( ) newshape = lower.shape + (n_samples,) - if lower.shape == () and upper.shape == (): - width = upper - lower # scalar - else: - width = (upper - lower)[:, np.newaxis] - lower = lower[:, np.newaxis] + width = (upper - lower)[..., np.newaxis] + lower = lower[..., np.newaxis] samples = lower + width * np.random.uniform(size=newshape) return cls(samples, **kwargs) diff --git a/astropy/uncertainty/tests/test_distribution.py b/astropy/uncertainty/tests/test_distribution.py index 9fe47457e569..8d2b1de4f0d3 100644 --- a/astropy/uncertainty/tests/test_distribution.py +++ b/astropy/uncertainty/tests/test_distribution.py @@ -688,3 +688,29 @@ def setup_class(cls): cls.d = cls.d * u.Unit("km,m") cls.item = cls.item * u.Unit("Mm,km") cls.b_item = cls.b_item * u.km + + +def test_helper_uniform_multidim_regression(): + """ + Regression test: uniform() must support ndim >= 2 inputs. + + Before fix (using [:, np.newaxis]) this raised a broadcasting ValueError. + After fix (using [..., np.newaxis]) it should work and preserve trailing + sample axis. + """ + lower = np.zeros((3, 4)) + upper = np.ones((3, 4)) + n_samples = 7 + + d = ds.uniform(lower=lower, upper=upper, n_samples=n_samples) + + # Public shape hides sample axis + assert d.shape == (3, 4) + assert d.n_samples == n_samples + + # Underlying samples must include trailing sampling axis + assert d.distribution.shape == (3, 4, n_samples) + + # Bounds must hold for all samples + assert np.all(d.distribution >= lower[..., np.newaxis]) + assert np.all(d.distribution <= upper[..., np.newaxis]) diff --git a/docs/changes/coordinates/19308.bugfix.rst b/docs/changes/coordinates/19308.bugfix.rst new file mode 100644 index 000000000000..ebb9e210d15f --- /dev/null +++ b/docs/changes/coordinates/19308.bugfix.rst @@ -0,0 +1,2 @@ +Fixed a broadcasting bug in ``astropy.uncertainty.distributions.uniform`` where +multi-dimensional inputs (``ndim >= 2``) would raise a ``ValueError``. From 5f3df3e4d13ffdd6d0bde053a1b9bc72136fcfc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 23 Feb 2026 18:23:27 +0100 Subject: [PATCH 074/199] DEP: set upper bound on numpy requirement (7.2.x) (#19233) * DEP: set upper bound on numpy requirement (7.2.x) * TST: ignore deprecation warnings for numpy.char.chararray --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 429dc547236d..b172d0e4a32b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ keywords = [ "ascii", ] dependencies = [ - "numpy>=1.24", + "numpy>=1.24, <2.7", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires "astropy-iers-data>=0.2026.1.26.0.43.56", "PyYAML>=6.0.0", @@ -254,6 +254,7 @@ filterwarnings = [ "error", # https://github.com/astropy/astropy/issues/18126 "ignore:'_UnionGenericAlias' is deprecated and slated for removal in Python 3.17:DeprecationWarning", # not PYTHON_LT_3_14 + "ignore:The chararray class is deprecated and will be removed in a future release.:DeprecationWarning", # not NUMPY_LT_2_5 ] doctest_norecursedirs = [ "*/setup_package.py", From df0ee8b09cb43f1ece56cb86cfe5a10e29a7b909 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:43:53 -0500 Subject: [PATCH 075/199] Backport PR #19315: BUG: fix Gaussian2D's forward compatibility with numpy 2.5 (dev) --- astropy/modeling/functional_models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/astropy/modeling/functional_models.py b/astropy/modeling/functional_models.py index 49e3b99fd568..7b1fd5e2ef44 100644 --- a/astropy/modeling/functional_models.py +++ b/astropy/modeling/functional_models.py @@ -9,6 +9,7 @@ from astropy import units as u from astropy.units import Quantity, UnitsError +from astropy.utils.compat import NUMPY_LT_2_5 from astropy.utils.compat.optional_deps import HAS_SCIPY from astropy.utils.exceptions import AstropyDeprecationWarning @@ -380,6 +381,13 @@ def __init__( raise ValueError("Covariance matrix must be 2x2") eig_vals, eig_vecs = np.linalg.eig(cov_matrix) + if not NUMPY_LT_2_5 or eig_vals.dtype.kind == "c": + # in numpy 2.5+, return values are *always* complex + assert np.all(eig_vals.imag == 0) + assert np.all(eig_vecs.imag == 0) + eig_vals = eig_vals.real + eig_vecs = eig_vecs.real + x_stddev, y_stddev = np.sqrt(eig_vals) y_vec = eig_vecs[:, 0] theta = np.arctan2(y_vec[1], y_vec[0]) From 504f907c4101efbc725945250fa4ec8003d53563 Mon Sep 17 00:00:00 2001 From: Naksh Yadav Date: Tue, 24 Feb 2026 07:26:02 +0530 Subject: [PATCH 076/199] Backport PR #19312: More wording improvements [ci skip] --- CODE_OF_CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index e44a9261c4ca..6ace29669d47 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1 +1 @@ -All Astropy community members are expected to abide by the [Astropy Project Code of Conduct](http://www.astropy.org/code_of_conduct.html). +All Astropy community members are expected to abide by the [Astropy Project Code of Conduct](https://www.astropy.org/code_of_conduct.html). From 4a2248001181e7bf995f795c3313846bea2ad0e0 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:33:15 -0500 Subject: [PATCH 077/199] Merge pull request #19330 from maxnoe/earth-user-agent Add a user agent to geo-api requests (cherry picked from commit dab63bffa562ead3838998f4cef6eb1d7c96413a) --- astropy/coordinates/earth.py | 5 ++++- docs/changes/coordinates/19330.bugfix.rst | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/changes/coordinates/19330.bugfix.rst diff --git a/astropy/coordinates/earth.py b/astropy/coordinates/earth.py index 77069810e24d..294f8807fe65 100644 --- a/astropy/coordinates/earth.py +++ b/astropy/coordinates/earth.py @@ -14,6 +14,7 @@ from astropy.units.quantity import QuantityInfoBase from astropy.utils import data from astropy.utils.compat import COPY_IF_NEEDED +from astropy.utils.data import conf as data_conf from astropy.utils.data import get_pkg_data_contents from .angles import Angle, Latitude, Longitude @@ -74,8 +75,10 @@ def _get_json_result(url, err_str, use_google): from .name_resolve import NameResolveError try: + headers = {"User-Agent": data_conf.default_http_user_agent} + req = urllib.request.Request(url, headers=headers) # Retrieve JSON response from Google maps API - resp = urllib.request.urlopen(url, timeout=data.conf.remote_timeout) + resp = urllib.request.urlopen(req, timeout=data.conf.remote_timeout) resp_data = json.loads(resp.read().decode("utf8")) except urllib.error.URLError as e: diff --git a/docs/changes/coordinates/19330.bugfix.rst b/docs/changes/coordinates/19330.bugfix.rst new file mode 100644 index 000000000000..60df745beb2b --- /dev/null +++ b/docs/changes/coordinates/19330.bugfix.rst @@ -0,0 +1,2 @@ +Send a User-Agent header to the OpenStreetMap API in ``EarthLocation.of_address`` +to fix access denied errors. From 625ab025d398d02d7832433720b0bfa47fd115bd Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:38:36 -0500 Subject: [PATCH 078/199] Backport PR #19023: Fix getdata()'s lower and upper keywords. --- astropy/io/fits/convenience.py | 4 +++- astropy/io/fits/tests/test_convenience.py | 19 +++++++++++++++++++ docs/changes/io.fits/19023.bugfix.rst | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 docs/changes/io.fits/19023.bugfix.rst diff --git a/astropy/io/fits/convenience.py b/astropy/io/fits/convenience.py index 3253ba9caa2f..83b64816071d 100644 --- a/astropy/io/fits/convenience.py +++ b/astropy/io/fits/convenience.py @@ -251,7 +251,9 @@ def getdata(filename, *args, header=None, lower=None, upper=None, view=None, **k if data.dtype.descr[0][0] == "": # this data does not have fields return - data.dtype.names = [trans(n) for n in data.dtype.names] + + for n in data.dtype.names: + data.columns.change_name(n, trans(n)) # allow different views into the underlying ndarray. Keep the original # view just in case there is a problem diff --git a/astropy/io/fits/tests/test_convenience.py b/astropy/io/fits/tests/test_convenience.py index 14c021dadea2..a699f0b30534 100644 --- a/astropy/io/fits/tests/test_convenience.py +++ b/astropy/io/fits/tests/test_convenience.py @@ -513,3 +513,22 @@ def test_getdata_ext_not_given_nodata_noext(self): IndexError, match="No data in Primary HDU and no extension HDU found." ): fits.getdata(buf) + + def test_getdata_lower_upper(self, tmp_path): + t = Table([np.zeros(5), np.ones(5), np.arange(5)], names=["a", "b", "c"]) + t.write(tmp_path / "lower.fits") + t = Table([np.zeros(5), np.ones(5), np.arange(5)], names=["A", "B", "C"]) + t.write(tmp_path / "upper.fits") + + ref = np.zeros(5) + lowup = fits.getdata(tmp_path / "lower.fits", 1, upper=True) + assert lowup.dtype.names == ("A", "B", "C") + assert_array_equal(lowup["A"], ref) + assert_array_equal(lowup["a"], ref) + assert_array_equal(lowup.A, ref) + + uplow = fits.getdata(tmp_path / "upper.fits", 1, lower=True) + assert uplow.dtype.names == ("a", "b", "c") + assert_array_equal(uplow["A"], ref) + assert_array_equal(uplow["a"], ref) + assert_array_equal(uplow.a, ref) diff --git a/docs/changes/io.fits/19023.bugfix.rst b/docs/changes/io.fits/19023.bugfix.rst new file mode 100644 index 000000000000..bfca12894ad4 --- /dev/null +++ b/docs/changes/io.fits/19023.bugfix.rst @@ -0,0 +1 @@ +Fix ``getdata()``'s lower and upper keywords. From 300a802622eb4e8259a75a89f75d8051877b23ff Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Thu, 26 Feb 2026 20:02:14 +0100 Subject: [PATCH 079/199] Merge pull request #19335 from saimn/backport-19267 Handle chararray deprecation --- astropy/io/fits/column.py | 16 ++++++++-------- astropy/io/fits/fitsrec.py | 10 +++++----- astropy/io/fits/hdu/table.py | 6 +++--- astropy/io/fits/tests/test_table.py | 8 ++++---- astropy/utils/compat/numpycompat.py | 10 ++++++++++ docs/changes/io.fits/19335.bugfix.rst | 5 +++++ 6 files changed, 35 insertions(+), 20 deletions(-) create mode 100644 docs/changes/io.fits/19335.bugfix.rst diff --git a/astropy/io/fits/column.py b/astropy/io/fits/column.py index 804adb07c95f..ff513628367f 100644 --- a/astropy/io/fits/column.py +++ b/astropy/io/fits/column.py @@ -14,9 +14,9 @@ from textwrap import indent import numpy as np -from numpy import char as chararray from astropy.utils import lazyproperty +from astropy.utils.compat import chararray, get_chararray from astropy.utils.exceptions import AstropyUserWarning from .card import CARD_LENGTH, Card @@ -696,14 +696,14 @@ def __init__( # input arrays can be just list or tuple, not required to be ndarray # does not include Object array because there is no guarantee # the elements in the object array are consistent. - if not isinstance(array, (np.ndarray, chararray.chararray, Delayed)): + if not isinstance(array, (np.ndarray, chararray, Delayed)): try: # try to convert to a ndarray first if array is not None: array = np.array(array) except Exception: try: # then try to convert it to a strings array itemsize = int(recformat[1:]) - array = chararray.array(array, itemsize=itemsize) + array = get_chararray(array, itemsize=itemsize) except ValueError: # then try variable length array # Note: This includes _FormatQ by inheritance @@ -1388,7 +1388,7 @@ def _convert_to_valid_data_type(self, array): fsize = dims[-1] else: fsize = np.dtype(format.recformat).itemsize - return chararray.array(array, itemsize=fsize, copy=False) + return get_chararray(array, itemsize=fsize, copy=False) else: return _convert_array(array, np.dtype(format.recformat)) elif "L" in format: @@ -2082,7 +2082,7 @@ def __new__(cls, input, dtype="S"): try: # this handles ['abc'] and [['a','b','c']] # equally, beautiful! - input = [chararray.array(x, itemsize=1) for x in input] + input = [get_chararray(x, itemsize=1) for x in input] except Exception: raise ValueError(f"Inconsistent input data array: {input}") @@ -2105,10 +2105,10 @@ def __setitem__(self, key, value): """ if isinstance(value, np.ndarray) and value.dtype == self.dtype: pass - elif isinstance(value, chararray.chararray) and value.itemsize == 1: + elif isinstance(value, chararray) and value.itemsize == 1: pass elif self.element_dtype == "S": - value = chararray.array(value, itemsize=1) + value = get_chararray(value, itemsize=1) else: value = np.array(value, dtype=self.element_dtype) np.ndarray.__setitem__(self, key, value) @@ -2262,7 +2262,7 @@ def _makep(array, descr_output, format, nrows=None): else: rowval = [0] * data_output.max if format.dtype == "S": - data_output[idx] = chararray.array(encode_ascii(rowval), itemsize=1) + data_output[idx] = get_chararray(encode_ascii(rowval), itemsize=1) else: data_output[idx] = np.array(rowval, dtype=format.dtype) diff --git a/astropy/io/fits/fitsrec.py b/astropy/io/fits/fitsrec.py index 9787b618e32c..1bdba8b3e5dc 100644 --- a/astropy/io/fits/fitsrec.py +++ b/astropy/io/fits/fitsrec.py @@ -8,9 +8,9 @@ from functools import reduce import numpy as np -from numpy import char as chararray from astropy.utils import lazyproperty +from astropy.utils.compat import chararray, get_chararray from .column import ( _VLF, @@ -831,7 +831,7 @@ def _convert_p(self, column, field, recformat): dt = np.dtype(recformat.dtype + str(1)) arr_len = count * dt.itemsize da = raw_data[offset : offset + arr_len].view(dt) - da = np.char.array(da.view(dtype=dt), itemsize=count) + da = get_chararray(da.view(dtype=dt), itemsize=count) dummy[idx] = decode_ascii(da) else: dt = np.dtype(recformat.dtype) @@ -1204,7 +1204,7 @@ def _scale_back(self, update_heap_pointers=True): if isinstance(self._coldefs, _AsciiColDefs): self._scale_back_ascii(index, dummy, raw_field) # binary table string column - elif isinstance(raw_field, chararray.chararray): + elif isinstance(raw_field, chararray): self._scale_back_strings(index, dummy, raw_field) # all other binary table columns else: @@ -1361,8 +1361,8 @@ def _get_recarray_field(array, key): # This is currently needed for backwards-compatibility and for # automatic truncation of trailing whitespace field = np.recarray.field(array, key) - if field.dtype.char in ("S", "U") and not isinstance(field, chararray.chararray): - field = field.view(chararray.chararray) + if field.dtype.char in ("S", "U") and not isinstance(field, chararray): + field = field.view(chararray) return field diff --git a/astropy/io/fits/hdu/table.py b/astropy/io/fits/hdu/table.py index 3121c8d49077..e9d2f2f293fe 100644 --- a/astropy/io/fits/hdu/table.py +++ b/astropy/io/fits/hdu/table.py @@ -11,7 +11,6 @@ from contextlib import suppress import numpy as np -from numpy import char as chararray # This module may have many dependencies on astropy.io.fits.column, but # astropy.io.fits.column has fewer dependencies overall, so it's easier to @@ -37,6 +36,7 @@ from astropy.io.fits.header import Header, _pad_length from astropy.io.fits.util import _is_int, _str_to_num, path_like from astropy.utils import lazyproperty +from astropy.utils.compat import chararray from .base import DELAYED, ExtensionHDU, _ValidHDU @@ -1489,7 +1489,7 @@ def _binary_table_byte_swap(data): formats.append(field_dtype) offsets.append(field_offset) - if isinstance(field, chararray.chararray): + if isinstance(field, chararray): continue # only swap unswapped @@ -1507,7 +1507,7 @@ def _binary_table_byte_swap(data): coldata = data.field(idx) for c in coldata: if ( - not isinstance(c, chararray.chararray) + not isinstance(c, chararray) and c.itemsize > 1 and c.dtype.str[0] in swap_types ): diff --git a/astropy/io/fits/tests/test_table.py b/astropy/io/fits/tests/test_table.py index f3bae3f523a4..c91ad65eaa26 100644 --- a/astropy/io/fits/tests/test_table.py +++ b/astropy/io/fits/tests/test_table.py @@ -9,7 +9,6 @@ import numpy as np import pytest -from numpy import char as chararray try: import objgraph @@ -24,6 +23,7 @@ from astropy.io.fits.verify import VerifyError from astropy.table import Table from astropy.units import Unit, UnitsWarning, UnrecognizedUnit +from astropy.utils.compat import get_chararray from astropy.utils.exceptions import AstropyUserWarning from .conftest import FitsTestCase @@ -156,7 +156,7 @@ def test_open(self, home_is_data): fd = fits.open(self.data("test0.fits")) # create some local arrays - a1 = chararray.array(["abc", "def", "xx"]) + a1 = get_chararray(["abc", "def", "xx"]) r1 = np.array([11.0, 12.0, 13.0], dtype=np.float32) # create a table from scratch, using a mixture of columns from existing @@ -319,7 +319,7 @@ def test_ascii_table(self): # Test Start Column - a1 = chararray.array(["abcd", "def"]) + a1 = get_chararray(["abcd", "def"]) r1 = np.array([11.0, 12.0]) c1 = fits.Column(name="abc", format="A3", start=19, array=a1) c2 = fits.Column(name="def", format="E", start=3, array=r1) @@ -1970,7 +1970,7 @@ def test_string_column_padding(self): "p\x00\x00\x00\x00\x00\x00\x00\x00\x00" ) - acol = fits.Column(name="MEMNAME", format="A10", array=chararray.array(a)) + acol = fits.Column(name="MEMNAME", format="A10", array=get_chararray(a)) ahdu = fits.BinTableHDU.from_columns([acol]) assert ahdu.data.tobytes().decode("raw-unicode-escape") == s ahdu.writeto(self.temp("newtable.fits")) diff --git a/astropy/utils/compat/numpycompat.py b/astropy/utils/compat/numpycompat.py index 91cbc3805225..5fd0231a4fc2 100644 --- a/astropy/utils/compat/numpycompat.py +++ b/astropy/utils/compat/numpycompat.py @@ -4,6 +4,8 @@ earlier versions of Numpy. """ +import warnings + import numpy as np from astropy.utils import minversion @@ -19,6 +21,8 @@ "NUMPY_LT_2_4", "NUMPY_LT_2_4_1", "NUMPY_LT_2_5", + "chararray", + "get_chararray", ] # TODO: It might also be nice to have aliases to these named for specific @@ -36,3 +40,9 @@ COPY_IF_NEEDED = False if NUMPY_LT_2_0 else None + +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + + get_chararray = np.char.array + chararray = np.char.chararray diff --git a/docs/changes/io.fits/19335.bugfix.rst b/docs/changes/io.fits/19335.bugfix.rst new file mode 100644 index 000000000000..079edfa86a16 --- /dev/null +++ b/docs/changes/io.fits/19335.bugfix.rst @@ -0,0 +1,5 @@ +``np.char.chararray`` being deprecated in Numpy 2.5, in a future version +``io.fits`` will return a normal array instead of a ``chararray`` for string +columns. As a consequence the special chararray methods will be deprecated in +version 8.0 (e.g., ``.rstrip()`` or ``.decode()``). In version 7.2.x deprecation +warnings from Numpy are simply filtered. From 26ec157c57f1bb9468f992466cf886b6f0eb556a Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:06:05 -0500 Subject: [PATCH 080/199] Backport PR #19340: fix "_POSIX_C_SOURCE redefined" error and warnings --- astropy/convolution/src/convolve.c | 4 ++-- astropy/cosmology/_src/signature_deprecations.c | 2 +- astropy/utils/xml/src/iterparse.c | 2 +- astropy/wcs/include/astropy_wcs/pyutil.h | 3 +-- astropy/wcs/src/pyutil.c | 7 +++---- astropy/wcs/src/str_list_proxy.c | 2 +- astropy/wcs/src/unit_list_proxy.c | 2 +- astropy/wcs/src/wcslib_celprm_wrap.c | 6 +++--- astropy/wcs/src/wcslib_prjprm_wrap.c | 6 +++--- astropy/wcs/src/wcslib_wrap.c | 6 +++--- 10 files changed, 19 insertions(+), 21 deletions(-) diff --git a/astropy/convolution/src/convolve.c b/astropy/convolution/src/convolve.c index 8d66753c921c..07d7aed17030 100644 --- a/astropy/convolution/src/convolve.c +++ b/astropy/convolution/src/convolve.c @@ -25,14 +25,14 @@ */ +#include "convolve.h" + #include #include #include #include #include -#include "convolve.h" - #ifdef _OPENMP #include #endif diff --git a/astropy/cosmology/_src/signature_deprecations.c b/astropy/cosmology/_src/signature_deprecations.c index 6293d21d1059..fbcdce2adeaa 100644 --- a/astropy/cosmology/_src/signature_deprecations.c +++ b/astropy/cosmology/_src/signature_deprecations.c @@ -3,8 +3,8 @@ // MIT. see licenses/POSITIONAL_DEFAULTS.rst -#include // snprintf #include +#include // snprintf #include #define SINCE_CHAR_SIZE 32 diff --git a/astropy/utils/xml/src/iterparse.c b/astropy/utils/xml/src/iterparse.c index f932f74550f0..b948bec86d5e 100644 --- a/astropy/utils/xml/src/iterparse.c +++ b/astropy/utils/xml/src/iterparse.c @@ -22,12 +22,12 @@ * library. ******************************************************************************/ +#include #include #include // memcpy #ifndef _MSC_VER # include // read #endif -#include #include "structmember.h" #include "expat.h" diff --git a/astropy/wcs/include/astropy_wcs/pyutil.h b/astropy/wcs/include/astropy_wcs/pyutil.h index 13744207d20b..9122a3fbcfd5 100644 --- a/astropy/wcs/include/astropy_wcs/pyutil.h +++ b/astropy/wcs/include/astropy_wcs/pyutil.h @@ -6,12 +6,11 @@ #ifndef __PYUTIL_H__ #define __PYUTIL_H__ +#include #include "util.h" #define PY_ARRAY_UNIQUE_SYMBOL astropy_wcs_numpy_api -#include - #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include #include diff --git a/astropy/wcs/src/pyutil.c b/astropy/wcs/src/pyutil.c index 0b23eede6dfe..5418de2da75d 100644 --- a/astropy/wcs/src/pyutil.c +++ b/astropy/wcs/src/pyutil.c @@ -5,14 +5,13 @@ #define NO_IMPORT_ARRAY -#include // malloc, free -#include // memcpy - /* util.h must be imported first */ #include "astropy_wcs/pyutil.h" - #include "astropy_wcs/docstrings.h" +#include // malloc, free +#include // memcpy + #include "wcsfix.h" #include "wcshdr.h" #include "wcsprintf.h" diff --git a/astropy/wcs/src/str_list_proxy.c b/astropy/wcs/src/str_list_proxy.c index 3e5a31f3d27e..c0d283fa1c76 100644 --- a/astropy/wcs/src/str_list_proxy.c +++ b/astropy/wcs/src/str_list_proxy.c @@ -5,8 +5,8 @@ #define NO_IMPORT_ARRAY -#include // malloc, free #include "astropy_wcs/pyutil.h" +#include // malloc, free /*************************************************************************** * List-of-strings proxy object diff --git a/astropy/wcs/src/unit_list_proxy.c b/astropy/wcs/src/unit_list_proxy.c index 4657534da992..169815ed6398 100644 --- a/astropy/wcs/src/unit_list_proxy.c +++ b/astropy/wcs/src/unit_list_proxy.c @@ -5,9 +5,9 @@ #define NO_IMPORT_ARRAY -#include // strncmp #include "astropy_wcs/pyutil.h" #include "astropy_wcs/str_list_proxy.h" +#include // strncmp /*************************************************************************** * List-of-units proxy object diff --git a/astropy/wcs/src/wcslib_celprm_wrap.c b/astropy/wcs/src/wcslib_celprm_wrap.c index 0b392bbc6a65..e00c1809f7a8 100644 --- a/astropy/wcs/src/wcslib_celprm_wrap.c +++ b/astropy/wcs/src/wcslib_celprm_wrap.c @@ -1,11 +1,11 @@ #define NO_IMPORT_ARRAY -#include // calloc, malloc, free -#include // memcpy - #include "astropy_wcs/wcslib_celprm_wrap.h" #include "astropy_wcs/wcslib_prjprm_wrap.h" +#include // calloc, malloc, free +#include // memcpy + #include #include #include diff --git a/astropy/wcs/src/wcslib_prjprm_wrap.c b/astropy/wcs/src/wcslib_prjprm_wrap.c index d8575f7411cd..7fcef537a087 100644 --- a/astropy/wcs/src/wcslib_prjprm_wrap.c +++ b/astropy/wcs/src/wcslib_prjprm_wrap.c @@ -1,12 +1,12 @@ #define NO_IMPORT_ARRAY +#include "astropy_wcs/wcslib_celprm_wrap.h" +#include "astropy_wcs/wcslib_prjprm_wrap.h" + #include #include #include // calloc, malloc, free #include // memcpy, strlen, strncpy -#include "astropy_wcs/wcslib_celprm_wrap.h" -#include "astropy_wcs/wcslib_prjprm_wrap.h" - #include #include #include diff --git a/astropy/wcs/src/wcslib_wrap.c b/astropy/wcs/src/wcslib_wrap.c index 5c21d3707edf..b0c8a8027266 100755 --- a/astropy/wcs/src/wcslib_wrap.c +++ b/astropy/wcs/src/wcslib_wrap.c @@ -5,9 +5,6 @@ #define NO_IMPORT_ARRAY -#include // calloc, malloc, free -#include // memset, strlen, strncpy - #include "astropy_wcs/wcslib_wrap.h" #include "astropy_wcs/wcslib_auxprm_wrap.h" #include "astropy_wcs/wcslib_prjprm_wrap.h" @@ -18,6 +15,9 @@ #include "astropy_wcs/unit_list_proxy.h" #include /* from Python */ +#include // calloc, malloc, free +#include // memset, strlen, strncpy + #include #include #include From a92de2193d2ccbc98f6adbd8bb515b4dbb1e9930 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 2 Mar 2026 15:00:49 +0000 Subject: [PATCH 081/199] Backport PR #19347: Update publish workflow to use trusted publishing --- .github/workflows/publish.yml | 31 +++++++++++++++++++++++++------ docs/changes/19347.other.rst | 1 + 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 docs/changes/19347.other.rst diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index aad72a7d0f0a..0c5b077e5231 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,12 +21,12 @@ concurrency: cancel-in-progress: true jobs: - build_and_publish: - # This does the actual wheel building and publishing as part of the cron job + build: + # This does the actual wheel building as part of the cron job # or if triggered manually via the workflow dispatch, or for a tag. permissions: contents: none - uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@d9b81a07e789d1b1921ab5fe33de2ddcbbd02c3f # v2.3.0 + uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish.yml@a138926f6e4f9667d1306c24f24f5bdcaa01fbab # v2.5.0 if: | github.repository == 'astropy/astropy' && ( startsWith(github.ref, 'refs/tags/v') || @@ -36,8 +36,9 @@ jobs: ) with: - # We upload to PyPI for all tags starting with v but not ones ending in .dev - upload_to_pypi: ${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '.dev') && (github.event_name == 'push') }} + # We use trusted publishing so the upload is handled in a separate job below + upload_to_pypi: false + save_artifacts: true upload_to_anaconda: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} anaconda_user: astropy anaconda_package: astropy @@ -78,5 +79,23 @@ jobs: - cp3*-win_arm64 secrets: - pypi_token: ${{ secrets.pypi_token }} anaconda_token: ${{ secrets.anaconda_token }} + + upload: + # Upload to PyPI using trusted publishing for all tags starting with v but not ones ending in .dev + if: startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '.dev') && github.event_name == 'push' + name: Upload release to PyPI + runs-on: ubuntu-latest + needs: [build] + environment: pypi + permissions: + id-token: write + steps: + - name: Download artifacts + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + merge-multiple: true + pattern: dist-* + path: dist + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/docs/changes/19347.other.rst b/docs/changes/19347.other.rst new file mode 100644 index 000000000000..dae27136e928 --- /dev/null +++ b/docs/changes/19347.other.rst @@ -0,0 +1 @@ +Publishing files to PyPI is now done using the Trusted Publisher mechanism (https://docs.pypi.org/trusted-publishers/). From a8fab94baebfeb17b041fff52a7b97516e9def97 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:39:43 -0500 Subject: [PATCH 082/199] Manual backport of #19346 onto v7.2.x --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b172d0e4a32b..13c788d7fde5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ keywords = [ dependencies = [ "numpy>=1.24, <2.7", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires - "astropy-iers-data>=0.2026.1.26.0.43.56", + "astropy-iers-data>=0.2026.2.23.0.48.33", "PyYAML>=6.0.0", "packaging>=22.0.0", ] From 513dfe4df6056e0f226c69e0f7eb5d84db161641 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:06:59 -0500 Subject: [PATCH 083/199] Backport PR #19352: Documentation: fix spelling error in GCRS class description --- astropy/coordinates/builtin_frames/gcrs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astropy/coordinates/builtin_frames/gcrs.py b/astropy/coordinates/builtin_frames/gcrs.py index e9b66707bbef..76bab76562a5 100644 --- a/astropy/coordinates/builtin_frames/gcrs.py +++ b/astropy/coordinates/builtin_frames/gcrs.py @@ -40,7 +40,7 @@ class GCRS(BaseRADecFrame): """ A coordinate or frame in the Geocentric Celestial Reference System (GCRS). - GCRS is distinct form ICRS mainly in that it is relative to the Earth's + GCRS is distinct from ICRS mainly in that it is relative to the Earth's center-of-mass rather than the solar system Barycenter. That means this frame includes the effects of aberration (unlike ICRS). For more background on the GCRS, see the references provided in the From 95e996c1b0905d71001a4b1a974453c14a6c3f01 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:23:21 -0500 Subject: [PATCH 084/199] Backport PR #19354: TST: Update hash for mpldev image tests --- astropy/tests/figures/py311-test-image-mpldev-cov.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/astropy/tests/figures/py311-test-image-mpldev-cov.json b/astropy/tests/figures/py311-test-image-mpldev-cov.json index 886491a2af98..581b01663817 100644 --- a/astropy/tests/figures/py311-test-image-mpldev-cov.json +++ b/astropy/tests/figures/py311-test-image-mpldev-cov.json @@ -1,14 +1,14 @@ { - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "0c259f5278851bda21016cce3d5479e1d6202762587ad8d8ff9982fa5a9ce11a", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "79743d5939dd58fc0a84941e258a1fa78212195d6bbbb7cc7b11439a1816e9b0", "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_rectangular": "d9ad505b7455d6f3a1305273547b177e58d1f36418f5983d0faad29ddd5134a2", "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_nonrectangular": "0c1528f0ac4c30b66b0f427851dd82ae6fca2982db5c86dd20166318e2271c5d", "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_change_wcs": "c68e961f0a21cc0dcc43c523cee1c564d94bd96c795f976d51e5198fc52f83cc", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tight_layout": "ab936ab9b81dc3b35acea5257e108ac0071aec2225cefffa096b1873e650f2f8", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_image_plot": "4fe4c89d089e8f1f9584584826f25d3c884d4914f76a863e1a624f9ef2295b07", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axes_off": "3580258468fc0ff0fa6d140299b79f1f05c16c3e222447de38228e01983fbdd1", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[True]": "5364e39cab14f28b08073b31cce3dae8501c0431b1a49985810b764902747c8e", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[False]": "ee1e46856a19ba472ae9ea4588c614fbbd1b3a4a0787ef40e33f991a8f79d6c0", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[line]": "516c3882bbaa31a0d241eec2c70aca45b27b44929662371718b10025735db772", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[True]": "e2b531b65facf320881a3bea5adf91fd3bd6f4f267c83973c5c602b0cb1b5589", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[False]": "ec8157d34f8691023645d5bf82f98dbd1ed79abcfcfd0dbfdcc245bea0956caf", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[line]": "2fee3974028c571086c6ad2bbb52b8fb9fffe0dcb8d9ee6cb1b9611dea6ac6be", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contour_overlay": "3df6a162520b4ac6ec4727bb58bad168852c5b037b9202af0d85ae4997c8954e", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contourf_overlay": "5a68cd27e0cace22f2501d5a3c784f71b18e5f9f409e04ef8738761b0845c2d7", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_overlay_features_image": "1e0c92a5d43d935e7d497ae79b54681bb58b480782b412c5a521da66e928bca7", From a79ea925156f9a8fa2324e4ffacb62def95a23f5 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Tue, 3 Mar 2026 16:03:47 -0500 Subject: [PATCH 085/199] Backport PR #19351: BUG: Fix return vs raise in ShapedLikeNDArray.take() when out is passed --- astropy/utils/shapes.py | 2 +- astropy/utils/tests/test_shapes.py | 29 ++++++++++++++++++++++++++++- docs/changes/utils/19351.bugfix.rst | 1 + 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 docs/changes/utils/19351.bugfix.rst diff --git a/astropy/utils/shapes.py b/astropy/utils/shapes.py index 12cd520a7bb3..b42f22c07877 100644 --- a/astropy/utils/shapes.py +++ b/astropy/utils/shapes.py @@ -154,7 +154,7 @@ def take(self, indices, axis=None, out=None, mode="raise"): obviously, no output array can be given. """ if out is not None: - return NotImplementedError("cannot pass 'out' argument to 'take.") + raise NotImplementedError("cannot pass 'out' argument to 'take.") return self._apply("take", indices, axis=axis, mode=mode) diff --git a/astropy/utils/tests/test_shapes.py b/astropy/utils/tests/test_shapes.py index b4f1b0456b4c..8f14eabdba9d 100644 --- a/astropy/utils/tests/test_shapes.py +++ b/astropy/utils/tests/test_shapes.py @@ -7,7 +7,28 @@ from numpy.testing import assert_equal from astropy.utils.exceptions import AstropyDeprecationWarning -from astropy.utils.shapes import check_broadcast, simplify_basic_index, unbroadcast +from astropy.utils.shapes import ( + ShapedLikeNDArray, + check_broadcast, + simplify_basic_index, + unbroadcast, +) + + +class _ShapedDummy(ShapedLikeNDArray): + def __init__(self, values): + self._values = np.asarray(values) + + @property + def shape(self): + return self._values.shape + + def _apply(self, method, *args, **kwargs): + if callable(method): + values = method(self._values, *args, **kwargs) + else: + values = getattr(self._values, method)(*args, **kwargs) + return self.__class__(values) def test_check_broadcast_deprecation(): @@ -39,6 +60,12 @@ def test_unbroadcast(): assert z.shape == (3, 5) +def test_take_out_raises(): + shaped = _ShapedDummy([1, 2, 3, 4]) + with pytest.raises(NotImplementedError, match="cannot pass 'out' argument"): + shaped.take((0, 1), out=np.empty(2, dtype=int)) + + TEST_SHAPE = (13, 16, 4, 90) diff --git a/docs/changes/utils/19351.bugfix.rst b/docs/changes/utils/19351.bugfix.rst new file mode 100644 index 000000000000..1211519191af --- /dev/null +++ b/docs/changes/utils/19351.bugfix.rst @@ -0,0 +1 @@ +``ShapedLikeNDArray.take()`` now raises ``NotImplementedError`` when ``out`` is passed, instead of returning the exception object. From 2bab34c04f30e7c621cf9e833f2a15f1d308ae28 Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Fri, 6 Mar 2026 08:52:20 +0100 Subject: [PATCH 086/199] Backport PR #19362: Fix CompImageHDU header when initializing from a PrimaryHDU header --- astropy/io/fits/hdu/compressed/compressed.py | 6 ++- .../hdu/compressed/tests/test_compressed.py | 37 +++++++++++++++++++ docs/changes/io.fits/19362.bugfix.rst | 1 + 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 docs/changes/io.fits/19362.bugfix.rst diff --git a/astropy/io/fits/hdu/compressed/compressed.py b/astropy/io/fits/hdu/compressed/compressed.py index 8bac08aee0a1..52c21df0339a 100644 --- a/astropy/io/fits/hdu/compressed/compressed.py +++ b/astropy/io/fits/hdu/compressed/compressed.py @@ -335,7 +335,11 @@ def __init__( ) if header is not None and "SIMPLE" in header: - self.header["SIMPLE"] = header["SIMPLE"] + # SIMPLE and XTENSION are mutually exclusive per FITS standard + del self.header["XTENSION"] + del self.header["PCOUNT"] + del self.header["GCOUNT"] + self.header.insert(0, ("SIMPLE", header["SIMPLE"])) self.compression_type = compression_type self.tile_shape = _validate_tile_shape( diff --git a/astropy/io/fits/hdu/compressed/tests/test_compressed.py b/astropy/io/fits/hdu/compressed/tests/test_compressed.py index d7d5ec6cb917..cb09477e3bea 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_compressed.py +++ b/astropy/io/fits/hdu/compressed/tests/test_compressed.py @@ -1470,3 +1470,40 @@ def test_reserved_keywords_stripped(tmp_path): assert "ZBLANK" not in hdud[1].header assert "ZSCALE" not in hdud[1].header assert "ZZERO" not in hdud[1].header + + +def test_compimghdu_with_primary_header_no_dual_keywords(tmp_path): + """ + Regression test for https://github.com/astropy/astropy/issues/19361 + + When creating a CompImageHDU using a PrimaryHDU's header, the resulting + header should not contain both SIMPLE/ZSIMPLE and XTENSION/ZTENSION. + According to the FITS standard, a header must have either SIMPLE or + XTENSION, never both. Additionally, PCOUNT/GCOUNT are extension-only + keywords and should not appear in a primary-style header. + """ + # Create a PrimaryHDU with some data + hdu = fits.PrimaryHDU(data=np.arange(100, dtype=np.int32).reshape(10, 10)) + + # Create a CompImageHDU using the PrimaryHDU's header + comp_hdu = fits.CompImageHDU( + data=np.arange(100, dtype=np.int32).reshape(10, 10), + header=hdu.header, + ) + + # The image header should have SIMPLE (not XTENSION) when created from PrimaryHDU + assert "SIMPLE" in comp_hdu.header + assert "XTENSION" not in comp_hdu.header + # PCOUNT/GCOUNT are extension-only keywords + assert "PCOUNT" not in comp_hdu.header + assert "GCOUNT" not in comp_hdu.header + + # Write to file and check the raw bintable header + hdul = fits.HDUList([fits.PrimaryHDU(), comp_hdu]) + hdul.writeto(tmp_path / "test.fits") + + # Check the underlying bintable header has ZSIMPLE but not ZTENSION + with fits.open(tmp_path / "test.fits", disable_image_compression=True) as hdul: + bintable_header = hdul[1].header + assert "ZSIMPLE" in bintable_header + assert "ZTENSION" not in bintable_header diff --git a/docs/changes/io.fits/19362.bugfix.rst b/docs/changes/io.fits/19362.bugfix.rst new file mode 100644 index 000000000000..ce82b255df9e --- /dev/null +++ b/docs/changes/io.fits/19362.bugfix.rst @@ -0,0 +1 @@ +Fix a bug which caused CompImageHDU to have both SIMPLE and XTENSION keywords when initialized from a PrimaryHDU header. From b9c1ce39af78f740d1ddcde3e9c4ec9be057d247 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Fri, 6 Mar 2026 11:08:11 -0500 Subject: [PATCH 087/199] Backport PR #19364: Update APE14 link from v1 to v2 --- astropy/wcs/wcsapi/high_level_api.py | 2 +- astropy/wcs/wcsapi/low_level_api.py | 6 +++--- docs/wcs/index.rst | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/astropy/wcs/wcsapi/high_level_api.py b/astropy/wcs/wcsapi/high_level_api.py index 413e94f194ae..862dc4ebe228 100644 --- a/astropy/wcs/wcsapi/high_level_api.py +++ b/astropy/wcs/wcsapi/high_level_api.py @@ -62,7 +62,7 @@ class BaseHighLevelWCS(metaclass=abc.ABCMeta): Abstract base class for the high-level WCS interface. This is described in `APE 14: A shared Python interface for World Coordinate - Systems `_. + Systems `_. """ @property diff --git a/astropy/wcs/wcsapi/low_level_api.py b/astropy/wcs/wcsapi/low_level_api.py index 147a852f4614..47ea5e59bd31 100644 --- a/astropy/wcs/wcsapi/low_level_api.py +++ b/astropy/wcs/wcsapi/low_level_api.py @@ -11,7 +11,7 @@ class BaseLowLevelWCS(metaclass=abc.ABCMeta): Abstract base class for the low-level WCS interface. This is described in `APE 14: A shared Python interface for World Coordinate - Systems `_. + Systems `_. """ @property @@ -162,7 +162,7 @@ def world_axis_object_components(self): See the document `APE 14: A shared Python interface for World Coordinate Systems - `_ for examples. + `_ for examples. """ @property @@ -218,7 +218,7 @@ class and gets passed the positional and keyword arguments. It should See the document `APE 14: A shared Python interface for World Coordinate Systems - `_ for examples . + `_ for examples. """ # The following three properties have default fallback implementations, so diff --git a/docs/wcs/index.rst b/docs/wcs/index.rst index f8805463f64c..0ea51a293a37 100644 --- a/docs/wcs/index.rst +++ b/docs/wcs/index.rst @@ -22,7 +22,7 @@ For historical reasons and to support legacy software, `astropy.wcs` maintains two separate application interfaces. The ``High-Level API`` should be used by most applications. It abstracts out the underlying object and works transparently with other packages which support the -`Common Python Interface for WCS `_, +`Common Python Interface for WCS `_, allowing for a more flexible approach to the problem and avoiding the `limitations of the FITS WCS standard `_. From 5ef4abd152e065dc6d227e0b5fe2caa12f1e4617 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Fri, 6 Mar 2026 14:20:56 -0500 Subject: [PATCH 088/199] Backport PR #19369: Fix description of TDB-TT offset and location --- docs/time/index.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/time/index.rst b/docs/time/index.rst index a14c65183d21..36ddc8ab0c37 100644 --- a/docs/time/index.rst +++ b/docs/time/index.rst @@ -1142,12 +1142,11 @@ scale along with the auto-download feature:: request ``UT1-UTC`` for times beyond the range of IERS table data then the nearest available values will be provided. -In the case of the TDB to TT offset, most users need only provide the ``lon`` -and ``lat`` values when creating the |Time| object. If the +In the case of the TDB to TT offset, most users need only provide the +``location`` when creating the |Time| object. If the :attr:`~astropy.time.Time.delta_tdb_tt` attribute is not explicitly set, then the |PyERFA| routine `erfa.dtdb` will be used to compute the TDB to TT -offset. Note that if ``lon`` and ``lat`` are not explicitly initialized, -values of 0.0 degrees for both will be used. +offset. If ``location`` is not specified, the center of the Earth is assumed. Example ~~~~~~~ From 0ffed679f5aedd70898e79382a2d3392114c96b4 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Mon, 9 Mar 2026 20:02:48 -0400 Subject: [PATCH 089/199] Backport PR #19376: DOC: add examples for masked=False behavior in sigma_clip --- astropy/stats/sigma_clipping.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/astropy/stats/sigma_clipping.py b/astropy/stats/sigma_clipping.py index c8b3d70b0665..5a6536bdaae6 100644 --- a/astropy/stats/sigma_clipping.py +++ b/astropy/stats/sigma_clipping.py @@ -867,6 +867,26 @@ def sigma_clip( Note that along the other axis, no points would be clipped, as the standard deviation is higher. + + The behavior of ``masked=False`` depends on whether ``axis`` is + specified. When ``axis=None`` (the default), clipped values are + *removed* from the output, so the returned array may be shorter + than the input:: + + >>> import numpy as np + >>> from astropy.stats import sigma_clip + >>> x = np.ones(10) + >>> x[5] = 1000.0 + >>> clipped = sigma_clip(x, masked=False) + >>> len(clipped) # shorter than input: outlier was removed + 9 + + When ``axis`` is specified, clipped values are replaced with + ``np.nan`` instead:: + + >>> clipped_nan = sigma_clip(x, masked=False, axis=0) + >>> bool(np.isnan(clipped_nan[5])) # outlier replaced with nan + True """ sigclip = SigmaClip( sigma=sigma, From c1a57a9ad1bc3ab01991405009f40a45051b70c5 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:21:01 -0400 Subject: [PATCH 090/199] Backport PR #19384: add correct HST cloud link --- docs/io/fits/usage/cloud.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/io/fits/usage/cloud.rst b/docs/io/fits/usage/cloud.rst index 82e2b709debd..69d4112f2eee 100644 --- a/docs/io/fits/usage/cloud.rst +++ b/docs/io/fits/usage/cloud.rst @@ -93,7 +93,7 @@ Subsetting FITS files hosted in Amazon S3 cloud storage The FITS file used in the example above also happens to be available via Amazon cloud storage, where it is stored in a `public S3 bucket -`__ at the following location:: +`__ at the following location:: >>> s3_uri = "s3://stpubdata/hst/public/j8pu/j8pu0y010/j8pu0y010_drc.fits" From 4e29f5fbef5b6c789be6829b9a3db58342114d3d Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:44:54 -0400 Subject: [PATCH 091/199] Backport PR #19387: DOC: Ignore docutils.sourceforge.io in linkcheck --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index d152b2b720bf..1b942445d057 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -412,6 +412,7 @@ def _custom_edit_url( "https://hst-docs.stsci.edu", # CI blocked by service provider "https://www.tandfonline.com/", # 403 Client Error: Forbidden "https://stackoverflow.com/", # 403 Client Error: Forbidden + "https://docutils.sourceforge.io", # 403 Client Error: Forbidden "https://ieeexplore.ieee.org/", # 418 Client Error: I'm a teapot "https://pyfits.readthedocs.io/en/v3.2.1/", # defunct page in CHANGES.rst "https://pkgs.dev.azure.com/astropy-project", # defunct page in CHANGES.rst From ade8161e15a82717920a2e37ddbedb0d058442d1 Mon Sep 17 00:00:00 2001 From: Larry Bradley Date: Tue, 10 Mar 2026 13:44:12 -0400 Subject: [PATCH 092/199] Backport PR #19372: BUG: fix pickling of SigmaClip when Bottleneck is installed --- astropy/stats/nanfunctions.py | 51 +++++++++++++--------- astropy/stats/tests/test_sigma_clipping.py | 17 ++++++++ docs/changes/stats/19372.bugfix.rst | 1 + 3 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 docs/changes/stats/19372.bugfix.rst diff --git a/astropy/stats/nanfunctions.py b/astropy/stats/nanfunctions.py index efa9679d401b..20f64eb85def 100644 --- a/astropy/stats/nanfunctions.py +++ b/astropy/stats/nanfunctions.py @@ -101,29 +101,38 @@ def _apply_bottleneck( nanvar=np.nanvar, ) - def _dtype_dispatch(func_name): - # dispatch to bottleneck or numpy depending on the input array dtype - # this is done to workaround known accuracy bugs in bottleneck - # affecting float32 calculations - # see https://github.com/pydata/bottleneck/issues/379 - # see https://github.com/pydata/bottleneck/issues/462 - # see https://github.com/astropy/astropy/issues/17185 - # see https://github.com/astropy/astropy/issues/11492 - def wrapped(*args, **kwargs): - if args[0].dtype.str[1:] == "f8": - return bn_funcs[func_name](*args, **kwargs) - else: - return np_funcs[func_name](*args, **kwargs) + class _DtypeDispatch: + """Picklable dispatcher that routes to bottleneck or numpy based on dtype. + + Using a class instead of a closure ensures instances can be pickled, + which is required for pickling objects that store these functions as + attributes (e.g., SigmaClip._cenfunc_parsed). + + Bottleneck is only used for float64 (f8) arrays to work around known + accuracy bugs affecting float32 calculations: + - https://github.com/pydata/bottleneck/issues/379 + - https://github.com/pydata/bottleneck/issues/462 + - https://github.com/astropy/astropy/issues/17185 + - https://github.com/astropy/astropy/issues/11492 + """ - return wrapped + def __init__(self, func_name): + self.func_name = func_name - nansum = _dtype_dispatch("nansum") - nanmin = _dtype_dispatch("nanmin") - nanmax = _dtype_dispatch("nanmax") - nanmean = _dtype_dispatch("nanmean") - nanmedian = _dtype_dispatch("nanmedian") - nanstd = _dtype_dispatch("nanstd") - nanvar = _dtype_dispatch("nanvar") + def __call__(self, *args, **kwargs): + dt = args[0].dtype + if dt.kind == "f" and dt.itemsize == 8: + return bn_funcs[self.func_name](*args, **kwargs) + else: + return np_funcs[self.func_name](*args, **kwargs) + + nansum = _DtypeDispatch("nansum") + nanmin = _DtypeDispatch("nanmin") + nanmax = _DtypeDispatch("nanmax") + nanmean = _DtypeDispatch("nanmean") + nanmedian = _DtypeDispatch("nanmedian") + nanstd = _DtypeDispatch("nanstd") + nanvar = _DtypeDispatch("nanvar") else: nansum = np.nansum diff --git a/astropy/stats/tests/test_sigma_clipping.py b/astropy/stats/tests/test_sigma_clipping.py index 501273e07299..9e95bd77e2f6 100644 --- a/astropy/stats/tests/test_sigma_clipping.py +++ b/astropy/stats/tests/test_sigma_clipping.py @@ -1,5 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import pickle + import numpy as np import pytest from numpy.testing import assert_allclose, assert_equal @@ -722,3 +724,18 @@ def test_propagation_of_mask(): y = np.ma.masked_where(x > 1, x) assert_allclose(sigma_clipped_stats(y, grow=1), (1, 1, 0)) + + +def test_sigmaclip_pickle(): + """Regression test for gh-19343: SigmaClip cannot be pickled + when Bottleneck is installed.""" + + sigclip = SigmaClip(sigma=3.0, maxiters=5) + restored = pickle.loads(pickle.dumps(sigclip)) + assert restored.sigma == sigclip.sigma + assert restored.maxiters == sigclip.maxiters + # verify the restored object still works correctly + data = np.ma.array([1, 2, 3, 100, 4, 5]) + result = restored(data) + expected = sigclip(data) + np.testing.assert_array_equal(result, expected) diff --git a/docs/changes/stats/19372.bugfix.rst b/docs/changes/stats/19372.bugfix.rst new file mode 100644 index 000000000000..05d5b3d4b6aa --- /dev/null +++ b/docs/changes/stats/19372.bugfix.rst @@ -0,0 +1 @@ +Fixed pickling of ``SigmaClip`` objects when Bottleneck is installed. ``_dtype_dispatch`` returned a local closure which cannot be pickled; replaced with a picklable ``_DtypeDispatch`` class. From 8966d370ec8cd30f7da7df2dc134f6c40e34b2f4 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:16:07 -0400 Subject: [PATCH 093/199] Backport PR #19393: TST: Skip test_hub_proxy tests on OSX --- astropy/samp/tests/test_client.py | 7 +++++++ astropy/samp/tests/test_hub.py | 4 ++++ astropy/samp/tests/test_hub_proxy.py | 6 ++++++ astropy/samp/tests/test_hub_script.py | 2 ++ 4 files changed, 19 insertions(+) diff --git a/astropy/samp/tests/test_client.py b/astropy/samp/tests/test_client.py index d951c5bc6e7b..b8ba133b6f32 100644 --- a/astropy/samp/tests/test_client.py +++ b/astropy/samp/tests/test_client.py @@ -1,5 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import platform + import pytest # By default, tests should not use the internet. @@ -14,11 +16,13 @@ def setup_module(module): conf.use_internet = False +@pytest.mark.skipif(platform.system() == "Darwin", reason="Takes too long on OSX") def test_SAMPHubProxy(): """Test that SAMPHubProxy can be instantiated""" SAMPHubProxy() +@pytest.mark.skipif(platform.system() == "Darwin", reason="Takes too long on OSX") @pytest.mark.slow def test_SAMPClient(): """Test that SAMPClient can be instantiated""" @@ -26,6 +30,7 @@ def test_SAMPClient(): SAMPClient(proxy) +@pytest.mark.skipif(platform.system() == "Darwin", reason="Takes too long on OSX") def test_SAMPIntegratedClient(): """Test that SAMPIntegratedClient can be instantiated""" SAMPIntegratedClient() @@ -40,6 +45,7 @@ def samp_hub(): my_hub.stop() +@pytest.mark.skipif(platform.system() == "Darwin", reason="Takes too long on OSX") @pytest.mark.filterwarnings("ignore:unclosed Date: Wed, 11 Mar 2026 15:01:50 +0530 Subject: [PATCH 094/199] Backport PR #19368: BUG: Fix missing goto fail in create_parser after ValueError in parse_times.c --- astropy/time/src/parse_times.c | 1 + astropy/time/tests/test_fast_parser.py | 14 ++++++++++++++ docs/changes/time/19368.bugfix.rst | 1 + 3 files changed, 16 insertions(+) create mode 100644 docs/changes/time/19368.bugfix.rst diff --git a/astropy/time/src/parse_times.c b/astropy/time/src/parse_times.c index 0890b621bcd6..113ce2c3b31f 100644 --- a/astropy/time/src/parse_times.c +++ b/astropy/time/src/parse_times.c @@ -411,6 +411,7 @@ create_parser(PyObject *NPY_UNUSED(dummy), PyObject *args, PyObject *kwds) PyErr_SetString(PyExc_ValueError, "Parameter array must have 7 entries" "(year, month, day, hour, minute, integer second, fraction)"); + goto fail; } gufunc = (PyUFuncObject *)PyUFunc_FromFuncAndDataAndSignature( diff --git a/astropy/time/tests/test_fast_parser.py b/astropy/time/tests/test_fast_parser.py index 74cb0590893e..7e705beacc20 100644 --- a/astropy/time/tests/test_fast_parser.py +++ b/astropy/time/tests/test_fast_parser.py @@ -153,3 +153,17 @@ def test_fast_large_arrays(): assert t.size == 501 with pytest.raises(ValueError, match="Input values did not match any"): Time(["parrot"] * 1000) + + +def test_create_parser_invalid_pars_size(): + """Regression test: create_parser must raise ValueError cleanly + for parameter arrays that don't have exactly 7 entries, without + continuing execution with an active exception (missing goto fail).""" + import numpy as np + + from astropy.time import _parse_times + + # 6 entries instead of required 7 - should raise ValueError cleanly + bad_pars = np.zeros(6, dtype=_parse_times.dt_pars) + with pytest.raises(ValueError, match="Parameter array must have 7 entries"): + _parse_times.create_parser(bad_pars) diff --git a/docs/changes/time/19368.bugfix.rst b/docs/changes/time/19368.bugfix.rst new file mode 100644 index 000000000000..79a5070fe64c --- /dev/null +++ b/docs/changes/time/19368.bugfix.rst @@ -0,0 +1 @@ +Fixed missing ``goto fail`` in ``create_parser`` in ``parse_times.c`` after setting a ``ValueError`` for invalid parameter array size, preventing execution from continuing with an exception already set. From 71e0eadef77c5eac312678ffcfb2b2ed809c5bfb Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:06:48 -0400 Subject: [PATCH 095/199] Backport PR #19399: Fix imports from PR 19368 (BUG: Fix missing goto fail in create_parser after ValueError in parse_times.c) --- astropy/time/tests/test_fast_parser.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/astropy/time/tests/test_fast_parser.py b/astropy/time/tests/test_fast_parser.py index 7e705beacc20..ffa18600a8f2 100644 --- a/astropy/time/tests/test_fast_parser.py +++ b/astropy/time/tests/test_fast_parser.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from astropy.time import Time, TimeYearDayTime, conf +from astropy.time import Time, TimeYearDayTime, _parse_times, conf iso_times = [ "2000-02-29", @@ -159,9 +159,6 @@ def test_create_parser_invalid_pars_size(): """Regression test: create_parser must raise ValueError cleanly for parameter arrays that don't have exactly 7 entries, without continuing execution with an active exception (missing goto fail).""" - import numpy as np - - from astropy.time import _parse_times # 6 entries instead of required 7 - should raise ValueError cleanly bad_pars = np.zeros(6, dtype=_parse_times.dt_pars) From 7eb301d7e21079f2dc7cde89bb684d2be8e626c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Tue, 10 Mar 2026 20:47:05 +0100 Subject: [PATCH 096/199] Backport PR #19396: TST: Skip test_comp_image_quantize_level temporarily --- astropy/io/fits/hdu/compressed/tests/test_compressed.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astropy/io/fits/hdu/compressed/tests/test_compressed.py b/astropy/io/fits/hdu/compressed/tests/test_compressed.py index cb09477e3bea..17c6117806e2 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_compressed.py +++ b/astropy/io/fits/hdu/compressed/tests/test_compressed.py @@ -82,6 +82,7 @@ def test_comp_image(self, data, compression_type, quantize_level, byte_order): assert fd[1].header["NAXIS2"] == chdu.header["NAXIS2"] assert fd[1].header["BITPIX"] == chdu.header["BITPIX"] + @pytest.mark.skip(reason="FIXME: https://github.com/astropy/astropy/issues/19383") @pytest.mark.remote_data def test_comp_image_quantize_level(self): """ From 16cc412eadad1588fc7852cd3fb8f5856a8b42c6 Mon Sep 17 00:00:00 2001 From: Dhruv yadav Date: Wed, 11 Mar 2026 21:36:50 +0530 Subject: [PATCH 097/199] Backport PR #19358: BUG: Fix AssertionError for numpy bool index in _handle_index_argument --- astropy/table/_dataframes.py | 1 - astropy/table/tests/test_df.py | 18 ++++++++++++++++++ docs/changes/table/19358.bugfix.rst | 3 +++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 docs/changes/table/19358.bugfix.rst diff --git a/astropy/table/_dataframes.py b/astropy/table/_dataframes.py index 66948a473ae1..14a9897a68bb 100644 --- a/astropy/table/_dataframes.py +++ b/astropy/table/_dataframes.py @@ -154,7 +154,6 @@ def _handle_index_argument( elif index is None: return False else: - assert index is True raise ValueError("index=True requires a single-column primary key.") elif isinstance(index, str): diff --git a/astropy/table/tests/test_df.py b/astropy/table/tests/test_df.py index 197af4623673..fd88cb1b7e9d 100644 --- a/astropy/table/tests/test_df.py +++ b/astropy/table/tests/test_df.py @@ -328,6 +328,24 @@ def __init__(self, *args, **kwargs): with pytest.raises(ValueError, match="is not in the table columns"): self._to_dataframe(t, backend, use_legacy_pandas_api, index="not a column") + def test_to_df_index_numpy_bool_true_without_primary_key( + self, backend, use_legacy_pandas_api + ): + if backend != "pandas": + pytest.skip("requires pandas") + + t = table.QTable([[1, 2], [3, 4]], names=["a", "b"]) + + with pytest.raises( + ValueError, match="index=True requires a single-column primary key" + ): + self._to_dataframe( + t, + backend, + use_legacy_pandas_api, + index=np.bool_(True), + ) + def test_from_df_index(self, backend, use_legacy_pandas_api): """Test index handling in from_dataframe conversion.""" tm = Time([1998, 2002], format="jyear") diff --git a/docs/changes/table/19358.bugfix.rst b/docs/changes/table/19358.bugfix.rst new file mode 100644 index 000000000000..f837ee97b524 --- /dev/null +++ b/docs/changes/table/19358.bugfix.rst @@ -0,0 +1,3 @@ +Fixed ``AssertionError`` when passing ``numpy.bool_`` as the +``index`` argument to ``Table.to_pandas()`` or ``Table.to_dataframe()``. +The correct ``ValueError`` is now raised instead. From ae707e2d2ddaea9814d650de429de75fe1c4c527 Mon Sep 17 00:00:00 2001 From: Cyrus Date: Thu, 12 Mar 2026 20:22:23 +0530 Subject: [PATCH 098/199] Backport PR #19403: Fix missing f-string prefix in affine coordinate transform error message --- astropy/coordinates/transformations/affine.py | 2 +- docs/changes/coordinates/19403.bugfix.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/changes/coordinates/19403.bugfix.rst diff --git a/astropy/coordinates/transformations/affine.py b/astropy/coordinates/transformations/affine.py index ef5a8516f1f9..e08ecf8ecbfa 100644 --- a/astropy/coordinates/transformations/affine.py +++ b/astropy/coordinates/transformations/affine.py @@ -64,7 +64,7 @@ def _apply_transform(self, fromcoord, matrix, offset): raise TypeError( "Position information stored on coordinate frame " "is insufficient to do a full-space position " - "transformation (representation class: {data.__class__})" + f"transformation (representation class: {data.__class__})" ) if ( diff --git a/docs/changes/coordinates/19403.bugfix.rst b/docs/changes/coordinates/19403.bugfix.rst new file mode 100644 index 000000000000..31180d7ccf6f --- /dev/null +++ b/docs/changes/coordinates/19403.bugfix.rst @@ -0,0 +1 @@ +Fixed a missing f-string in a TypeError from ``BaseAffineTransform._apply_transform`` that caused ``{data.__class__}`` to appear literally in the error message. From d982d2ce81378a03dc94963c05d00b1eaea00b29 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:04:22 -0400 Subject: [PATCH 099/199] Backport PR #19392: TST: Cache file download for test_comp_image_quantize_level --- astropy/io/fits/hdu/compressed/tests/test_compressed.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/astropy/io/fits/hdu/compressed/tests/test_compressed.py b/astropy/io/fits/hdu/compressed/tests/test_compressed.py index 17c6117806e2..e5404c3187cc 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_compressed.py +++ b/astropy/io/fits/hdu/compressed/tests/test_compressed.py @@ -96,7 +96,9 @@ def test_comp_image_quantize_level(self): # Basically what scipy.datasets.ascent() does. fname = download_file( - "https://github.com/scipy/dataset-ascent/blob/main/ascent.dat?raw=true" + "https://github.com/scipy/dataset-ascent/blob/main/ascent.dat?raw=true", + cache=True, + show_progress=False, ) with open(fname, "rb") as f: scipy_data = np.array(pickle.load(f)) From ce6c9c3e5a3e70b2764ce288a49cf3cad9910cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Sat, 14 Mar 2026 10:09:01 +0100 Subject: [PATCH 100/199] Backport PR #19411: TST: avoid refering to astropy's own version in `minversion`'s doctest --- astropy/utils/introspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astropy/utils/introspection.py b/astropy/utils/introspection.py index fe9cd499af24..ad7bec8f2d2e 100644 --- a/astropy/utils/introspection.py +++ b/astropy/utils/introspection.py @@ -105,8 +105,8 @@ def minversion(module: ModuleType | str, version: str, inclusive: bool = True) - Examples -------- - >>> import astropy - >>> minversion(astropy, '0.4.4') + >>> import numpy + >>> minversion(numpy, '1.21.0') True """ if inspect.ismodule(module): From 7d46efb5b88a90d6f427b21a5bfc3d54e633facb Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Mon, 16 Mar 2026 09:14:45 +0100 Subject: [PATCH 101/199] Backport PR #19413: Remove unused nditer fallback code in io/fits --- astropy/io/fits/util.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/astropy/io/fits/util.py b/astropy/io/fits/util.py index ad74e20ab6bc..97253ccc4979 100644 --- a/astropy/io/fits/util.py +++ b/astropy/io/fits/util.py @@ -624,23 +624,9 @@ def _array_to_file_like(arr, fileobj): else: return - if hasattr(np, "nditer"): - # nditer version for non-contiguous arrays - for item in np.nditer(arr, order="C"): - fileobj.write(item.tobytes()) - else: - # Slower version for Numpy versions without nditer; - # The problem with flatiter is it doesn't preserve the original - # byteorder - byteorder = arr.dtype.byteorder - if (sys.byteorder == "little" and byteorder == ">") or ( - sys.byteorder == "big" and byteorder == "<" - ): - for item in arr.flat: - fileobj.write(item.byteswap().tobytes()) - else: - for item in arr.flat: - fileobj.write(item.tobytes()) + # nditer version for non-contiguous arrays + for item in np.nditer(arr, order="C"): + fileobj.write(item.tobytes()) def _write_string(f, s): From 238a2bc105411f4967fb4f17db497bf8f2f02d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 16 Mar 2026 11:09:23 +0100 Subject: [PATCH 102/199] Backport PR #19425: DOC: Ignore zenodo.org in linkcheck, fix pandas 404 --- docs/conf.py | 3 ++- docs/index_dev.rst | 2 +- docs/io/unified_table_text.rst | 12 ++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1b942445d057..c7d80d16f149 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -117,7 +117,7 @@ "pyerfa": ("https://pyerfa.readthedocs.io/en/stable/", None), "pytest": ("https://docs.pytest.org/en/stable/", None), "ipython": ("https://ipython.readthedocs.io/en/stable/", None), - "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + "pandas": ("https://pandas.pydata.org/docs/", None), "sphinx_automodapi": ( "https://sphinx-automodapi.readthedocs.io/en/stable/", None, @@ -408,6 +408,7 @@ def _custom_edit_url( "https://aa.usno.navy.mil/publications/docs/Circular_179.php", "http://data.astropy.org", "https://doi.org/", # CI blocked by service provider + "https://zenodo.org/", # 403 Client Error: Forbidden "https://ui.adsabs.harvard.edu", # CI blocked by service provider "https://hst-docs.stsci.edu", # CI blocked by service provider "https://www.tandfonline.com/", # 403 Client Error: Forbidden diff --git a/docs/index_dev.rst b/docs/index_dev.rst index ab0079910926..c5b078c4afd8 100644 --- a/docs/index_dev.rst +++ b/docs/index_dev.rst @@ -69,7 +69,7 @@ Details development/maintainers/index .. Note:: Parts of this guide were adapted from the - `pandas developer documentation `_. Astropy is grateful to the pandas team for their documentation efforts. + :ref:`pandas developer documentation `. Astropy is grateful to the ``pandas`` team for their documentation efforts. {%else%} diff --git a/docs/io/unified_table_text.rst b/docs/io/unified_table_text.rst index fdc95e616def..3ea7859abad2 100644 --- a/docs/io/unified_table_text.rst +++ b/docs/io/unified_table_text.rst @@ -163,10 +163,10 @@ column use: Pandas ------ -.. _pandas: https://pandas.pydata.org/pandas-docs/stable/index.html +.. _pandas: https://pandas.pydata.org/docs/ ``astropy`` `~astropy.table.Table` supports the ability to read or write tables -using some of the `I/O methods `_ +using some of the :ref:`I/O methods ` available within pandas_. This interface thus provides convenient wrappers to the following functions / methods: @@ -174,10 +174,10 @@ the following functions / methods: :header: "Format name", "Data Description", "Reader", "Writer" :widths: 25, 25, 25, 25 - ``pandas.csv``,`CSV `__,`read_csv() `_,`to_csv() `_ - ``pandas.json``,`JSON `__,`read_json() `_,`to_json() `_ - ``pandas.html``,`HTML `__,`read_html() `_,`to_html() `_ - ``pandas.fwf``,Fixed Width,`read_fwf() `_, + ``pandas.csv``,`CSV `__,:ref:`read_csv() `,:ref:`to_csv() ` + ``pandas.json``,`JSON `__,:ref:`read_json() `,:ref:`to_json() ` + ``pandas.html``,`HTML `__,:ref:`read_html() `,:ref:`to_html() ` + ``pandas.fwf``,Fixed Width,:ref:`read_fwf() `, **Notes**: From a2200a0c37ac5fcb88719281dfe3e4a3bb4a9072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 16 Mar 2026 13:16:39 +0100 Subject: [PATCH 103/199] Backport PR #19429: TST: stop building wheels against dev build deps in CI --- .github/workflows/publish.yml | 7 ------- pyproject.toml | 1 - 2 files changed, 8 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0c5b077e5231..d04f275fc07a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,14 +43,7 @@ jobs: anaconda_user: astropy anaconda_package: astropy anaconda_keep_n_latest: 10 - - # For nightly wheels as well as when building with the 'Build all wheels' label, we disable - # the build isolation and explicitly install the latest developer version of numpy as well as - # the latest stable versions of all other build-time dependencies. - # KEEP IN SYNC WITH [build-system] from pyproject.toml env: | - CIBW_BEFORE_BUILD: '${{ ((github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'Build all wheels')) && 'pip install -U --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple setuptools>=77.0.0 setuptools_scm cython numpy>=0.0.dev0 extension-helpers pyerfa') || '' }}' - CIBW_BUILD_FRONTEND: '${{ ((github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'Build all wheels')) && 'build; args: --no-isolation') || 'build' }}' EXTENSION_HELPERS_PY_LIMITED_API: 'cp311' test_extras: test diff --git a/pyproject.toml b/pyproject.toml index 13c788d7fde5..8eead2a01f2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -175,7 +175,6 @@ volint = "astropy.io.votable.volint:main" wcslint = "astropy.wcs.wcslint:main" [build-system] -# keep in sync with .github/workflows/publish.yml requires = ["setuptools>=77.0.0", "setuptools_scm>=8.0.0", "cython>=3.0.0, <4", From 4215b77b8b9fa88692dd37a41a0b7f2449c4a7a8 Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Mon, 16 Mar 2026 14:47:58 +0100 Subject: [PATCH 104/199] Backport PR #19416: Fix bug that caused compressed FITS file to become corrupted when editing headers --- astropy/io/fits/hdu/compressed/compressed.py | 18 +++++++++++++ .../hdu/compressed/tests/test_compressed.py | 27 +++++++++++++++++++ docs/changes/io.fits/19416.bugfix.rst | 1 + 3 files changed, 46 insertions(+) create mode 100644 docs/changes/io.fits/19416.bugfix.rst diff --git a/astropy/io/fits/hdu/compressed/compressed.py b/astropy/io/fits/hdu/compressed/compressed.py index 52c21df0339a..b256e15d18e1 100644 --- a/astropy/io/fits/hdu/compressed/compressed.py +++ b/astropy/io/fits/hdu/compressed/compressed.py @@ -736,3 +736,21 @@ def _data_size(self, value): # to None in __init__. if value is not None: raise RuntimeError("Cannot set CompImageHDU._data_size") + + @property + def _file(self): + # Delegate to bintable since that's where the actual file reference lives + if self._bintable is not None: + return self._bintable._file + return None + + @_file.setter + def _file(self, value): + # Delegate to bintable. When _flush_resize updates the HDU's file + # reference, this ensures the bintable's file reference is also updated. + # See https://github.com/astropy/astropy/issues/18612 + # + # Only set non-None values to avoid overwriting the bintable's valid + # file reference when the parent's __init__ sets _file = None. + if value is not None and self._bintable is not None: + self._bintable._file = value diff --git a/astropy/io/fits/hdu/compressed/tests/test_compressed.py b/astropy/io/fits/hdu/compressed/tests/test_compressed.py index e5404c3187cc..32ba39b7042e 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_compressed.py +++ b/astropy/io/fits/hdu/compressed/tests/test_compressed.py @@ -228,6 +228,33 @@ def test_open_comp_image_in_update_mode(self): # opening and closing it. assert mtime == os.stat(testfile).st_mtime + def test_update_comp_image_header(self): + """ + Regression test for https://github.com/astropy/astropy/issues/18612 + + Test that modifying the header of a compressed image in update mode + does not corrupt the file. + """ + data = np.arange(100, dtype=np.float32).reshape(10, 10) + + # Create a compressed FITS file + primary = fits.PrimaryHDU() + comp_hdu = fits.CompImageHDU(data, name="SCI", compression_type="RICE_1") + hdul = fits.HDUList([primary, comp_hdu]) + hdul.writeto(self.temp("test_comp_header.fits")) + + # Open in update mode and modify header + with fits.open(self.temp("test_comp_header.fits"), mode="update") as hdul: + hdul[1].header["TEST"] = (1, "Test keyword") + # The bug is triggered if a user explicitly flushes the file - which + # unnecessary but not wrong + hdul.flush() + + # Verify the file can be read and changes are present + with fits.open(self.temp("test_comp_header.fits")) as hdul: + assert hdul[1].header.get("TEST") == 1 + np.testing.assert_array_equal(hdul[1].data, data) + @pytest.mark.slow def test_open_scaled_in_update_mode_compressed(self): """ diff --git a/docs/changes/io.fits/19416.bugfix.rst b/docs/changes/io.fits/19416.bugfix.rst new file mode 100644 index 000000000000..2278a62e0a79 --- /dev/null +++ b/docs/changes/io.fits/19416.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug that caused compressed FITS files to become corrupted when opened in update mode and modifying the header. From 8fb0f741618faf3d4c0a48db31633075e5367776 Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Mon, 16 Mar 2026 14:57:49 +0100 Subject: [PATCH 105/199] Backport PR #19427: TST: Use alternate URL for ascent.dat --- astropy/io/fits/hdu/compressed/tests/test_compressed.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/astropy/io/fits/hdu/compressed/tests/test_compressed.py b/astropy/io/fits/hdu/compressed/tests/test_compressed.py index e5404c3187cc..d3f3fe01a4b7 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_compressed.py +++ b/astropy/io/fits/hdu/compressed/tests/test_compressed.py @@ -82,7 +82,6 @@ def test_comp_image(self, data, compression_type, quantize_level, byte_order): assert fd[1].header["NAXIS2"] == chdu.header["NAXIS2"] assert fd[1].header["BITPIX"] == chdu.header["BITPIX"] - @pytest.mark.skip(reason="FIXME: https://github.com/astropy/astropy/issues/19383") @pytest.mark.remote_data def test_comp_image_quantize_level(self): """ @@ -96,7 +95,7 @@ def test_comp_image_quantize_level(self): # Basically what scipy.datasets.ascent() does. fname = download_file( - "https://github.com/scipy/dataset-ascent/blob/main/ascent.dat?raw=true", + "https://raw.githubusercontent.com/scipy/dataset-ascent/main/ascent.dat", cache=True, show_progress=False, ) From dcac2122e3d32fc404e01a59ac645d4864cf1de5 Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Mon, 16 Mar 2026 15:28:47 +0100 Subject: [PATCH 106/199] Backport PR #19363: Add support for reading compressed FITS files with UNCOMPRESSED_DATA --- .../fits/hdu/compressed/_tiled_compression.py | 85 +++++++++++++----- .../tests/test_compression_failures.py | 88 +++++++++++++++++++ docs/changes/io.fits/19363.bugfix.rst | 1 + 3 files changed, 150 insertions(+), 24 deletions(-) create mode 100644 docs/changes/io.fits/19363.bugfix.rst diff --git a/astropy/io/fits/hdu/compressed/_tiled_compression.py b/astropy/io/fits/hdu/compressed/_tiled_compression.py index 2ef5b90ebefa..24a76d9e570b 100644 --- a/astropy/io/fits/hdu/compressed/_tiled_compression.py +++ b/astropy/io/fits/hdu/compressed/_tiled_compression.py @@ -206,7 +206,12 @@ def _check_compressed_header(header): raise TypeError(f"{kw} should be an integer") for suffix in range(1, header["TFIELDS"] + 1): - if header.get(f"TTYPE{suffix}", "").endswith("COMPRESSED_DATA"): + # Only validate COMPRESSED_DATA and GZIP_COMPRESSED_DATA columns. + # UNCOMPRESSED_DATA columns can have floating-point formats (PE, PD, etc.) + if header.get(f"TTYPE{suffix}", "") in ( + "COMPRESSED_DATA", + "GZIP_COMPRESSED_DATA", + ): for valid in ["PB", "PI", "PJ", "QB", "QI", "QJ"]: if header[f"TFORM{suffix}"].startswith((valid, f"1{valid}")): break @@ -262,6 +267,10 @@ def _column_dtype(compressed_coldefs, column_name): dtype = ">i2" elif tform[1] == "J": dtype = ">i4" + elif tform[1] == "E": + dtype = ">f4" + elif tform[1] == "D": + dtype = ">f8" return np.dtype(dtype) @@ -362,6 +371,8 @@ def decompress_image_data_section( gzip_compressed_data_column = None gzip_compressed_data_dtype = None + uncompressed_data_column = None + uncompressed_data_dtype = None # If all the data is requested, read in all the heap. if tuple(buffer_shape) == tuple(data_shape): @@ -383,35 +394,61 @@ def decompress_image_data_section( settings = _update_tile_settings(settings, compression_type, actual_tile_shape) if compressed_data_column[row_index][0] == 0: - if gzip_compressed_data_column is None: - gzip_compressed_data_column = np.array( - compressed_data["GZIP_COMPRESSED_DATA"] - ) - gzip_compressed_data_dtype = _column_dtype( - compressed_coldefs, "GZIP_COMPRESSED_DATA" - ) - # When quantizing floating point data, sometimes the data will not # quantize efficiently. In these cases the raw floating point data can # be losslessly GZIP compressed and stored in the `GZIP_COMPRESSED_DATA` - # column. + # column, or stored uncompressed in the `UNCOMPRESSED_DATA` column. - cdata = _get_data_from_heap( - bintable, - *gzip_compressed_data_column[row_index], - gzip_compressed_data_dtype, - heap_cache=heap_cache, - ) + if "GZIP_COMPRESSED_DATA" in compressed_coldefs.dtype.names: + if gzip_compressed_data_column is None: + gzip_compressed_data_column = np.array( + compressed_data["GZIP_COMPRESSED_DATA"] + ) + gzip_compressed_data_dtype = _column_dtype( + compressed_coldefs, "GZIP_COMPRESSED_DATA" + ) - tile_buffer = _decompress_tile(cdata, algorithm="GZIP_1") + cdata = _get_data_from_heap( + bintable, + *gzip_compressed_data_column[row_index], + gzip_compressed_data_dtype, + heap_cache=heap_cache, + ) - tile_data = _finalize_array( - tile_buffer, - bitpix=zbitpix, - tile_shape=actual_tile_shape, - algorithm="GZIP_1", - lossless=True, - ) + tile_buffer = _decompress_tile(cdata, algorithm="GZIP_1") + + tile_data = _finalize_array( + tile_buffer, + bitpix=zbitpix, + tile_shape=actual_tile_shape, + algorithm="GZIP_1", + lossless=True, + ) + + elif "UNCOMPRESSED_DATA" in compressed_coldefs.dtype.names: + if uncompressed_data_column is None: + uncompressed_data_column = np.array( + compressed_data["UNCOMPRESSED_DATA"] + ) + uncompressed_data_dtype = _column_dtype( + compressed_coldefs, "UNCOMPRESSED_DATA" + ) + + tile_data = _get_data_from_heap( + bintable, + *uncompressed_data_column[row_index], + uncompressed_data_dtype, + heap_cache=heap_cache, + ) + + # Data is already uncompressed, just reshape it + tile_data = tile_data.reshape(actual_tile_shape) + + else: + raise ValueError( + "COMPRESSED_DATA column has zero length but neither " + "GZIP_COMPRESSED_DATA nor UNCOMPRESSED_DATA column exists" + ) else: cdata = _get_data_from_heap( diff --git a/astropy/io/fits/hdu/compressed/tests/test_compression_failures.py b/astropy/io/fits/hdu/compressed/tests/test_compression_failures.py index 048bcb223231..c26a47095f6f 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_compression_failures.py +++ b/astropy/io/fits/hdu/compressed/tests/test_compression_failures.py @@ -171,3 +171,91 @@ def test_header_value_no_double_int_image(self, kw): compress_image_data( hdu.data, hdu.compression_type, bintable.header, bintable.columns ) + + @pytest.mark.parametrize( + ("numpy_dtype", "fits_format", "big_endian_dtype"), + [ + (np.int16, "1PI", ">i2"), + (np.float32, "1PE", ">f4"), + ], + ) + def test_uncompressed_data_column( + self, tmp_path, numpy_dtype, fits_format, big_endian_dtype + ): + """ + Regression test for https://github.com/astropy/astropy/issues/15477 + + Test that data stored in UNCOMPRESSED_DATA column is correctly read. + UNCOMPRESSED_DATA columns can have integer (PI, PJ) or floating-point + (PE, PD) formats. This tests the case where some tiles have their data + in UNCOMPRESSED_DATA instead of COMPRESSED_DATA (which can happen when + compression fails for certain tiles). + """ + tile_size = 5 + original_data = np.arange(100, dtype=numpy_dtype).reshape((10, 10)) + hdu = fits.CompImageHDU( + data=original_data, + compression_type="RICE_1", + tile_shape=(tile_size, tile_size), + ) + + filename = tmp_path / "test_uncompressed_tiles.fits" + hdu.writeto(filename) + + # Read and modify: put first tile's data in UNCOMPRESSED_DATA instead. + # This simulates a file created by another tool (like cfitsio directly). + with fits.open(filename, disable_image_compression=True) as hdul: + orig_table = hdul[1].data + orig_header = hdul[1].header.copy() + orig_cols = hdul[1].columns + n_tiles = len(orig_table) + + compressed_col_data = np.empty(n_tiles, dtype=object) + uncompressed_col_data = np.empty(n_tiles, dtype=object) + + for i in range(n_tiles): + if i == 0: + # First tile: put in UNCOMPRESSED_DATA, leave COMPRESSED_DATA empty + compressed_col_data[i] = np.array([], dtype=np.uint8) + tile_data = original_data[:tile_size, :tile_size].flatten() + uncompressed_col_data[i] = tile_data.astype(big_endian_dtype) + else: + # Other tiles: keep in COMPRESSED_DATA + compressed_col_data[i] = orig_table["COMPRESSED_DATA"][i] + uncompressed_col_data[i] = np.array([], dtype=numpy_dtype) + + # Build column list: COMPRESSED_DATA, UNCOMPRESSED_DATA, plus any + # other columns from the original (ZSCALE, ZZERO for float data) + new_col_list = [ + fits.Column( + name="COMPRESSED_DATA", + format=orig_cols["COMPRESSED_DATA"].format, + array=compressed_col_data, + ), + fits.Column( + name="UNCOMPRESSED_DATA", + format=f"{fits_format}({tile_size * tile_size})", + array=uncompressed_col_data, + ), + ] + + # Preserve other columns (ZSCALE, ZZERO, etc.) needed for quantized data + for col in orig_cols: + if col.name not in ("COMPRESSED_DATA", "GZIP_COMPRESSED_DATA"): + new_col_list.append( + fits.Column( + name=col.name, + format=col.format, + array=orig_table[col.name], + ) + ) + + new_cols = fits.ColDefs(new_col_list) + new_table = fits.BinTableHDU.from_columns(new_cols, header=orig_header) + modified_filename = tmp_path / "test_uncompressed_tiles_modified.fits" + fits.HDUList([hdul[0].copy(), new_table]).writeto(modified_filename) + + # Verify the data is correctly read. Use atol=0.1 to account for + # quantization noise in float data (ZSCALE is typically ~0.13 for this data) + with fits.open(modified_filename) as hdul: + np.testing.assert_allclose(hdul[1].data, original_data, atol=0.1) diff --git a/docs/changes/io.fits/19363.bugfix.rst b/docs/changes/io.fits/19363.bugfix.rst new file mode 100644 index 000000000000..a0c61a1ebf9e --- /dev/null +++ b/docs/changes/io.fits/19363.bugfix.rst @@ -0,0 +1 @@ +Fix support for reading FITS files using image tile compression with UNCOMPRESSED_DATA columns. From adbc30334dadbd2b6fe4b1d040129df83dc16b60 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:21:35 -0400 Subject: [PATCH 107/199] Backport PR #19432: TST: Ensure that Latitude([lon, lon]) raises --- astropy/coordinates/tests/test_angles.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/astropy/coordinates/tests/test_angles.py b/astropy/coordinates/tests/test_angles.py index 0c5f17290df7..de0073ce183c 100644 --- a/astropy/coordinates/tests/test_angles.py +++ b/astropy/coordinates/tests/test_angles.py @@ -789,6 +789,10 @@ def test_lon_as_lat(): lat = Latitude([20], "deg") lat[0] = lon + # Also check list of longitudes (regression test for gh-11757). + with pytest.raises(TypeError, match="cannot be created from"): + Latitude([lon, lon]) + # Check we can work around the Lat vs Long checks by casting explicitly to Angle. lat = Latitude(Angle(lon)) assert lat.value == 10.0 From 37284b21232d852dcc89d9fa4aa47c987af84678 Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Mon, 16 Mar 2026 09:38:55 +0100 Subject: [PATCH 108/199] Backport PR #19414: Replace reduce(operator.mul) and np.prod with math.prod in io/fits --- astropy/io/fits/column.py | 14 +++++--------- astropy/io/fits/file.py | 7 +++---- astropy/io/fits/fitsrec.py | 13 +++++-------- astropy/io/fits/hdu/groups.py | 3 ++- astropy/io/fits/hdu/image.py | 4 ++-- astropy/io/fits/hdu/table.py | 3 ++- 6 files changed, 19 insertions(+), 25 deletions(-) diff --git a/astropy/io/fits/column.py b/astropy/io/fits/column.py index ff513628367f..3203931646fc 100644 --- a/astropy/io/fits/column.py +++ b/astropy/io/fits/column.py @@ -1,15 +1,14 @@ # Licensed under a 3-clause BSD style license - see PYFITS.rst import copy +import math import numbers -import operator import re import sys import warnings import weakref from collections import OrderedDict from contextlib import suppress -from functools import reduce from itertools import pairwise from textwrap import indent @@ -1203,7 +1202,7 @@ def _verify_keywords( # TDIMs have different meaning for VLA format, # no warning should be thrown msg = None - elif reduce(operator.mul, dims_tuple) > format.repeat: + elif math.prod(dims_tuple) > format.repeat: msg = ( f"The repeat count of the column format {name!r} for column " f"{format!r} is fewer than the number of elements per the TDIM " @@ -2112,9 +2111,7 @@ def __setitem__(self, key, value): else: value = np.array(value, dtype=self.element_dtype) np.ndarray.__setitem__(self, key, value) - nelem = value.shape - len_value = np.prod(nelem) - self.max = max(self.max, len_value) + self.max = max(self.max, value.size) def tolist(self): return [list(item) for item in super().tolist()] @@ -2266,8 +2263,7 @@ def _makep(array, descr_output, format, nrows=None): else: data_output[idx] = np.array(rowval, dtype=format.dtype) - nelem = data_output[idx].shape - descr_output[idx, 0] = np.prod(nelem) + descr_output[idx, 0] = data_output[idx].size descr_output[idx, 1] = _offset # detect overflow when using P format @@ -2484,7 +2480,7 @@ def _convert_record2fits(format): ndims = len(shape) repeat = 1 if ndims > 0: - nel = np.array(shape, dtype="i8").prod() + nel = math.prod(shape) if nel > 1: repeat = nel diff --git a/astropy/io/fits/file.py b/astropy/io/fits/file.py index ca5912397fe2..520252910de1 100644 --- a/astropy/io/fits/file.py +++ b/astropy/io/fits/file.py @@ -4,15 +4,14 @@ import gzip import http.client import io +import math import mmap -import operator import os import re import sys import tempfile import warnings import zipfile -from functools import reduce import numpy as np @@ -356,7 +355,7 @@ def readarray(self, size=None, offset=0, dtype=np.uint8, shape=None): shape = (size // dtype.itemsize,) if size and shape: - actualsize = np.prod(shape) * dtype.itemsize + actualsize = math.prod(shape) * dtype.itemsize if actualsize > size: raise ValueError( @@ -430,7 +429,7 @@ def readarray(self, size=None, offset=0, dtype=np.uint8, shape=None): shape=shape, dtype=dtype, offset=offset, buffer=self._mmap ) else: - count = reduce(operator.mul, shape) + count = math.prod(shape) self._file.seek(offset) data = _array_from_file(self._file, dtype, count) return data.reshape(shape) diff --git a/astropy/io/fits/fitsrec.py b/astropy/io/fits/fitsrec.py index 1bdba8b3e5dc..ec7f7602e3b0 100644 --- a/astropy/io/fits/fitsrec.py +++ b/astropy/io/fits/fitsrec.py @@ -1,11 +1,10 @@ # Licensed under a 3-clause BSD style license - see PYFITS.rst import copy -import operator +import math import warnings import weakref from contextlib import suppress -from functools import reduce import numpy as np @@ -842,9 +841,7 @@ def _convert_p(self, column, field, recformat): if vla_shape[0] == 1: dummy[idx] = dummy[idx].reshape(1, len(dummy[idx])) else: - vla_dim = vla_shape[1:] - vla_first = int(len(dummy[idx]) / np.prod(vla_dim)) - dummy[idx] = dummy[idx].reshape((vla_first,) + vla_dim) + dummy[idx] = dummy[idx].reshape((-1,) + vla_shape[1:]) dummy[idx] = dummy[idx].view(dummy[idx].dtype.newbyteorder(">")) # Each array in the field may now require additional @@ -942,7 +939,7 @@ def _convert_other(self, column, field, recformat): # ignore dim and don't convert dim = None else: - nitems = reduce(operator.mul, dim) + nitems = math.prod(dim) if _str: actual_nitems = field.itemsize elif len(field.shape) == 1: @@ -1020,7 +1017,7 @@ def _convert_other(self, column, field, recformat): if dim and not isinstance(recformat, _FormatP): # Apply the new field item dimensions - nitems = reduce(operator.mul, dim) + nitems = math.prod(dim) if field.ndim > 1: field = field[:, :nitems] if _str: @@ -1160,7 +1157,7 @@ def _scale_back(self, update_heap_pointers=True): # The VLA has potentially been updated, so we need to # update the array descriptors raw_field[:] = 0 # reset - npts = [np.prod(arr.shape) for arr in self._converted[name]] + npts = [arr.size for arr in self._converted[name]] raw_field[: len(npts), 0] = npts raw_field[1:, 1] = ( diff --git a/astropy/io/fits/hdu/groups.py b/astropy/io/fits/hdu/groups.py index f88d0bad66b0..0906f886a153 100644 --- a/astropy/io/fits/hdu/groups.py +++ b/astropy/io/fits/hdu/groups.py @@ -1,5 +1,6 @@ # Licensed under a 3-clause BSD style license - see PYFITS.rst +import math import sys import numpy as np @@ -339,7 +340,7 @@ def columns(self): bscales.append(self._header.get("BSCALE")) bzeros.append(self._header.get("BZEROS")) data_shape = self.shape[:-1] - formats.append(str(int(np.prod(data_shape))) + format) + formats.append(str(math.prod(data_shape)) + format) dim.append(data_shape) parnames = _unique_parnames(parnames) diff --git a/astropy/io/fits/hdu/image.py b/astropy/io/fits/hdu/image.py index b42741102c99..bb40e6a7b87a 100644 --- a/astropy/io/fits/hdu/image.py +++ b/astropy/io/fits/hdu/image.py @@ -509,8 +509,8 @@ def _scale_internal( min = self.data.min().compute() max = self.data.max().compute() else: - min = np.minimum.reduce(self.data.flat) - max = np.maximum.reduce(self.data.flat) + min = self.data.min() + max = self.data.max() if _type == np.uint8: # uint8 case _zero = min diff --git a/astropy/io/fits/hdu/table.py b/astropy/io/fits/hdu/table.py index e9d2f2f293fe..14b5f2981461 100644 --- a/astropy/io/fits/hdu/table.py +++ b/astropy/io/fits/hdu/table.py @@ -3,6 +3,7 @@ import contextlib import csv +import math import operator import os import re @@ -1401,7 +1402,7 @@ def format_value(col, val): idx += vla_len elif dtype[col].shape: # This is an array column - array_size = int(np.multiply.reduce(dtype[col].shape)) + array_size = math.prod(dtype[col].shape) slice_ = slice(idx, idx + array_size) idx += array_size else: From 6610dcdd93f4ce129531475cf9dfb7f9d99b3c04 Mon Sep 17 00:00:00 2001 From: Cyrus Date: Wed, 18 Mar 2026 20:16:30 +0530 Subject: [PATCH 109/199] Backport PR #19404: Fix silent data corruption in FITS_rec slice assignment with negative indices --- astropy/io/fits/fitsrec.py | 10 ++--- astropy/io/fits/tests/test_table.py | 59 ++++++++++++++++++++++++++- docs/changes/io.fits/19404.bugfix.rst | 4 ++ 3 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 docs/changes/io.fits/19404.bugfix.rst diff --git a/astropy/io/fits/fitsrec.py b/astropy/io/fits/fitsrec.py index ec7f7602e3b0..5d499af927dd 100644 --- a/astropy/io/fits/fitsrec.py +++ b/astropy/io/fits/fitsrec.py @@ -574,13 +574,9 @@ def __setitem__(self, key, value): return if isinstance(key, slice): - end = min(len(self), key.stop or len(self)) - end = max(0, end) - start = max(0, key.start or 0) - end = min(end, start + len(value)) - - for idx in range(start, end): - self.__setitem__(idx, value[idx - start]) + start, stop, step = key.indices(len(self)) + for idx, val in zip(range(start, stop, step), value, strict=True): + self.__setitem__(idx, val) return if isinstance(value, FITS_record): diff --git a/astropy/io/fits/tests/test_table.py b/astropy/io/fits/tests/test_table.py index c91ad65eaa26..87bfe501203d 100644 --- a/astropy/io/fits/tests/test_table.py +++ b/astropy/io/fits/tests/test_table.py @@ -1176,6 +1176,63 @@ def test_row_setitem(self): assert row["c1"] == 12 assert row["c2"] == "xyz" + def test_fitsrec_negative_slice_setitem(self): + """Regression test for negative slice assignment in FITS_rec. + + Negative slices like data[-2:] were writing to wrong row indices + because negative start/stop values were clamped to 0 instead of + being resolved relative to the array length. + """ + + def _make_data(x=None, y=None): + x = x if x is not None else [1.0, 2.0, 3.0, 4.0, 5.0] + cols = [fits.Column(name="x", format="D", array=x)] + if y is not None: + cols.append(fits.Column(name="y", format="J", array=y)) + return fits.BinTableHDU.from_columns(cols) + + src_data = _make_data(x=[99.0, 88.0], y=[990, 880]).data + + # Test negative start slice: data[-2:] + data = _make_data(y=[10, 20, 30, 40, 50]).data + data[-2:] = [src_data[0], src_data[1]] + assert list(data["x"]) == [1.0, 2.0, 3.0, 99.0, 88.0] + assert list(data["y"]) == [10, 20, 30, 990, 880] + + # Test negative start and stop: data[-3:-1] + data = _make_data(y=[10, 20, 30, 40, 50]).data + data[-3:-1] = [src_data[0], src_data[1]] + assert list(data["x"]) == [1.0, 2.0, 99.0, 88.0, 5.0] + assert list(data["y"]) == [10, 20, 990, 880, 50] + + # Test positive slice still works + data = _make_data().data + data[0:2] = [src_data[0], src_data[1]] + assert list(data["x"]) == [99.0, 88.0, 3.0, 4.0, 5.0] + + # Test step slice: data[::2] + data = _make_data().data + data[::2] = [src_data[0], src_data[1], src_data[0]] + assert list(data["x"]) == [99.0, 2.0, 88.0, 4.0, 99.0] + + # Test negative step slice: data[::-1] + data = _make_data().data + reversed_src = _make_data(x=[55.0, 44.0, 33.0, 22.0, 11.0]).data + data[::-1] = [reversed_src[i] for i in range(5)] + assert list(data["x"]) == [11.0, 22.0, 33.0, 44.0, 55.0] + + # Test round-trip through a FITS file + hdu = _make_data() + hdu.data[-2:] = [src_data[0], src_data[1]] + hdu.writeto(self.temp("test_neg_slice.fits")) + saved = fits.getdata(self.temp("test_neg_slice.fits")) + assert list(saved["x"]) == [1.0, 2.0, 3.0, 99.0, 88.0] + + # Test that mismatched lengths raise ValueError (like NumPy does) + data = _make_data().data + with pytest.raises(ValueError): + data[-2:] = [src_data[0]] # slice covers 2 rows but only 1 value given + def test_fits_record_len(self): counts = np.array([312, 334, 308, 317]) names = np.array(["NGC1", "NGC2", "NGC3", "NCG4"]) @@ -1314,7 +1371,7 @@ def test_assign_multiple_rows_to_table(self): # Assign the 4 rows from the second table to rows 5 thru 8 of the # new table. Note that the last row of the new table will still be # initialized to the default values. - tbhdu2.data[4:] = tbhdu.data + tbhdu2.data[4:8] = tbhdu.data # Verify that all ndarray objects within the HDU reference the # same ndarray. diff --git a/docs/changes/io.fits/19404.bugfix.rst b/docs/changes/io.fits/19404.bugfix.rst new file mode 100644 index 000000000000..a28512f6d491 --- /dev/null +++ b/docs/changes/io.fits/19404.bugfix.rst @@ -0,0 +1,4 @@ +Fixed silent data corruption in ``FITS_rec.__setitem__`` when using negative +slice indices. Assigning to slices like ``data[-2:] = new_rows`` previously +wrote to the wrong rows because negative indices were clamped to 0 instead of +being resolved relative to the array length. From 24f0496e2c3414499b3336cc55e0a93f31ded8d1 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:50:22 -0400 Subject: [PATCH 110/199] Backport PR #19375: DOC: Warn against invalid extra bytes in FITS files --- docs/io/fits/performance.inc.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/io/fits/performance.inc.rst b/docs/io/fits/performance.inc.rst index 3ac7bc545a30..cb3827a8dae8 100644 --- a/docs/io/fits/performance.inc.rst +++ b/docs/io/fits/performance.inc.rst @@ -8,6 +8,9 @@ Performance Tips ================ +Use dask for lazy compute +------------------------- + It is possible to set the data array for :class:`~astropy.io.fits.PrimaryHDU` and :class:`~astropy.io.fits.ImageHDU` to a `dask `_ array. If this is written to disk, the dask array will be computed as it is being @@ -21,11 +24,22 @@ written, which will avoid using excessive memory: >>> hdu = fits.PrimaryHDU(data=array) >>> hdu.writeto('test_dask.fits') +Arbitrary padding end of file will degrade performance +------------------------------------------------------ + +As discussed in detail in +`GitHub issue 19296 `_, +arbitrary padding at the end of a FITS file might cause the parser +to inefficiently search for the END card of the next header. +Therefore, we recommend that astropy users to not blindly open +untrusted large FITS files without independently verifying their fidelity +first. + .. TODO: determine whether the following is quantitatively true, and either .. uncomment or remove. -.. Performance Tips -.. ================ +.. Turn off memmap to run faster but use more memory +.. ------------------------------------------------- .. .. By default, :func:`astropy.io.fits.open` will open files using memory-mapping, .. which means that the data is not necessarily read into memory until it is From 74c49252c22e54d967293ee8dd4d9e3a7de5d371 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:57:51 -0400 Subject: [PATCH 111/199] Manual backport of #19461 onto v7.2.x --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8eead2a01f2b..63571e0349b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ keywords = [ dependencies = [ "numpy>=1.24, <2.7", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires - "astropy-iers-data>=0.2026.2.23.0.48.33", + "astropy-iers-data>=0.2026.3.16.0.53.33", "PyYAML>=6.0.0", "packaging>=22.0.0", ] From 76212bc32bd6efeb1e1add89f6c648d324c725bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Wed, 18 Mar 2026 17:12:27 +0100 Subject: [PATCH 112/199] Backport PR #19462 to v7.2.x (MNT: sort runtime dependencies alphabetically to stabilize auto upgrades to astropy-iers-data) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 63571e0349b3..2d58b4c9fed8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,11 +43,11 @@ keywords = [ "ascii", ] dependencies = [ + "astropy-iers-data>=0.2026.3.16.0.53.33", "numpy>=1.24, <2.7", + "packaging>=22.0.0", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires - "astropy-iers-data>=0.2026.3.16.0.53.33", "PyYAML>=6.0.0", - "packaging>=22.0.0", ] [project.optional-dependencies] From 5973a8a66710382be50bf40a3228e1ce18907209 Mon Sep 17 00:00:00 2001 From: Cyrus Date: Thu, 19 Mar 2026 20:18:48 +0530 Subject: [PATCH 113/199] Backport PR #19450: Fix table index getting corrupted when a row assignment fails --- astropy/table/index.py | 22 ++++++++++++++++-- astropy/table/tests/test_index.py | 35 +++++++++++++++++++++++++++++ docs/changes/table/19450.bugfix.rst | 2 ++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 docs/changes/table/19450.bugfix.rst diff --git a/astropy/table/index.py b/astropy/table/index.py index fd357d981bb3..d78f25841e7e 100644 --- a/astropy/table/index.py +++ b/astropy/table/index.py @@ -571,10 +571,28 @@ def replace(self, row, col_name, val): val : col.info.dtype Value to insert at specified row of col """ + # Save the original key before removing so it can be restored if the + # subsequent add fails (e.g. incompatible dtype). Without this, a + # failed assignment silently drops the row from the index even though + # the underlying column data was never changed. + orig_key = tuple(c[row] for c in self.columns) self.remove_row(row, reorder=False) - key = [c[row] for c in self.columns] + key = list(orig_key) key[self.col_position(col_name)] = val - self.data.add(tuple(key), row) + try: + self.data.add(tuple(key), row) + except Exception as exc: + self.data.add(orig_key, row) + if not isinstance(exc, ValueError): + # Some index engines (BST, SCEngine) raise UFuncTypeError or + # other TypeError subclasses when comparing incompatible types + # during the sorted-insert. Convert to ValueError so callers + # always get a consistent, descriptive error. + raise ValueError( + f"Cannot update index for column '{col_name}' " + f"with value {val!r}: {exc}" + ) from exc + raise def replace_rows(self, col_slice): """ diff --git a/astropy/table/tests/test_index.py b/astropy/table/tests/test_index.py index f55a643eb45f..73be1ee68c5d 100644 --- a/astropy/table/tests/test_index.py +++ b/astropy/table/tests/test_index.py @@ -848,3 +848,38 @@ def test_unique_indices_after_multicol_index_slice(): t2 = t[:1] assert len(t2.indices) == 1 # without fix would be 2, both with id ("a", "b"). assert t2.indices[0].id == ("a", "b") + + +def test_index_not_corrupted_on_failed_row_assignment(engine): + """Regression test: index must survive a failed row assignment. + + When ``table[row] = values`` raises because one of the values is + incompatible with its column dtype, the table index was left in an + inconsistent state. Specifically, ``Index.replace`` removed the + existing key from the sorted array *before* trying to insert the new + one; if the insert failed the old key was permanently gone even though + the column data was never changed. + + After the fix the index must round-trip correctly: the original key is + still findable and no ghost key is present. The test is run for all + three available index engines (BST, SortedArray, SCEngine). + """ + t = Table({"x": [1, 2, 3], "y": [4, 5, 6]}) + t.add_index("y", engine=engine) + + with pytest.raises(ValueError): + # "bad" is not convertible to the int64 dtype of column y + t[0] = (99, "bad") + + # Data must be unchanged + assert t[0]["x"] == 1 + assert t[0]["y"] == 4 + + # Index must still find the original key + result = t.loc[4] + assert result["x"] == 1 + assert result["y"] == 4 + + # No ghost entry for the attempted new value + with pytest.raises(KeyError): + t.loc[99] diff --git a/docs/changes/table/19450.bugfix.rst b/docs/changes/table/19450.bugfix.rst new file mode 100644 index 000000000000..5f3e137dea8f --- /dev/null +++ b/docs/changes/table/19450.bugfix.rst @@ -0,0 +1,2 @@ +Fixed table index getting corrupted when a row assignment raises an exception +mid-update. The index is now properly restored to its original state on failure. From e58313dc0d9caf99e7c2cdd2302d4c9ca0ea48d7 Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Thu, 19 Mar 2026 19:14:18 +0100 Subject: [PATCH 114/199] Backport PR #19367: Fix reading FITS files with default memmap settings on WASM --- astropy/io/fits/file.py | 28 ++++++++++++----- astropy/io/fits/tests/test_core.py | 45 ++++++++++++++++++++++++--- docs/changes/io.fits/19367.bugfix.rst | 1 + 3 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 docs/changes/io.fits/19367.bugfix.rst diff --git a/astropy/io/fits/file.py b/astropy/io/fits/file.py index 520252910de1..266cce0d6a53 100644 --- a/astropy/io/fits/file.py +++ b/astropy/io/fits/file.py @@ -422,17 +422,29 @@ def readarray(self, size=None, offset=0, dtype=np.uint8, shape=None): access=MEMMAP_MODES["denywrite"], offset=0, ) + elif not self.strict_memmap: + # mmap failed but wasn't explicitly requested, + # fall back to non-mmap reading + warnings.warn( + "Could not memory map array; falling back to " + "non-memory-mapped file reading. You can disable " + "this warning by setting memmap=False", + AstropyUserWarning, + ) + self.memmap = False else: raise - return np.ndarray( - shape=shape, dtype=dtype, offset=offset, buffer=self._mmap - ) - else: - count = math.prod(shape) - self._file.seek(offset) - data = _array_from_file(self._file, dtype, count) - return data.reshape(shape) + if self._mmap is not None: + return np.ndarray( + shape=shape, dtype=dtype, offset=offset, buffer=self._mmap + ) + + # Non-mmap path (also used as fallback when mmap fails) + count = math.prod(shape) + self._file.seek(offset) + data = _array_from_file(self._file, dtype, count) + return data.reshape(shape) finally: # Make sure we leave the file in the position we found it; on # some platforms (e.g. Windows) mmaping a file handle can also diff --git a/astropy/io/fits/tests/test_core.py b/astropy/io/fits/tests/test_core.py index e8b354860f37..f5d774e323f4 100644 --- a/astropy/io/fits/tests/test_core.py +++ b/astropy/io/fits/tests/test_core.py @@ -1247,15 +1247,52 @@ def mmap_patched(*args, **kwargs): return mmap_original(*args, **kwargs) with fits.open(self.data("test0.fits"), memmap=True) as hdulist: - with patch.object(mmap, "mmap", side_effect=mmap_patched) as p: - with pytest.warns( + with ( + patch.object(mmap, "mmap", side_effect=mmap_patched) as p, + pytest.warns( AstropyUserWarning, match=r"Could not memory map array with mode='readonly'", - ): - data = hdulist[1].data + ), + ): + data = hdulist[1].data p.reset_mock() assert not data.flags.writeable + def test_mmap_unsupported_fallback(self): + """ + Regression test for https://github.com/astropy/astropy/issues/19305 + + Tests that when mmap fails with an unsupported error (e.g., in WASM + environments), astropy falls back to non-mmap reading if memmap was + not explicitly requested, but raises the error if memmap=True was + explicitly set. + """ + + def mmap_patched(*args, **kwargs): + # Simulate mmap not being supported (e.g., WASM environment) + exc = OSError("mmap not supported") + exc.errno = errno.ENODEV + raise exc + + # With default memmap setting (not specified), we should fall back to + # non-mmap reading with a warning. We open file first, then patch only + # during data access to avoid interfering with cleanup. + with fits.open(self.data("test0.fits")) as hdul: + with ( + patch.object(mmap, "mmap", side_effect=mmap_patched), + pytest.warns(AstropyUserWarning, match=r"Could not memory map"), + ): + data = hdul[1].data + assert data is not None + + # With explicit memmap=True, we should raise the error + with fits.open(self.data("test0.fits"), memmap=True) as hdul: + with ( + patch.object(mmap, "mmap", side_effect=mmap_patched), + pytest.raises(OSError, match=r"mmap not supported"), + ): + hdul[1].data + def test_mmap_closing(self): """ Tests that the mmap reference is closed/removed when there aren't any diff --git a/docs/changes/io.fits/19367.bugfix.rst b/docs/changes/io.fits/19367.bugfix.rst new file mode 100644 index 000000000000..ed5faafc252b --- /dev/null +++ b/docs/changes/io.fits/19367.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug that caused reading FITS files to not work properly on WASM when relying on the default memmap settings. From 6c7d9110b8737138b49da9396f28c6ac2f62c447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 19 Mar 2026 19:29:47 +0100 Subject: [PATCH 115/199] Backport PR #19468: DOC: Add docstring explaining jointype integer mapping in _np_utils.pyx --- astropy/table/_np_utils.pyx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/astropy/table/_np_utils.pyx b/astropy/table/_np_utils.pyx index 3290fd761091..703d6b80d3f4 100644 --- a/astropy/table/_np_utils.pyx +++ b/astropy/table/_np_utils.pyx @@ -27,6 +27,33 @@ def join_inner(np.ndarray[DTYPE_t, ndim=1] idxs, """ Do the inner-loop cartesian product for np_utils.join() processing. (The "inner" is about the inner loop, not inner join). + + Parameters + ---------- + idxs : ndarray + Array of boundary indices for matching keys. + idx_sort : ndarray + Sorted indices of the combined left and right keys. + len_left : int + Length of the left table. + jointype : int + Integer encoding the type of join to perform: + + * 0 = inner (strict intersection of keys) + * 1 = outer / cartesian (union of all keys) + * 2 = left (all keys from the left table) + * 3 = right (all keys from the right table) + + Returns + ------- + masked : int + Flag indicating if any values were masked during the join (1 if true, 0 if false). + n_out : int + The final number of rows in the joined output. + left_out, right_out : ndarray + Integer arrays of indices mapping the output rows back to the original left/right tables. + left_mask, right_mask : ndarray + Boolean arrays indicating which output rows are missing corresponding data in the left/right tables. """ cdef DTYPE_t n_out = 0 cdef DTYPE_t max_key_idxs = 0 From ffafcf6b5d649ad6188580bc090f9b51f6a4a9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Tue, 24 Mar 2026 00:59:18 +0100 Subject: [PATCH 116/199] Backport PR #19486: TST: Update test_noncelestial_angular hash for mpldev --- astropy/tests/figures/py311-test-image-mpldev-cov.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astropy/tests/figures/py311-test-image-mpldev-cov.json b/astropy/tests/figures/py311-test-image-mpldev-cov.json index 581b01663817..39a3c6c11949 100644 --- a/astropy/tests/figures/py311-test-image-mpldev-cov.json +++ b/astropy/tests/figures/py311-test-image-mpldev-cov.json @@ -29,7 +29,7 @@ "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_set_coord_type": "ee5e873fd467292f7e76f0890051f33762b399fa3936357797e7338f80e914eb", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_regression": "c06679408a13b816854a17b962cf4ec2afcef2a917c62fc4c0e68894967be754", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axislabels_regression": "d34dac4a594e8c4597f8cebb19921d815e1384c41a53d2455a94fb9664b9fcc2", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_noncelestial_angular": "4e20c0fe1dcc7c89aa0774512e4782be7e5cc42493dbaa8917555fcf7e82da57", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_noncelestial_angular": "71ec12f7362b9157076269008d400a2edbb8814e9442221d73cb6ad32e160e83", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_patches_distortion": "704af668b56837628f11b3dedbba60c0d11fda2e896ddbba9dee0e8acb5b99d7", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_quadrangle": "0223dba7d207f37c644c5c0860f6ef18b11b8013438030c89d88d700ea2ed437", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_args": "59a354f169ba1084b590aacddc6fc57f41223fab43b2f3c71f53872b83673be9", From 59ecec45573b709d1791cd745a017dbc22d1d8c2 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:11:09 -0400 Subject: [PATCH 117/199] Companion for PR 19472 for v7.2.x (Change the way we update astropy-iers-data for active branches) (#19473) --- .github/workflows/update_astropy_iers_data_pin.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/update_astropy_iers_data_pin.yml b/.github/workflows/update_astropy_iers_data_pin.yml index e3c36f7e8a3c..76afed341c60 100644 --- a/.github/workflows/update_astropy_iers_data_pin.yml +++ b/.github/workflows/update_astropy_iers_data_pin.yml @@ -5,8 +5,6 @@ name: Auto-update astropy-iers-data minimum version on: - schedule: - - cron: '0 0 2 * *' # Monthly workflow_dispatch: permissions: @@ -45,14 +43,15 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: + base: ${{ github.ref_name }} branch: update-astropy-iers-data-pin branch-suffix: timestamp delete-branch: true - labels: no-changelog-entry-needed, utils.iers + labels: no-changelog-entry-needed, utils.iers, skip-basebranch-check title: Update minimum required version of astropy-iers-data body: | This is an automated update of the minimum version of astropy-iers-data package. - :pray: Please apply backport labels to any active backport branches for v6.0.x or later. :pray: + One pull request per active branch would be opened separately. Please apply proper milestone to each of them. :warning: Please close and re-open this pull request to trigger the CI. :warning: From f5fb647ac76b777b2bfaf9a2d36ced82ea43f1bf Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 24 Mar 2026 17:36:33 +0000 Subject: [PATCH 118/199] Update minimum required version of astropy-iers-data --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2d58b4c9fed8..2228aa8e86b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ keywords = [ "ascii", ] dependencies = [ - "astropy-iers-data>=0.2026.3.16.0.53.33", + "astropy-iers-data>=0.2026.3.23.0.51.38", "numpy>=1.24, <2.7", "packaging>=22.0.0", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires From 47eacf4b8f78ca098dbef756ff48749105d5f3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Tue, 24 Mar 2026 12:21:39 -0700 Subject: [PATCH 119/199] Backport PR #19485: DOC: Update xmllint URL --- docs/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index 6f7275140283..dc436438769e 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -163,7 +163,7 @@ The further dependencies provide more specific features: :meth:`astropy.table.Table.show_in_notebook` to display the Astropy table in Jupyter notebook for ``backend="ipydatagrid"``. -- `xmllint `_: To validate VOTABLE XML files. +- `xmllint `_: To validate VOTABLE XML files. This is a command line tool installed outside of Python. - `pandas `_: To convert From 1c1ac62cb9c9bd507f9a98c1cd5afc60e25a239c Mon Sep 17 00:00:00 2001 From: Simon Conseil Date: Wed, 18 Mar 2026 12:30:19 +0100 Subject: [PATCH 120/199] Backport PR #19438: Fix verification of CompImageHDU headers --- astropy/io/fits/hdu/base.py | 38 +++++++++++++------ astropy/io/fits/hdu/compressed/compressed.py | 31 +++++---------- .../hdu/compressed/tests/test_compressed.py | 38 +++++++++++++++++++ astropy/io/fits/hdu/image.py | 14 ++++--- docs/changes/io.fits/19438.bugfix.rst | 3 ++ 5 files changed, 86 insertions(+), 38 deletions(-) create mode 100644 docs/changes/io.fits/19438.bugfix.rst diff --git a/astropy/io/fits/hdu/base.py b/astropy/io/fits/hdu/base.py index 5dcc0e726933..c988a5cb7067 100644 --- a/astropy/io/fits/hdu/base.py +++ b/astropy/io/fits/hdu/base.py @@ -885,6 +885,19 @@ def match_header(cls, header): """ return first(header.keys()) not in ("SIMPLE", "XTENSION") + @property + def _expected_header_primary_like(self): + """ + Whether verification should expect a primary-HDU-style header (SIMPLE + keyword, no PCOUNT/GCOUNT) or extension-HDU-style header (XTENSION + keyword, PCOUNT/GCOUNT required). + + By default this is determined by whether the HDU is an ExtensionHDU, + but subclasses can override this for special cases like CompImageHDU + where the header format depends on the file content. + """ + return not isinstance(self, ExtensionHDU) + @property def size(self): """ @@ -957,12 +970,12 @@ def _verify(self, option="warn"): # Verify location and value of mandatory keywords. # Do the first card here, instead of in the respective HDU classes, so # the checking is in order, in case of required cards in wrong order. - if isinstance(self, ExtensionHDU): - firstkey = "XTENSION" - firstval = self._extension - else: + if self._expected_header_primary_like: firstkey = "SIMPLE" firstval = True + else: + firstkey = "XTENSION" + firstval = self._extension self.req_cards(firstkey, 0, None, firstval, option, errs) self.req_cards( @@ -1544,13 +1557,16 @@ def _verify(self, option="warn"): errs = super()._verify(option=option) # Verify location and value of mandatory keywords. - naxis = self._header.get("NAXIS", 0) - self.req_cards( - "PCOUNT", naxis + 3, lambda v: (_is_int(v) and v >= 0), 0, option, errs - ) - self.req_cards( - "GCOUNT", naxis + 4, lambda v: (_is_int(v) and v == 1), 1, option, errs - ) + # PCOUNT and GCOUNT are only required for extension-style headers, + # not for primary-style headers (e.g., CompImageHDU with SIMPLE). + if not self._expected_header_primary_like: + naxis = self._header.get("NAXIS", 0) + self.req_cards( + "PCOUNT", naxis + 3, lambda v: (_is_int(v) and v >= 0), 0, option, errs + ) + self.req_cards( + "GCOUNT", naxis + 4, lambda v: (_is_int(v) and v == 1), 1, option, errs + ) return errs diff --git a/astropy/io/fits/hdu/compressed/compressed.py b/astropy/io/fits/hdu/compressed/compressed.py index b256e15d18e1..cfa2a1a29a02 100644 --- a/astropy/io/fits/hdu/compressed/compressed.py +++ b/astropy/io/fits/hdu/compressed/compressed.py @@ -18,7 +18,6 @@ from astropy.io.fits.hdu.image import ImageHDU from astropy.io.fits.header import Header from astropy.io.fits.util import _is_int -from astropy.io.fits.verify import _ErrList from astropy.utils.decorators import lazyproperty from astropy.utils.exceptions import AstropyUserWarning @@ -399,6 +398,15 @@ def match_header(cls, header): return True + @property + def _expected_header_primary_like(self): + # CompImageHDU can have either a primary-style header (SIMPLE) or an + # extension-style header (XTENSION) depending on whether the original + # image was a primary HDU or an extension HDU. fpack-compressed primary + # HDUs have ZSIMPLE in the bintable header which becomes SIMPLE in the + # image header. + return "SIMPLE" in self._header + @property def compression_type(self): return self._compression_type @@ -673,27 +681,6 @@ def section(self): """ return CompImageSection(self) - def _verify(self, *args, **kwargs): - # The following is the default _verify for ImageHDU - errs = super()._verify(*args, **kwargs) - - # However in some cases the decompressed header is actually like a - # PrimaryHDU header rather than an ImageHDU header, in which case - # there are certain errors we can ignore - if "SIMPLE" in self.header: - errs_filtered = [] - for err in errs: - if len(err) >= 2 and err[1] in ( - "'XTENSION' card does not exist.", - "'PCOUNT' card does not exist.", - "'GCOUNT' card does not exist.", - ): - continue - errs_filtered.append(err) - return _ErrList(errs_filtered) - else: - return errs - def fileinfo(self): if self._bintable is not None: return self._bintable.fileinfo() diff --git a/astropy/io/fits/hdu/compressed/tests/test_compressed.py b/astropy/io/fits/hdu/compressed/tests/test_compressed.py index 1276a8201c04..9c1b205b6ad3 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_compressed.py +++ b/astropy/io/fits/hdu/compressed/tests/test_compressed.py @@ -6,6 +6,7 @@ import pickle import re import time +import warnings from io import BytesIO import numpy as np @@ -1501,6 +1502,43 @@ def test_reserved_keywords_stripped(tmp_path): assert "ZZERO" not in hdud[1].header +def test_compimghdu_with_primary_header_verify_fix(): + """ + Test that CompImageHDU created from a PrimaryHDU header can be verified + with 'fix' option without corrupting the header. + + When a CompImageHDU has SIMPLE in its header (indicating it came from a + primary HDU), verification should not try to insert XTENSION, PCOUNT, + or GCOUNT. + """ + + # Create a CompImageHDU using a PrimaryHDU's header + primary = fits.PrimaryHDU(data=np.arange(100, dtype=np.int32).reshape(10, 10)) + comp_hdu = fits.CompImageHDU( + data=np.arange(100, dtype=np.int32).reshape(10, 10), + header=primary.header, + ) + + # Header should have SIMPLE, not XTENSION + assert "SIMPLE" in comp_hdu.header + assert "XTENSION" not in comp_hdu.header + + # Run verify('fix') - this should not modify the header or emit warnings + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + comp_hdu.verify("fix") + verify_warnings = [ + warning for warning in w if issubclass(warning.category, VerifyWarning) + ] + assert len(verify_warnings) == 0, f"Got warnings: {verify_warnings}" + + # Header should still have SIMPLE, not XTENSION (no corruption) + assert "SIMPLE" in comp_hdu.header + assert "XTENSION" not in comp_hdu.header + assert "PCOUNT" not in comp_hdu.header + assert "GCOUNT" not in comp_hdu.header + + def test_compimghdu_with_primary_header_no_dual_keywords(tmp_path): """ Regression test for https://github.com/astropy/astropy/issues/19361 diff --git a/astropy/io/fits/hdu/image.py b/astropy/io/fits/hdu/image.py index bb40e6a7b87a..6a4d1270fac5 100644 --- a/astropy/io/fits/hdu/image.py +++ b/astropy/io/fits/hdu/image.py @@ -1077,6 +1077,7 @@ class PrimaryHDU(_ImageBaseHDU): """ _default_name = "PRIMARY" + _expected_header_primary_like = True def __init__( self, @@ -1262,13 +1263,16 @@ def _verify(self, option="warn"): ImageHDU verify method. """ errs = super()._verify(option=option) - naxis = self._header.get("NAXIS", 0) # PCOUNT must == 0, GCOUNT must == 1; the former is verified in # ExtensionHDU._verify, however ExtensionHDU._verify allows PCOUNT - # to be >= 0, so we need to check it here - self.req_cards( - "PCOUNT", naxis + 3, lambda v: (_is_int(v) and v == 0), 0, option, errs - ) + # to be >= 0, so we need to check it here. + # Only check for extension-style headers (not primary-style like + # CompImageHDU with SIMPLE). + if not self._expected_header_primary_like: + naxis = self._header.get("NAXIS", 0) + self.req_cards( + "PCOUNT", naxis + 3, lambda v: (_is_int(v) and v == 0), 0, option, errs + ) return errs diff --git a/docs/changes/io.fits/19438.bugfix.rst b/docs/changes/io.fits/19438.bugfix.rst new file mode 100644 index 000000000000..1a35992df565 --- /dev/null +++ b/docs/changes/io.fits/19438.bugfix.rst @@ -0,0 +1,3 @@ +Fix a bug that caused header verification for CompImageHDU to not work correctly and +in some cases produce corrupt files when the decompressed header was a primary HDU +header not an extension header. From 6f23686492623daaf6906aa7f5e4f41145aa2f99 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Fri, 27 Mar 2026 17:15:40 +0100 Subject: [PATCH 121/199] Backport PR #19466: Fix MaskedColumn info _format_funcs deepcopy --- astropy/table/column.py | 17 +++++++++++++++++ astropy/table/tests/test_column.py | 13 +++++++++++++ docs/changes/table/19466.bugfix.rst | 2 ++ 3 files changed, 32 insertions(+) create mode 100644 docs/changes/table/19466.bugfix.rst diff --git a/astropy/table/column.py b/astropy/table/column.py index 01616468eca1..4065392dade3 100644 --- a/astropy/table/column.py +++ b/astropy/table/column.py @@ -1668,6 +1668,23 @@ def __new__( return self + def __deepcopy__(self, memo=None): + out = super().__deepcopy__(memo) + + # MaskedArray.__deepcopy__ copies self.__dict__ entries directly via + # deepcopy(), bypassing the DataInfo descriptor. That leaves a raw + # deep-copied MaskedColumnInfo in out.__dict__["info"] which has + # _attrs (e.g. serialize_method) but lacks bound-only slot state such + # as _format_funcs. Re-assigning through the descriptor (DataInfo.__set__) + # creates a fresh bound MaskedColumnInfo, copies across the _attrs from + # the raw object, and sets _format_funcs = {} as expected. + # + # See https://github.com/astropy/astropy/pull/19466 for more details. + if "info" in out.__dict__: + out.info = self.info + + return out + @property def fill_value(self): return self.get_fill_value() # defer to native ma.MaskedArray method diff --git a/astropy/table/tests/test_column.py b/astropy/table/tests/test_column.py index 33c73bda1649..b3fd73b07145 100644 --- a/astropy/table/tests/test_column.py +++ b/astropy/table/tests/test_column.py @@ -1131,6 +1131,19 @@ def test_masked_column_serialize_method_propagation(): assert mc5.info.serialize_method["ecsv"] == "data_mask" +def test_masked_column_deepcopy_info_format_funcs(): + """Test the fix for #19412""" + mc = table.MaskedColumn([1.0, 2.0, 3.0], mask=[True, False, True]) + # Set a non-default serialize method to make sure that gets copied over. + mc.info.serialize_method["ecsv"] = "data_mask" + + mc_copy = copy.deepcopy(mc) + + assert mc_copy.info.serialize_method["ecsv"] == "data_mask" + # Prior to the fix, the _format_funcs did not exist on the info object. + assert mc_copy.info._format_funcs == {} + + @pytest.mark.parametrize("dtype", ["S", "U", "i"]) def test_searchsorted(Column, dtype): c = Column([1, 2, 2, 3], dtype=dtype) diff --git a/docs/changes/table/19466.bugfix.rst b/docs/changes/table/19466.bugfix.rst new file mode 100644 index 000000000000..10c79a8de077 --- /dev/null +++ b/docs/changes/table/19466.bugfix.rst @@ -0,0 +1,2 @@ +Fixes a problem where deepcopying a MaskedColumn did not correctly create the ``info`` +attribute. This resulted in an inability to print the column after the deepcopy. From 32ace855853494b3477a151d2961bcbf57778be1 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:59:57 -0400 Subject: [PATCH 122/199] Backport PR #19508: DOC: migrate RTD python image from mambaforge (deprecated) to miniforge --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index de4a5af85a08..322fa423119f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: "ubuntu-22.04" tools: - python: "mambaforge-4.10" + python: "miniforge3-25.11" jobs: post_checkout: - git fetch --unshallow || true From 80af48d64730cf09c3fdfd50a9b81b0d0aed25dc Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:02:17 -0400 Subject: [PATCH 123/199] Backport PR #19515: DOC: Remove dead link for barycorr reference --- docs/coordinates/velocities.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/coordinates/velocities.rst b/docs/coordinates/velocities.rst index fe4c65baae15..937b5e8ead12 100644 --- a/docs/coordinates/velocities.rst +++ b/docs/coordinates/velocities.rst @@ -572,7 +572,7 @@ radial velocity and :math:`v_b` is the barycentric correction returned by the barycentric correction in this way leads to errors of order 3 m/s. The barycentric correction in `~astropy.coordinates.SkyCoord.radial_velocity_correction` is consistent -with the `IDL implementation `_ of +with the IDL implementation of the Wright & Eastmann (2014) paper to a level of 10 mm/s for a source at infinite distance. We do not include the Shapiro delay nor the light travel time correction from equation 28 of that paper. The neglected terms From d4ddc2a4cd922f2222226b295fe027d4ab504c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Wed, 1 Apr 2026 15:50:25 +0200 Subject: [PATCH 124/199] Backport PR #19520: BUG: future-proof `np.timedelta64` invocations --- astropy/table/_dataframes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astropy/table/_dataframes.py b/astropy/table/_dataframes.py index 14a9897a68bb..67f0e765bde2 100644 --- a/astropy/table/_dataframes.py +++ b/astropy/table/_dataframes.py @@ -65,7 +65,7 @@ def _encode_mixins(tbl: Table) -> Table: for col in time_cols: if isinstance(col, TimeDelta): new_col = (col.sec * 1e9).astype("timedelta64[ns]") - nat = np.timedelta64("NaT") + nat = np.timedelta64("NaT", "ns") else: new_col = col.datetime64.copy() nat = np.datetime64("NaT") From d5e863223fbc0b55e09ca4d468c7333696d2928f Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:30:33 -0400 Subject: [PATCH 125/199] Backport PR #19505: DOC: future proof a doctest for compat with Python 3.15 --- docs/coordinates/skycoord.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/coordinates/skycoord.rst b/docs/coordinates/skycoord.rst index d2c7bc62819a..bb99f84c2aac 100644 --- a/docs/coordinates/skycoord.rst +++ b/docs/coordinates/skycoord.rst @@ -878,7 +878,7 @@ To change the representation of a coordinate object by setting the >>> c.x Traceback (most recent call last): ... - AttributeError: 'SkyCoord' object has no attribute 'x' + AttributeError: 'SkyCoord' object has no attribute 'x'... >>> c.representation_type = 'spherical' >>> c # doctest: +FLOAT_CMP From 3ca32333e0f4af401636d08960a2d0a9f601f9d3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 2 Apr 2026 00:26:15 +0000 Subject: [PATCH 126/199] Update minimum required version of astropy-iers-data --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2228aa8e86b2..903a38adcc57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ keywords = [ "ascii", ] dependencies = [ - "astropy-iers-data>=0.2026.3.23.0.51.38", + "astropy-iers-data>=0.2026.4.1.15.5.49", "numpy>=1.24, <2.7", "packaging>=22.0.0", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires From 89303205b68073100c4918f8bae088dbc16c08f5 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Thu, 2 Apr 2026 14:46:14 +0200 Subject: [PATCH 127/199] Backport PR #19507: BUG: add support for new internal ufuncs in numpy 2.5 (`np._core.imag` and `np._core.real`) --- astropy/units/quantity_helper/helpers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/astropy/units/quantity_helper/helpers.py b/astropy/units/quantity_helper/helpers.py index 68eec348ab3d..95be38fc724a 100644 --- a/astropy/units/quantity_helper/helpers.py +++ b/astropy/units/quantity_helper/helpers.py @@ -18,6 +18,7 @@ NUMPY_LT_2_1, NUMPY_LT_2_2, NUMPY_LT_2_3, + NUMPY_LT_2_5, ) if NUMPY_LT_2_0: @@ -460,6 +461,9 @@ def helper_clip(f, unit1, unit2, unit3): np.trunc, np.positive, ) +if not NUMPY_LT_2_5: + invariant_ufuncs += (np_umath.imag, np_umath.real) + for ufunc in invariant_ufuncs: UFUNC_HELPERS[ufunc] = helper_invariant From 40c18c53d1dc27894cc4323ecda2d8aad8656a67 Mon Sep 17 00:00:00 2001 From: xbreak Date: Tue, 7 Apr 2026 12:42:06 +0200 Subject: [PATCH 128/199] Backport PR #19530: Correct unix_tai epoch documentation --- docs/time/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/time/index.rst b/docs/time/index.rst index 36ddc8ab0c37..9a3212f4012a 100644 --- a/docs/time/index.rst +++ b/docs/time/index.rst @@ -347,7 +347,7 @@ Format Scale Reference date ============ ====== ======================== ``cxcsec`` TT ``1998-01-01 00:00:00`` ``unix`` UTC ``1970-01-01 00:00:00`` -``unix_tai`` TAI ``1970-01-01 00:00:08`` +``unix_tai`` TAI ``1970-01-01 00:00:00`` ``gps`` TAI ``1980-01-06 00:00:19`` ============ ====== ======================== From 6a28724231eb8cc5f7adc0f77d556f580ef53ae1 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:43:49 -0400 Subject: [PATCH 129/199] Backport PR #19536: DOC: Small clarification about UNIX time on how it elapses --- docs/time/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/time/index.rst b/docs/time/index.rst index 9a3212f4012a..a68af35c78bc 100644 --- a/docs/time/index.rst +++ b/docs/time/index.rst @@ -339,6 +339,7 @@ Time from Epoch Formats The formats ``cxcsec``, ``gps``, ``unix``, and ``unix_tai`` are special in that they provide a floating point representation of the elapsed time in seconds +(with the caveat that ``unix`` does not include leap seconds) since a particular reference date. These formats have a intrinsic time scale which is used to compute the elapsed seconds since the reference date. From 2a494f0afad97350777f55990acd90630151601b Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:49:08 -0400 Subject: [PATCH 130/199] Backport PR #19534: BUG: ensure get_data_and_mask respects an input.masked attribute --- astropy/utils/masked/core.py | 5 ++-- astropy/utils/masked/tests/test_containers.py | 26 ++++++++++++++++++- docs/changes/utils/19534.bugfix.rst | 3 +++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 docs/changes/utils/19534.bugfix.rst diff --git a/astropy/utils/masked/core.py b/astropy/utils/masked/core.py index 8d69d2fde72f..e65dbc106e09 100644 --- a/astropy/utils/masked/core.py +++ b/astropy/utils/masked/core.py @@ -55,7 +55,8 @@ def get_data_and_mask(array): Parameters ---------- array : array-like - Possibly masked item, judged by whether it has a ``mask`` attribute. + Possibly masked item, judged by whether it has a ``mask`` attribute + (but ignored when a possible ``masked`` attribute is `False`). If so, checks for having an ``unmasked`` attribute (as expected for instances of `~astropy.utils.masked.Masked`), or uses the ``_data`` attribute if the inpuit is an instance of `~numpy.ma.MaskedArray`. @@ -75,7 +76,7 @@ def get_data_and_mask(array): """ mask = getattr(array, "mask", None) - if mask is None: + if mask is None or not getattr(array, "masked", True): return array, None try: diff --git a/astropy/utils/masked/tests/test_containers.py b/astropy/utils/masked/tests/test_containers.py index 0ac5ea624fc1..c76c931419c5 100644 --- a/astropy/utils/masked/tests/test_containers.py +++ b/astropy/utils/masked/tests/test_containers.py @@ -8,7 +8,7 @@ from astropy.coordinates import SkyCoord from astropy.coordinates import representation as r from astropy.time import Time -from astropy.utils.masked import Masked +from astropy.utils.masked import Masked, get_data_and_mask class TestRepresentations: @@ -31,6 +31,14 @@ def test_initialization(self): assert_array_equal(self.mc.y, self.my) assert_array_equal(self.mc.z, self.mz) + def test_get_data_and_mask(self): + c, m = get_data_and_mask(self.c) + assert m is None + assert c is self.c + c, m = get_data_and_mask(self.mc) + assert_array_equal(m, self.mask) + assert not c.masked + def test_norm(self): # Need stacking and erfa override. norm = self.mc.norm() @@ -74,6 +82,14 @@ def test_initialization(self): assert_array_equal(self.msc.data.lon, self.mra) assert_array_equal(self.msc.data.lat, self.mdec) + def test_get_data_and_mask(self): + sc, m = get_data_and_mask(self.sc) + assert m is None + assert sc is self.sc + sc, m = get_data_and_mask(self.msc) + assert_array_equal(m, self.mask) + assert not sc.masked + def test_transformation(self): gcrs = self.sc.gcrs mgcrs = self.msc.gcrs @@ -129,6 +145,14 @@ def test_initialization(self): assert_array_equal(self.mt.jd1.unmasked, self.t.jd1) assert_array_equal(self.mt.jd2.unmasked, self.t.jd2) + def test_get_data_and_mask(self): + t, m = get_data_and_mask(self.t) + assert m is None + assert t is self.t + t, m = get_data_and_mask(self.mt) + assert_array_equal(m, self.mask) + assert not t.masked + @pytest.mark.parametrize("format_", ["jd", "cxcsec", "jyear"]) def test_different_formats(self, format_): # Formats do not yet work with everything; e.g., isot is not supported diff --git a/docs/changes/utils/19534.bugfix.rst b/docs/changes/utils/19534.bugfix.rst new file mode 100644 index 000000000000..6b4bf8d5a3b0 --- /dev/null +++ b/docs/changes/utils/19534.bugfix.rst @@ -0,0 +1,3 @@ +Ensure that ``utils.masked.get_data_and_mask`` works properly with containers, +returning ``None`` for the mask if ``masked`` is not set (instead of returning +an all-False array). From a3154e0268d727e271724edb8b4bafe2e8d2de9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 9 Apr 2026 11:19:23 +0200 Subject: [PATCH 131/199] Backport PR #19544: BUG: use non-deprecated numpy APIs to override dtype of a Distribution --- astropy/uncertainty/core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/astropy/uncertainty/core.py b/astropy/uncertainty/core.py index 6d95dd26c128..83180bc6c4cf 100644 --- a/astropy/uncertainty/core.py +++ b/astropy/uncertainty/core.py @@ -101,7 +101,10 @@ def __new__(cls, samples): interface["strides"] = samples.strides[:-1] structured = np.asarray(DummyArray(interface, base=samples)) # Set our new structured dtype. - structured.dtype = new_dtype + if NUMPY_LT_2_5: + structured.dtype = new_dtype + else: + structured._set_dtype(new_dtype) # Get rid of trailing dimension of 1. if NUMPY_LT_2_5: structured.shape = samples.shape[:-1] From 485b7e3860dc693dd658a7039934e3a13b0486a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Sat, 11 Apr 2026 16:48:48 +0200 Subject: [PATCH 132/199] Backport PR #19562: TST: bump mpldev image test references --- .../figures/py311-test-image-mpldev-cov.json | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/astropy/tests/figures/py311-test-image-mpldev-cov.json b/astropy/tests/figures/py311-test-image-mpldev-cov.json index 39a3c6c11949..5c5bfba140b9 100644 --- a/astropy/tests/figures/py311-test-image-mpldev-cov.json +++ b/astropy/tests/figures/py311-test-image-mpldev-cov.json @@ -1,62 +1,62 @@ { - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "79743d5939dd58fc0a84941e258a1fa78212195d6bbbb7cc7b11439a1816e9b0", - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_rectangular": "d9ad505b7455d6f3a1305273547b177e58d1f36418f5983d0faad29ddd5134a2", - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_nonrectangular": "0c1528f0ac4c30b66b0f427851dd82ae6fca2982db5c86dd20166318e2271c5d", - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_change_wcs": "c68e961f0a21cc0dcc43c523cee1c564d94bd96c795f976d51e5198fc52f83cc", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tight_layout": "ab936ab9b81dc3b35acea5257e108ac0071aec2225cefffa096b1873e650f2f8", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_image_plot": "4fe4c89d089e8f1f9584584826f25d3c884d4914f76a863e1a624f9ef2295b07", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "5ba2b23bc4107e33eb9ffcfb656dc5b487969443f847497c0a9ab6ad6c4624bf", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_rectangular": "45669dd577a0750602555da53c590da0b70a64d899e38a6b3be1c8c0c393dc52", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_nonrectangular": "38145a9457711720f12888ba62aeb28ac06f0c05425856af415949e49ecc0b97", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_change_wcs": "6a3b3e21d12fb83e24c3307c24fe300e520db4b158d407f6df7fc79dfa2210cf", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tight_layout": "5bd7b10da254a8cee16b4d2304faf3a9d876f9f512f2778f1326aec2d9c10371", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_image_plot": "a10da5ff6a071bb278b51fe811887e978773cf9f69717eeae9ded395e37bccfd", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axes_off": "3580258468fc0ff0fa6d140299b79f1f05c16c3e222447de38228e01983fbdd1", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[True]": "e2b531b65facf320881a3bea5adf91fd3bd6f4f267c83973c5c602b0cb1b5589", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[False]": "ec8157d34f8691023645d5bf82f98dbd1ed79abcfcfd0dbfdcc245bea0956caf", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[line]": "2fee3974028c571086c6ad2bbb52b8fb9fffe0dcb8d9ee6cb1b9611dea6ac6be", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contour_overlay": "3df6a162520b4ac6ec4727bb58bad168852c5b037b9202af0d85ae4997c8954e", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contourf_overlay": "5a68cd27e0cace22f2501d5a3c784f71b18e5f9f409e04ef8738761b0845c2d7", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_overlay_features_image": "1e0c92a5d43d935e7d497ae79b54681bb58b480782b412c5a521da66e928bca7", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_curvilinear_grid_patches_image": "32b60fc009c2acdfb11d2d522d883d8893943d0458f513684038aca28e64e406", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image": "f740f21fd0af4e502753934add6abcbc68ff3b73428b3d8eec1132d34086595b", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image_lonlat": "96b021d7f89817c3ca3f0f2c0528658c9df095c113e0b4a2330ef5e321f03b9b", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_coord": "142be2288f9a08595e022627e8f2b00596123c7cd7304c6bde9e09b5b3212f44", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scatter_coord": "142be2288f9a08595e022627e8f2b00596123c7cd7304c6bde9e09b5b3212f44", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_text_coord": "4e81a420d4e4e4ed080c61027d0aa5198bf015ea17455a874f3fe78e4b268672", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_line": "c0163e76c738b41274185756370d1502bf968f500a283dc76122d258426fe96a", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_changed_axis_units": "75a85cc15104030366272835729af049bdd54ac49575159cbed30c15d249e065", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_minor_ticks": "e35de8268a042d86016cc684f441c96ab0ba374af78c44c99c4ddfdfb9aa4a25", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_labels": "a494be95719c60530307a288b109e9a8d4da96d295e8b9891f8f3e162a3ce64d", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_no_ticks": "5836f6d36ce6ce89156b251055e5b00b6063a35bff4b7ca1b2da725da14a67ad", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_rcparams": "1437e6206edf7bc906382558097c2da346812d4ba184157d3b1c8f5b9c5e1262", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[True]": "3492f9ddb93014073086269f862aa7c6183da0c28a3a98afd330aa9e0f27cf09", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[False]": "8019d129a842be8d6c91543790f1e7b1933b2b91edfa7885a0d71b0ca8ef00d7", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[line]": "75516135332695601d2fe25381c360b03ebf5526c9376bdeab0e618069b1516a", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contour_overlay": "45eee6ea3948c70ffd6e8758423dea0c90a83bd525420f34687f7981b451e911", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contourf_overlay": "44d57ebdd3216650be39b019729b532a88ceb95f83f1f8dc664377f4f64a631e", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_overlay_features_image": "bf160601fe1e0bf8996f71c2907422cffaa468eb2755f4fd065dd2b5694af848", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_curvilinear_grid_patches_image": "3af87106f1b0fcf6070d33b748557d897be80531ab39192b227c82a409bef835", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image": "bd4013e426adde9461a46b2c88b26020fb198bb14d7b4835e4cdeed8ca185678", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image_lonlat": "eff17af9665669826c8924cb0686f9ef313bdad8a477e12622d8d7d07422efb3", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_coord": "6d5cf6dae41a88239870282530c60f771f6369b600856c81b4991610821c3b3b", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scatter_coord": "6d5cf6dae41a88239870282530c60f771f6369b600856c81b4991610821c3b3b", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_text_coord": "5ddca4e5ee5a86b6cb29588e5826ab271268f9502be440af5852bf0aa2724d2a", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_line": "2bae09e3199778dcbea8f058e200a245367f7b70072b285cd7fb82635fcd7482", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_changed_axis_units": "c8b0ac1c48f9beba0261caea8aae466574c296ea088f4439e54497f04f7940f5", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_minor_ticks": "c314eb4ef0f20621c2fc62ba09d0d71afe67baafbdb289d1b79308ea6164291d", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_labels": "e1cefee39eedc7320e17e64e2c4e711f574c833c9f0155aa431944aa2f9ca899", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_no_ticks": "7ac52e09ffde02327bf01d233fd7947a74a1a36e9ec273f94ffd04a5d3f699d5", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_rcparams": "132410d077bf5d6ff682f285961b4a3b8b6750c344ffd4660f1fb176aa424680", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles": "11ef218080920301ada1ef9cea558599ca110ba49e3dc4e9dc547c013b87fcc7", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles_non_square_axes": "2ffe1157db233bc80e0eda08a30b916cc56a8ff0a7f34c3a0a1b1037695fe1a5", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_set_coord_type": "ee5e873fd467292f7e76f0890051f33762b399fa3936357797e7338f80e914eb", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_regression": "c06679408a13b816854a17b962cf4ec2afcef2a917c62fc4c0e68894967be754", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axislabels_regression": "d34dac4a594e8c4597f8cebb19921d815e1384c41a53d2455a94fb9664b9fcc2", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_noncelestial_angular": "71ec12f7362b9157076269008d400a2edbb8814e9442221d73cb6ad32e160e83", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles_non_square_axes": "19b6034dc4f1e350efe12eb2e5a6add1bf47ffe37d79bd05f1d15cff684af365", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_set_coord_type": "c66c6870631760871da3d722cf4739f2e542f6d10937f5c1e87054ea67121ae4", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_regression": "e1cbb0d489ecc3ca72258e87bbf0006bbc1bd2979df464cb61c68f5f38615987", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axislabels_regression": "72598fe74fb8fc1d0ab83df6ef4886accd08c8b8f47938408fa9327c8a37e2eb", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_noncelestial_angular": "191e3eab7bef77a5ee6ea956a5fb1bc15b95769dfef472a1cad4a8042f6136d4", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_patches_distortion": "704af668b56837628f11b3dedbba60c0d11fda2e896ddbba9dee0e8acb5b99d7", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_quadrangle": "0223dba7d207f37c644c5c0860f6ef18b11b8013438030c89d88d700ea2ed437", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_args": "59a354f169ba1084b590aacddc6fc57f41223fab43b2f3c71f53872b83673be9", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_header": "11383bf77fda831fae02beae555038a6f7cff027119c547bac1644c36fc038f6", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scalebar": "034ae9301a5ad448d803d7b28435c4fb98b5e45731ccd3375798a0b2896ca8a3", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_elliptical_frame": "3aee6e8bcb0b1283993d6743683f13077aab179a85540e3dcb301fef8c66aaf7", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_hms_labels": "1047e944e3cb798a39702be44c9612cb27a26e4fd246a5cf0abe047a51985d5c", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_latex_labels": "2bfbb32d3ac53f781ca901bb3c4630f9ae99ae54ad3cac2b7ffe8863980e7e2e", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_params": "78bdce5072b3e9ac87b7e76832f3fc889c115aef3f894004a50b7eeba0d32ebd", - "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs": "f8976c6c777c720a5de21f76fa5a9c39d24ff70e7f11ce75a2a5e56daba4bd30", - "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs_format_unit": "e0eea94f0ca0e37e96d903c3f441ecf0eb20b6870bfbd8a3426ad3e8305d1cee", - "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs_get_transform": "73e65438281efd42cda9e459467b18f7695aa50589782d0ce0cfbd298b69cb14", - "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_2d_wcs_correlated": "c2fd3acaa4739844288e1ce23dcb0a70b71aaf190d85f4a9164e7ab90836a76b", - "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_sliced_low_level_wcs[slices0-custom:pos.helioprojective.lon]": "60c967eef03cc2f038d7915f1b988dfa60852fa7b950b83c6d3f49054862d5d1", - "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_sliced_low_level_wcs[slices1-custom:pos.helioprojective.lat]": "0a14e5153eb91e2a3a96501f02a21581effc7d6b7ae31e29a402a3abf30d5b12", - "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices0-hpln]": "60c967eef03cc2f038d7915f1b988dfa60852fa7b950b83c6d3f49054862d5d1", - "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices1-hplt]": "0a14e5153eb91e2a3a96501f02a21581effc7d6b7ae31e29a402a3abf30d5b12", - "astropy.visualization.wcsaxes.tests.test_images.test_allsky_labels_wrap": "8908818b526d4a506505da890eff47121b23ee41656dc15dff3e19f7d03094af", - "astropy.visualization.wcsaxes.tests.test_images.test_tickable_gridlines": "b01476e68e63a8e0b80ffd533968081518c5eda059274a104e73658eb0cdf2db", - "astropy.visualization.wcsaxes.tests.test_images.test_overlay_nondegree_unit": "2bbb8832852f6c5ea95839cdeed778dc1b1b41cc6d6d2efa7266dadc3edab146", - "astropy.visualization.wcsaxes.tests.test_images.test_nosimplify": "f88c0868cc16d98ee93baca3681d68ebb5a4c0a0ab82e75e9977cec1e92f4a73", - "astropy.visualization.wcsaxes.tests.test_images.test_custom_formatter": "00aa0ada490271baa37ce7f23d169a5871d7afba6647257fe212fe931c425d05", - "astropy.visualization.wcsaxes.tests.test_images.test_equatorial_arcsec": "d30d80194f04319d38f65239d57cc300dc42c0678b433f71ffb74445c000dbbf", - "astropy.visualization.wcsaxes.tests.test_images.test_wcs_preserve_units": "60a979ca74d39b22e641dbc547c6f7d52777702412aac6824f4f6ec176aba960", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay": "1f025403a8df3208ace222556e78fd67a74a5bbcb7d8bcd70ad0913c52a52a27", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay_auto_coord_meta": "2f737bb70fb1a5452cb0efa79010376614dc559e9aff607f638f044ac6b04448", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_direct_init": "1f24c5243bfdf0f30e88afc4f2f5d66db85954cea50a2910af94975ff8765b45", - "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_5d_with_names": "135f6e8530ab0de32fe1a5907ba9a4f9aad43335e5a4e029962c2611e4334723", - "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_2d_celestial_arcsec": "b9d44e1b54ff14cc214aac132372bdebdd6893a8c5faceed2e09620674bc7e5e" + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_args": "01dffa3f79ddbfa5c119a49fc1282fee63f2792ccebc189ad44ab644b59f1559", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_header": "c4b34fab02c5be0d9d69b2615a1b05c3b73ddf9e40031c6873abe26ab3090472", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scalebar": "4b5c3fc70a8bbd7aaeed0d15d78bc491c8253ca2cc7d239c343306e0a71f44c5", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_elliptical_frame": "3cf0c18fa653fafaaba2287b4a8c3e125eb29c2ba6a120918d0e088cbc8467d2", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_hms_labels": "507fb9485ce61050323d021347ce43acd1929f44249cfc98bd684ce685b4315b", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_latex_labels": "66a902d8e9ed0f6e95a5433d4842ede2163ff11387804bf93ed810632e1cd373", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_params": "b197947dedc38821e02f31edb4e259a2d87439470947aa5c53bdfc89faa9d9b3", + "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs": "bf85a4cc85935869d4c26a39d155d90f68a9d266adcf2bac0d10b5da384d7045", + "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs_format_unit": "f632e2eddb9d683a085558339c23995049a7463644d6d6f997d923f5c6aaaa42", + "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs_get_transform": "0bcd3100d9d347cb14ae767107ea39885473964318d8fb14066c6217ee38a23b", + "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_2d_wcs_correlated": "47ffb96cf60b000028f26ae80e1ce9c35d8d43bf75d32f00b6e00e74656fcea3", + "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_sliced_low_level_wcs[slices0-custom:pos.helioprojective.lon]": "24af7845ef230f2be4949377f8d2a8e7b3bccd2622d8f488442ed09fc07b2b5d", + "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_sliced_low_level_wcs[slices1-custom:pos.helioprojective.lat]": "03708033709fe9b63375f96fbf5ea44b4e17cae968d385f5899d4b2e42be1f6c", + "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices0-hpln]": "24af7845ef230f2be4949377f8d2a8e7b3bccd2622d8f488442ed09fc07b2b5d", + "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices1-hplt]": "03708033709fe9b63375f96fbf5ea44b4e17cae968d385f5899d4b2e42be1f6c", + "astropy.visualization.wcsaxes.tests.test_images.test_allsky_labels_wrap": "4697fefe84a8b0e5b58cc35bc250921ec3fffc7cbdee85836427af90fe4ac475", + "astropy.visualization.wcsaxes.tests.test_images.test_tickable_gridlines": "5ce7b377d7cf15d3be97a922929536413a36fc2c1a34423ecee8f04d361d35fd", + "astropy.visualization.wcsaxes.tests.test_images.test_overlay_nondegree_unit": "4f5819ad0dc6cc6c5a4776265f114f81b192bfd11ce1ddbb3d8b43da6b56e642", + "astropy.visualization.wcsaxes.tests.test_images.test_nosimplify": "336423447a5066d39a60ac4b0839dbe8eb2fab6a742830a71464f941c6307c66", + "astropy.visualization.wcsaxes.tests.test_images.test_custom_formatter": "1142e72415e9affaa97c7e763d1274632fe1d9cf8a9eae2a579a179fd14d6b9d", + "astropy.visualization.wcsaxes.tests.test_images.test_equatorial_arcsec": "f048285e01f99235e711068e06ea5729b1ff5258604a944a5fa26042e613dae6", + "astropy.visualization.wcsaxes.tests.test_images.test_wcs_preserve_units": "4111f2731c268bbe8ff57551b011afd6dd918efe07236507625f6e99e69b7263", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay": "f7635158075969632bc3cabd6cfa9798a3d4227f173aed32a546c540c65d9b4d", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay_auto_coord_meta": "9a3cc14c87a8349487888c69bab345748894be4295598eaed2475804f97b5f8a", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_direct_init": "021364576662b07f1eedf58251d657244fd1b67bb16ec4c99a0506b1dde0d66e", + "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_5d_with_names": "b243aec60020235d04c816743c0fcc643495f58d4d4d5086588c59628d9cea76", + "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_2d_celestial_arcsec": "ecb11a595e1352b3b8e6967494fa5eac63992fc878bf15a934329bb28aeb61d6" } From e59c15bc633d1aecdc68d3f4bf8a87c8b01b1dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 13 Apr 2026 19:21:15 +0200 Subject: [PATCH 133/199] Backport PR #19564: Update credits to add new contributors --- .mailmap | 76 +++++++++++++++++++++++++++++------------------- docs/credits.rst | 36 +++++++++++++++++++---- 2 files changed, 76 insertions(+), 36 deletions(-) diff --git a/.mailmap b/.mailmap index d59c207a98ce..7ca014583be9 100644 --- a/.mailmap +++ b/.mailmap @@ -6,7 +6,7 @@ Adam Ginsburg Adele Plunkett Adrian Price-Whelan Adrian Price-Whelan -Akshat Dixit Akshat1Nar +Akshat Dixit Albert Y. Shih Aleh Khvalko Aleksi Suutarinen @@ -42,6 +42,7 @@ Axel Donath Benjamin Alan Weaver Benjamin Alan Weaver +Ben Green <120208759+ScatteredComet@users.noreply.github.com> Benjamin Roulston Benjamin Winkel Bhavya Khandelwal @@ -50,7 +51,7 @@ Brett Graham Brett Morris Brett Morris Brett Morris -Brett Morris Brett M. Morris +Brett Morris Brian Soto Brian Svoboda Brigitta Sipőcz @@ -69,6 +70,7 @@ Christian Clauss Christoph Gohlke Christopher Bonnett Clara Brasseur +Clara Brasseur Clément Robert Clément Robert Craig Jones @@ -86,8 +88,7 @@ Daniel Datsev Daniel Datsev Daniel Giles Daniel Lenz -Daniel Ryan Dan Ryan -Daniel Ryan DanRyanIrish +Daniel Ryan Daria Cara Daria Cara <36781821+daria-cara@users.noreply.github.com> David Collom @@ -98,11 +99,12 @@ David Grant <33813984+DavoGrant@ David Kirkby David Pérez-Suárez David Shupe -Deen-Dot Deen-dot -Deen-Dot Deen-dot <80238871+Deen-dot@users.noreply.github.com> +Deen-Dot +Deen-Dot <80238871+Deen-dot@users.noreply.github.com> Demitri Muna Demitri Muna Demitri Muna +Dhruv Yadav Derek Homeier Derek Homeier Derek Homeier <709020+dhomeier@users.noreply.github.com> @@ -110,27 +112,27 @@ Diego Asterio de Zaballa Douglas Burke Dylan Gregersen Edward Gomez -Edward Slavich Ed Slavich +Edward Slavich Eduardo Olinto <90293761+olintoeduardo@users.noreply.github.com> Eero Vaher Elijah Bernstein-Cooper Emily Deibert Emma Hogan Eric Depagne -Eric Koch +Eric Koch E. Madison Bray E. Madison Bray E. Madison Bray E. Rykoff Emir Karamehmetoglu -Emir Karamehmetoglu Emir +Emir Karamehmetoglu Evan Jones <60061381+E-W-Jones@users.noreply.github.com> Everett Schlawin Everett Schlawin Esteban Pardo Sánchez Francesco Montesano -Gabriel Brammer Gabriel Brammer -Gabriel Brammer Gabe Brammer +Gabriel Brammer +Gabriel Brammer Gabriel Perren Gabriel Perren Geert Barentsen @@ -142,6 +144,7 @@ Graham Kanarek Guillaume Pernot Guillaume Pernot Gustavo Bragança +Henrike F. Hannes Breytenbach Hans Moritz Günther Hans Moritz Günther @@ -156,6 +159,7 @@ Humna Awan Igor Lemos Ivo Busko Ivo Busko +J. Berg Jackson Hayward Jaime Andrés Jake VanderPlas @@ -192,6 +196,7 @@ John Parejko Johnny Greco Johnny Greco Jon Carifio +Joren Hammudoglu Jonathan Foster Jonathan Foster Jonathan Gagne @@ -211,7 +216,8 @@ Kacper Kowalik Kacper Kowalik Kang Wang Karan Grover -Karl Gordon Karl D. Gordon +Kartavay Verma +Karl Gordon Karl Vyhmeister Kelle Cruz Kevin Gullikson @@ -221,6 +227,7 @@ Kris Stern Kunam Balaram Reddy Kyle Barbary Kyle Oman +Kyle Oman Larry Bradley Larry Bradley Laura Watkins @@ -237,9 +244,12 @@ Lisa Walter Loïc Séguin-C Luke G. Bouma Luke Kelley -Luz Paz luz paz -Maximilian Linhoff Maximilian Nöthe +Luz Paz +Maximilian Linhoff Maximilian Linhoff +Maximilian Linhoff +M. Atakan Gürkan +M. S. R. Dinesh <39215691+msrdinesh@users.noreply.github.com> Madhura Parikh Magali Mebsout Magnus Persson @@ -247,9 +257,8 @@ Maneesh Yadav Mangala Gowri Krishnamoorthy Manon Marchand Marcello Nascif <118627858+marcellonascif@users.noreply.github.com> +Marten van Kerkwijk Marten van Kerkwijk -Marten van Kerkwijk Marten H. van Kerkwijk -Marten van Kerkwijk Marten Henric van Kerkwijk Marten van Kerkwijk Marten van Kerkwijk Marten van Kerkwijk @@ -278,7 +287,7 @@ Mike Alexandersen Mikhail Minin Moataz Hisham Mridul Seth -Mubin Manasia Mubin17 <48038715+Mubin17@users.noreply.github.com> +Mubin Manasia <48038715+Mubin17@users.noreply.github.com> Nabil Freij Nadia Dencheva Nadia Dencheva @@ -297,7 +306,7 @@ Ole Streicher Ole Streicher Parikshit Sakurikar Patricio Rojo -Pauline Barmby +Pauline Barmby Perry Greenfield P. L. Lim <2090236+pllim@users.noreply.github.com> P. L. Lim <2090236+pllim@users.noreply.github.com> @@ -308,13 +317,15 @@ Pritish Chakraborty Rachel Guo Ricardo Fonseca Ricardo Fonseca -Richard R -Richard R <58728519+rrjbca@users.noreply.github.com> +Richard R. +Richard R. <58728519+rrjbca@users.noreply.github.com> +R. Virinchi +Reem Hamraz Ricky O'Steen <39831871+rosteen@users.noreply.github.com> Ritiek Malhotra Ritwick DSouza Robel Geda -Robel Geda robelgeda +Robel Geda Rohan Rajpal Rohit Kapoor Rohit Patil @@ -330,7 +341,7 @@ Sara Ogaz Sarah Graves Sashank Mishra Sebastian Meßlinger <39328484+krachyon@users.noreply.github.com> -Sebastian Meßlinger Sebastian +Sebastian Meßlinger Sergio Pascual Shane Maloney Shane Maloney @@ -340,24 +351,25 @@ Shilpi Jain Shivansh Mishra Shivansh Mishra Simon Alinder <92031780+AlinderS@users.noreply.github.com> -Simon Alinder AlinderS +Simon Alinder Simon Conseil Simon Conseil Simon Conseil Simon Conseil Simon Conseil Simon Liedtke -Somia Floret Somia FLORET -Somia Floret Somia Floret <57394764+somilia@users.noreply.github.com> +Somia Floret +Somia Floret <57394764+somilia@users.noreply.github.com> Sourabh Cheedella Stelios Voutsinas Steve Crawford Steve Crawford Steve Crawford -Stuart Littlefair +Stuart Littlefair Stuart Mumford Sudheesh Singanamalla Sudheesh Singanamalla +Surya K. Sushobhana Patra Tanvi Pooranmal Meena <96572616+mtanvi19@users.noreply.github.com> <96572616+TanviPooranmal@users.noreply.github.com> Thomas Erben @@ -366,16 +378,21 @@ Thompson Le Blanc Tiago Gomes Tim Jenness Tim Jenness -Timothy P. Ellsworth Bowers Timothy Ellsworth Bowers +Timothy P. Ellsworth Bowers Tom Aldcroft Tom Donaldson Tom J Wilson Tyler Finethy VSN Reddy Janga -Vishnunarayan K I +Vishnunarayan K. I. +Varun Kasyap Pentamaraju +Vishwas Vital Fernández William Jamieson +William Jamieson Yannick Copin +Yaocheng Chen +Yaocheng Chen <51320658+yaochengchen@users.noreply.github.com> Yash Kumar Yash Nandwana Yash Sharma @@ -383,5 +400,4 @@ Yingqi Ying <33911276+dyq0811@users.noreply.github.com> Zach Edwards Zac Hatfield-Dodds Zé Vinicius -Zhiyuan Ma Jerry Ma -Zhiyuan Ma Zhiyuan Ma +Zhiyuan Ma diff --git a/docs/credits.rst b/docs/credits.rst index 37cc5b7526b4..ae5fd60688d2 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -15,6 +15,7 @@ Core Package Contributors * Adam Ginsburg * Adam Turner * Adele Plunkett +* Aditya Lohuni * Aditya Sharma * Adrian Price-Whelan * Adrien Thob @@ -69,6 +70,7 @@ Core Package Contributors * Axel Donath * Azalee Bostroem * Bastian Beischer +* Ben Green * Ben Greiner * Benjamin Alan Weaver * Benjamin Roulston @@ -80,6 +82,8 @@ Core Package Contributors * Bhavya Khandelwal * Bili Dong * Bill Cleveland +* Bill Wolf +* Bodhi Silberling * Bogdan Nicula * Bojan Nikolic * Brandie-M @@ -91,6 +95,7 @@ Core Package Contributors * britgit * Bruce Merry * Bruno Oliveira +* Bruno Sanchez * Bryce Kalmbach * Bryce Nordgren * Caden Gobat @@ -98,6 +103,7 @@ Core Package Contributors * Carl Osterwisch * Carl Schaffer * Caspar van Leeuwen +* Charalampos Stratakis * Chiara Marmo * Chris Beaumont * Chris Hanley @@ -115,6 +121,7 @@ Core Package Contributors * Conor MacBride * Cristian Ardelean * Curtis McCully +* Cyrus * Damien LaRocque * Dan Foreman-Mackey * Dan P\. Cunningham @@ -140,6 +147,7 @@ Core Package Contributors * Demitri Muna * Derek Homeier * Devin Crichton +* Dhruv Yadav * Diego Alonso * Diego Asterio de Zaballa * disha @@ -171,10 +179,12 @@ Core Package Contributors * Erik Tollerud * Erin Allard * Esteban Pardo Sánchez +* Evan Chen * Evan Jones * Even Rouault * Everett Schlawin * Evert Rol +* Fazeel Usmani * Felipe Cybis Pereira * Felipe Gameleira * Felix Yan @@ -211,7 +221,7 @@ Core Package Contributors * Heinz-Alexander Fuetterer * Helen Sherwood-Taylor * Hélvio Peixoto -* Henrike F +* Henrike F\. * Henry Schreiner * Himanshu Pathak * homeboy445 @@ -223,6 +233,7 @@ Core Package Contributors * Igor Lemos * ikkamens * Inada Naoki +* J\. Berg * J\. Goutin * J\. Xavier Prochaska * Jackson Hayward @@ -247,6 +258,7 @@ Core Package Contributors * Jeff Taylor * Jeffrey McBeth * Jero Bado +* Jett Higgins * jimboH * Jo Bovy * Joanna Power @@ -264,6 +276,7 @@ Core Package Contributors * Jonathan Foster * Jonathan Sick * Jonathan Whitmore +* Joren Hammudoglu * Jörg Dietrich * Jose Sabater * José Sabater Montes @@ -283,6 +296,7 @@ Core Package Contributors * Karl Gordon * Karl Vyhmeister * Karl Wessel +* Kartavay Verma * Katrin Leinweber * Kelle Cruz * Kevin Gullikson @@ -323,8 +337,8 @@ Core Package Contributors * Luke G\. Bouma * Luke Kelley * Luz Paz -* M Atakan Gürkan -* M S R Dinesh +* M\. Atakan Gürkan +* M\. S\. R\. Dinesh * Mabry Cervin * Madhura Parikh * Magali Mebsout @@ -399,6 +413,7 @@ Core Package Contributors * mzhengxi * Nabil Freij * Nadia Dencheva +* Naksh Yadav * Nathanial Hendler * Nathaniel Starkman * Naveen Selvadurai @@ -447,6 +462,7 @@ Core Package Contributors * Preshanth Jagannathan * Pritish Chakraborty * Pushkar Kopparla +* R\. Virinchi * Rachel Guo * Raghuram Devarakonda * Ralf Gommers @@ -455,12 +471,14 @@ Core Package Contributors * Rasmus Handberg * Ravi Kumar * Ray Plante +* Reem Hamraz * Régis Terrier * Ricardo Fonseca * Ricardo Ogando -* Richard R +* Richard R\. * Ricky O'Steen * Rik van Lieshout +* RinZ27 * Ritiek Malhotra * Ritwick DSouza * Roban Hultman Kramer @@ -536,6 +554,7 @@ Core Package Contributors * Stuart Littlefair * Stuart Mumford * Sudheesh Singanamalla +* Surya K\. * Sushobhana Patra * Suyog Garg * Swapnil Sharma @@ -544,6 +563,7 @@ Core Package Contributors * Tanuj Rastogi * Tanvi Pooranmal Meena * Thais Borges +* Thomas Dutkiewicz * Thomas Erben * Thomas J\. Fan * Thomas Robitaille @@ -564,21 +584,25 @@ Core Package Contributors * Tom Kooij * Tomas Babej * Tyler Finethy +* Varun Kasyap Pentamaraju * Varun Nikam * Vatsala Swaroop * Víctor Terrón * Víctor Zabalza * Victoria Dye * Vinayak Mehta -* Vishnunarayan K I +* Vishnunarayan K\. I\. +* Vishwas * Vital Fernández * Volodymyr Savchenko * VSN Reddy Janga * Wilfred Tyler Gee * William Jamieson * Wolfgang Kerzendorf +* xbreak * xuewc * Yannick Copin +* Yaocheng Chen * Yash Kumar * Yash Nandwana * Yash Sharma @@ -592,10 +616,10 @@ Core Package Contributors * Zhen-Kai Gao * Zhiyuan Ma * Zlatan Vasović +* Περικλής Παντελαίος Other Credits ============= - * Kyle Barbary for designing the Astropy logos and documentation themes. * Andrew Pontzen and the `pynbody `_ team (For code that grew into :mod:`astropy.units`) From a9e463dc0ff34632729d840f7dc318a4d86a71cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 9 Apr 2026 08:23:31 +0200 Subject: [PATCH 134/199] Backport PR #19540 to v7.2.x (BUG: use non-deprecated numpy APIs to override dtype of a base column) --- astropy/table/column.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/astropy/table/column.py b/astropy/table/column.py index 4065392dade3..c18a4482755e 100644 --- a/astropy/table/column.py +++ b/astropy/table/column.py @@ -9,7 +9,7 @@ from numpy import ma from astropy.units import Quantity, StructuredUnit, Unit -from astropy.utils.compat import COPY_IF_NEEDED, NUMPY_LT_2_0 +from astropy.utils.compat import COPY_IF_NEEDED, NUMPY_LT_2_0, NUMPY_LT_2_5 from astropy.utils.console import color_print from astropy.utils.data_info import BaseColumnInfo, dtype_info_name from astropy.utils.metadata import MetaData @@ -1269,7 +1269,10 @@ def __setattr__(self, item, value): raise AttributeError( "cannot set mask value to a column in non-masked Table" ) - super().__setattr__(item, value) + if not NUMPY_LT_2_5 and item == "dtype": + self._set_dtype(value) + else: + super().__setattr__(item, value) if item == "unit" and issubclass(self.dtype.type, np.number): try: From 2283532bcf3778b28de8bd70bf87915d0ee8998a Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 21 Apr 2026 13:41:56 +0200 Subject: [PATCH 135/199] Backport PR #19580: DOC: Fix RST syntactic defects --- astropy/io/votable/tree.py | 2 +- astropy/io/votable/util.py | 2 +- .../periodograms/lombscargle/implementations/chi2_impl.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/astropy/io/votable/tree.py b/astropy/io/votable/tree.py index 31af4b73538a..148208622a79 100644 --- a/astropy/io/votable/tree.py +++ b/astropy/io/votable/tree.py @@ -2309,7 +2309,7 @@ def ref(self): def get_ref(self): """ - Lookup the :class:`Param` instance that this :class:``PARAMref`` + Lookup the :class:`Param` instance that this ``PARAMref`` references. """ for param in self._table._votable.iter_fields_and_params(): diff --git a/astropy/io/votable/util.py b/astropy/io/votable/util.py index 970a9cbd0384..4ec8c0f72c49 100644 --- a/astropy/io/votable/util.py +++ b/astropy/io/votable/util.py @@ -33,7 +33,7 @@ def convert_to_writable_filelike(fd, compressed=False): - a file path string, in which case it is opened, and the file object is returned. - - an object with a :meth:``write`` method, in which case that + - an object with a ``write`` method, in which case that object is returned. compressed : bool, optional diff --git a/astropy/timeseries/periodograms/lombscargle/implementations/chi2_impl.py b/astropy/timeseries/periodograms/lombscargle/implementations/chi2_impl.py index 67c279f33f39..e19b136b4d20 100644 --- a/astropy/timeseries/periodograms/lombscargle/implementations/chi2_impl.py +++ b/astropy/timeseries/periodograms/lombscargle/implementations/chi2_impl.py @@ -22,7 +22,7 @@ def lombscargle_chi2( ---------- t, y, dy : array-like times, values, and errors of the data points. These should be - broadcastable to the same shape. None should be `~astropy.units.Quantity``. + broadcastable to the same shape. None should be `~astropy.units.Quantity`. frequency : array-like frequencies (not angular frequencies) at which to calculate periodogram normalization : str, optional From c49e325656562c1b2988aa0ef53e427bd70d8117 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Thu, 23 Apr 2026 09:53:07 +0200 Subject: [PATCH 136/199] Backport PR #19578: TST: avoid a deprecation warning from numpy 2.5 (datetime64 without and explicit unit) --- astropy/time/tests/test_mask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astropy/time/tests/test_mask.py b/astropy/time/tests/test_mask.py index b70820942c9a..eb27078a3ef4 100644 --- a/astropy/time/tests/test_mask.py +++ b/astropy/time/tests/test_mask.py @@ -367,7 +367,7 @@ def test_all_formats(format_, masked_cls, masked_array_type): def test_datetime64_with_nat(): - dt64 = np.array(["nat", "2001-02-03", "2001-02-04"], dtype="datetime64") + dt64 = np.array(["nat", "2001-02-03", "2001-02-04"], dtype="datetime64[ns]") mdt64 = Masked(dt64, mask=[False, True, False]) t = Time(mdt64) assert t.masked From 1c89ef1077d6eb3eb7b797310fa698d8b7a986e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 23 Apr 2026 11:59:02 +0200 Subject: [PATCH 137/199] Backport PR #19584: Dealing with deprecation of setting dtype in record arrays, and unitless times --- astropy/io/fits/hdu/table.py | 22 ++++++++++++++++++---- astropy/table/_dataframes.py | 2 +- astropy/time/tests/test_basic.py | 13 ++++++++++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/astropy/io/fits/hdu/table.py b/astropy/io/fits/hdu/table.py index 14b5f2981461..9416de2c7491 100644 --- a/astropy/io/fits/hdu/table.py +++ b/astropy/io/fits/hdu/table.py @@ -37,7 +37,7 @@ from astropy.io.fits.header import Header, _pad_length from astropy.io.fits.util import _is_int, _str_to_num, path_like from astropy.utils import lazyproperty -from astropy.utils.compat import chararray +from astropy.utils.compat import NUMPY_LT_2_5, chararray from .base import DELAYED, ExtensionHDU, _ValidHDU @@ -208,7 +208,12 @@ def _get_tbdata(self): def _init_tbdata(self, data): columns = self.columns - data.dtype = data.dtype.newbyteorder(">") + if NUMPY_LT_2_5: + data.dtype = data.dtype.newbyteorder(">") + else: + # TODO: see if a a view is possible still; initial trial gives + # error in test_table.py::TestVLATables::test_empty_vla_raw_data. + np.ndarray._set_dtype(data, data.dtype.newbyteorder(">")) # hack to enable pseudo-uint support data._uint = self._uint @@ -1517,11 +1522,20 @@ def _binary_table_byte_swap(data): for arr in reversed(to_swap): arr.byteswap(True) - data.dtype = np.dtype({"names": names, "formats": formats, "offsets": offsets}) + new_dtype = np.dtype({"names": names, "formats": formats, "offsets": offsets}) + if NUMPY_LT_2_5: + data.dtype = new_dtype + else: + # TODO: ensure self.data is never used with this context manager, so that + # a view here would work (and the second _set_dtype can be removed). + np.ndarray._set_dtype(data, new_dtype) yield data for arr in to_swap: arr.byteswap(True) - data.dtype = orig_dtype + if NUMPY_LT_2_5: + data.dtype = orig_dtype + else: + np.ndarray._set_dtype(data, orig_dtype) diff --git a/astropy/table/_dataframes.py b/astropy/table/_dataframes.py index 67f0e765bde2..470f132f59f0 100644 --- a/astropy/table/_dataframes.py +++ b/astropy/table/_dataframes.py @@ -68,7 +68,7 @@ def _encode_mixins(tbl: Table) -> Table: nat = np.timedelta64("NaT", "ns") else: new_col = col.datetime64.copy() - nat = np.datetime64("NaT") + nat = np.datetime64("NaT", "ns") if col.masked: new_col[col.mask] = nat tbl[col.info.name] = new_col diff --git a/astropy/time/tests/test_basic.py b/astropy/time/tests/test_basic.py index cc209485b05b..6a23b35191f1 100644 --- a/astropy/time/tests/test_basic.py +++ b/astropy/time/tests/test_basic.py @@ -5,6 +5,7 @@ import functools import gc import os +import warnings from copy import deepcopy from decimal import Decimal, localcontext from io import StringIO @@ -30,6 +31,7 @@ conf, ) from astropy.utils import iers +from astropy.utils.compat import NUMPY_LT_2_5 from astropy.utils.compat.optional_deps import HAS_H5PY, HAS_PYTZ from astropy.utils.exceptions import AstropyDeprecationWarning @@ -1082,7 +1084,16 @@ def test_plot_date(self): # val = matplotlib.dates.date2num('2000-01-01') val = 730120.0 else: - val = date2num(datetime.datetime(2000, 1, 1)) + # Ignore warnings from numpy for older matplotlib + # https://github.com/astropy/astropy/issues/19586 + if NUMPY_LT_2_5: + val = date2num(datetime.datetime(2000, 1, 1)) + else: + with warnings.catch_warnings( + action="ignore", category=DeprecationWarning + ): + val = date2num(datetime.datetime(2000, 1, 1)) + t = Time("2000-01-01 00:00:00", scale="utc") assert np.allclose(t.plot_date, val, atol=1e-5, rtol=0) From 367e66b1a8a897d5d748b0e23cdb84497908b3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Fri, 24 Apr 2026 11:24:30 +0200 Subject: [PATCH 138/199] Backport PR #19559: BUG: ensure thread safety in `set_temp_(cache|config)` context managers --- astropy/config/paths.py | 63 +++++++--- astropy/config/tests/test_concurrency.py | 149 +++++++++++++++++++++++ astropy/config/tests/test_configs.py | 26 ---- astropy/conftest.py | 29 +++++ docs/changes/config/19559.bugfix.rst | 2 + 5 files changed, 229 insertions(+), 40 deletions(-) create mode 100644 astropy/config/tests/test_concurrency.py create mode 100644 docs/changes/config/19559.bugfix.rst diff --git a/astropy/config/paths.py b/astropy/config/paths.py index 201a1376187c..1e313fb3c54f 100644 --- a/astropy/config/paths.py +++ b/astropy/config/paths.py @@ -10,6 +10,7 @@ from functools import partial, wraps from inspect import cleandoc from pathlib import Path +from threading import RLock from types import TracebackType from typing import Literal, ParamSpec from warnings import warn @@ -25,9 +26,14 @@ "set_temp_config", ] - P = ParamSpec("P") +# Any number of threads may call any combination of set_temp_cache and set_temp_config, +# in any order. Hence they need to share a single lock (otherwise deadlocks would be possible). +# In particular, the possibility of nesting more than one instance of each context imposes +# the use of a re-entrant lock (RLock). +_PATHS_LOCK = RLock() + def get_config_dir_path(rootname: str = "astropy") -> Path: """ @@ -131,14 +137,16 @@ def __init__( self._path = path self._delete = delete - self._prev_path = self.__class__._temp_path def __enter__(self) -> str: - self.__class__._temp_path = self._path + _PATHS_LOCK.acquire() + self._prev_path = self.__class__._temp_path try: + self.__class__._temp_path = self._path return str(self.__class__._get_dir_path(rootname="astropy")) except Exception: self.__class__._temp_path = self._prev_path + _PATHS_LOCK.release() raise def __exit__( @@ -147,10 +155,13 @@ def __exit__( value: BaseException | None, tb: TracebackType | None, ) -> None: - self.__class__._temp_path = self._prev_path + try: + self.__class__._temp_path = self._prev_path - if self._delete and self._path is not None: - shutil.rmtree(self._path) + if self._delete and self._path is not None: + shutil.rmtree(self._path) + finally: + _PATHS_LOCK.release() def __call__(self, func: Callable[P, object]) -> Callable[P, None]: """Implements use as a decorator.""" @@ -269,6 +280,9 @@ class set_temp_config(_SetTempPath): This may also be used as a decorator on a function to set the config path just within that function. + Thread safety is guaranteed since astropy 7.2.1, but concurrency isn't: + only a single thread at a time may execute code within this context. + Parameters ---------- path : str, optional @@ -294,9 +308,24 @@ def __enter__(self) -> str: # config file (e.g., iers.conf.auto_download=False for our tests). from .configuration import _cfgobjs - self._cfgobjs_copy = _cfgobjs.copy() - _cfgobjs.clear() - return super().__enter__() + _PATHS_LOCK.acquire() + + try: + _cfgobjs_copy = _cfgobjs.copy() + except Exception: + _PATHS_LOCK.release() + raise + + self._cfgobjs_copy = _cfgobjs_copy + + try: + _cfgobjs.clear() + return super().__enter__() + except Exception: + _cfgobjs.update(self._cfgobjs_copy) + del self._cfgobjs_copy + _PATHS_LOCK.release() + raise def __exit__( self, @@ -304,12 +333,15 @@ def __exit__( value: BaseException | None, tb: TracebackType | None, ) -> None: - from .configuration import _cfgobjs + try: + from .configuration import _cfgobjs - _cfgobjs.clear() - _cfgobjs.update(self._cfgobjs_copy) - del self._cfgobjs_copy - super().__exit__(type, value, tb) + _cfgobjs.clear() + _cfgobjs.update(self._cfgobjs_copy) + del self._cfgobjs_copy + super().__exit__(type, value, tb) + finally: + _PATHS_LOCK.release() class set_temp_cache(_SetTempPath): @@ -325,6 +357,9 @@ class set_temp_cache(_SetTempPath): This may also be used as a decorator on a function to set the cache path just within that function. + Thread safety is guaranteed since astropy 7.2.1, but concurrency isn't: + only a single thread at a time may execute code within this context. + Parameters ---------- path : str diff --git a/astropy/config/tests/test_concurrency.py b/astropy/config/tests/test_concurrency.py new file mode 100644 index 000000000000..5fd7daa569b9 --- /dev/null +++ b/astropy/config/tests/test_concurrency.py @@ -0,0 +1,149 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from functools import partial +from pathlib import Path +from threading import Barrier +from typing import Generic, TypeAlias, TypeVar +from uuid import uuid4 + +import pytest + +from astropy.config import ( + get_cache_dir_path, + get_config_dir_path, + set_temp_cache, + set_temp_config, +) +from astropy.config.paths import _SetTempPath + +N_THREADS = 10 + +T = TypeVar("T") + + +@dataclass(slots=True, frozen=True, kw_only=True) +class Result(Generic[T]): + actual: T + expected: T + + def match(self) -> bool: + return self.actual == self.expected + + +PathGetter: TypeAlias = Callable[[], Path] + + +def getter_from_manager(cm: type[_SetTempPath]) -> PathGetter: + # this is functionally equivalent to an immutable dict + # it could be refactored into a frozendict when Python 3.14 is unsupported + if cm is set_temp_cache: + return get_cache_dir_path + elif cm is set_temp_config: + return get_config_dir_path + else: + raise ValueError + + +def closure_base( + ctx_manager: type[_SetTempPath], + *, + directory: Path, + barrier: Barrier, +) -> Result[Path]: + local_path = directory / str(uuid4()) + local_path.mkdir() + getter = getter_from_manager(ctx_manager) + + barrier.wait() + with ctx_manager(local_path): + res = getter() + + return Result(actual=res, expected=local_path / "astropy") + + +def assert_valid_results(results: list[Result[T]]) -> None: + __tracebackhide__ = True + assert len(results) == N_THREADS + assert len(set(results)) == N_THREADS + assert all(r.match() for r in results) + + +@pytest.mark.parametrize("ctx_manager", [set_temp_cache, set_temp_config]) +@pytest.mark.usefixtures("ignore_config_paths_global_state") +def test_set_temp_dir(ctx_manager, tmp_path): + closure = partial( + closure_base, + ctx_manager, + directory=tmp_path, + barrier=Barrier(N_THREADS), + ) + with ThreadPoolExecutor(max_workers=N_THREADS) as executor: + futures = [executor.submit(closure) for _ in range(N_THREADS)] + + assert_valid_results([f.result() for f in futures]) + + +@pytest.mark.parametrize( + "cm_out, cm_in", + [ + pytest.param(set_temp_cache, set_temp_cache, id="cache-cache"), + pytest.param(set_temp_cache, set_temp_config, id="cache-config"), + pytest.param(set_temp_config, set_temp_cache, id="config-cache"), + pytest.param(set_temp_config, set_temp_config, id="config-config"), + ], +) +@pytest.mark.usefixtures("ignore_config_paths_global_state") +def test_nesting(cm_out: type[_SetTempPath], cm_in: type[_SetTempPath], tmp_path: Path): + # check that nesting doesn't deadlock + barrier = Barrier(N_THREADS) + getter_out = getter_from_manager(cm_out) + getter_in = getter_from_manager(cm_in) + + def closure() -> Result[tuple[Path, Path, Path, Path]]: + local_path_1 = tmp_path / str(uuid4()) + local_path_1.mkdir() + local_path_2 = tmp_path / str(uuid4()) + local_path_2.mkdir() + + barrier.wait() + with cm_out(local_path_1): + res0 = getter_out() + with cm_in(local_path_2): + res1 = getter_in() + res2 = getter_out() + res3 = getter_out() + return Result( + actual=(res0, res1, res2, res3), + expected=( + local_path_1 / "astropy", + local_path_2 / "astropy", + (local_path_1 if cm_in != cm_out else local_path_2) / "astropy", + local_path_1 / "astropy", + ), + ) + + with ThreadPoolExecutor(max_workers=N_THREADS) as executor: + futures = [executor.submit(closure) for _ in range(N_THREADS)] + + assert_valid_results([f.result() for f in futures]) + + +@pytest.mark.usefixtures("ignore_config_paths_global_state") +def test_mixed_settings(tmp_path): + barrier = Barrier(N_THREADS) + + cache_setter = partial( + closure_base, set_temp_cache, directory=tmp_path, barrier=barrier + ) + config_setter = partial( + closure_base, set_temp_config, directory=tmp_path, barrier=barrier + ) + + closures = [cache_setter if n % 2 else config_setter for n in range(N_THREADS)] + with ThreadPoolExecutor(max_workers=N_THREADS) as executor: + futures = [executor.submit(c) for c in closures] + + assert_valid_results([f.result() for f in futures]) diff --git a/astropy/config/tests/test_configs.py b/astropy/config/tests/test_configs.py index 2177478d9cd0..972c48ce7080 100644 --- a/astropy/config/tests/test_configs.py +++ b/astropy/config/tests/test_configs.py @@ -8,7 +8,6 @@ from contextlib import nullcontext from inspect import cleandoc from pathlib import Path -from threading import Lock import pytest @@ -18,31 +17,6 @@ OLD_CONFIG = {} -_IGNORE_CONFIG_PATHS_GLOBAL_STATE_LOCK = Lock() - - -@pytest.fixture -def ignore_config_paths_global_state(monkeypatch, tmp_path_factory): - # ignore global state of the test session - # and preserve thread safety across all users of this fixture - with _IGNORE_CONFIG_PATHS_GLOBAL_STATE_LOCK: - monkeypatch.delenv("XDG_CACHE_HOME", raising=False) - monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) - - monkeypatch.setattr(paths.set_temp_cache, "_temp_path", None) - monkeypatch.setattr(paths.set_temp_config, "_temp_path", None) - - # also mock $HOME as it's part of the global state taken into account - # for path detection - mock_home_dir = tmp_path_factory.mktemp("MOCK_HOME") - - def mock_home(): - return mock_home_dir - - monkeypatch.setattr(Path, "home", mock_home) - - yield - def setup_module(): OLD_CONFIG.clear() diff --git a/astropy/conftest.py b/astropy/conftest.py index 29cf1316a82c..a5e0b9e51918 100644 --- a/astropy/conftest.py +++ b/astropy/conftest.py @@ -9,6 +9,7 @@ import tempfile import warnings from pathlib import Path +from threading import Lock try: from pytest_astropy_header.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS @@ -49,6 +50,34 @@ def fast_thread_switching(): sys.setswitchinterval(old) +_IGNORE_CONFIG_PATHS_GLOBAL_STATE_LOCK = Lock() + + +@pytest.fixture +def ignore_config_paths_global_state(monkeypatch, tmp_path_factory): + from astropy.config import set_temp_cache, set_temp_config + + # ignore global state of the test session + # and preserve thread safety across all users of this fixture + with _IGNORE_CONFIG_PATHS_GLOBAL_STATE_LOCK: + monkeypatch.delenv("XDG_CACHE_HOME", raising=False) + monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) + + monkeypatch.setattr(set_temp_cache, "_temp_path", None) + monkeypatch.setattr(set_temp_config, "_temp_path", None) + + # also mock $HOME as it's part of the global state taken into account + # for path detection + mock_home_dir = tmp_path_factory.mktemp("MOCK_HOME") + + def mock_home(): + return mock_home_dir + + monkeypatch.setattr(Path, "home", mock_home) + + yield + + def pytest_configure(config): # Ensure number of columns and lines is deterministic for testing from astropy import conf diff --git a/docs/changes/config/19559.bugfix.rst b/docs/changes/config/19559.bugfix.rst new file mode 100644 index 000000000000..7da143c2926d --- /dev/null +++ b/docs/changes/config/19559.bugfix.rst @@ -0,0 +1,2 @@ +Disabling thread concurrency within ``set_temp_cache`` and ``set_temp_config`` +context managers, ensuring thread safety. From c0294f5b5cff42b49da616f45f159aa180c1fe8b Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 24 Apr 2026 23:29:57 +0100 Subject: [PATCH 139/199] Backport PR #19594: BUG: implement flush() and isatty() on _DummyFile --- astropy/utils/misc.py | 6 ++++++ astropy/utils/tests/test_misc.py | 9 +++++++++ docs/changes/utils/19594.bugfix.rst | 4 ++++ 3 files changed, 19 insertions(+) create mode 100644 docs/changes/utils/19594.bugfix.rst diff --git a/astropy/utils/misc.py b/astropy/utils/misc.py index d1689a3b761a..52b473b78e86 100644 --- a/astropy/utils/misc.py +++ b/astropy/utils/misc.py @@ -80,6 +80,12 @@ class _DummyFile: def write(self, s: str) -> None: pass + def flush(self) -> None: + pass + + def isatty(self) -> bool: + return False + @contextlib.contextmanager def silence() -> Generator[None, None, None]: diff --git a/astropy/utils/tests/test_misc.py b/astropy/utils/tests/test_misc.py index c3b60a2f3720..3ea33d2793de 100644 --- a/astropy/utils/tests/test_misc.py +++ b/astropy/utils/tests/test_misc.py @@ -178,3 +178,12 @@ def test_indent_deprecation(): def test_format_exception_deprecation(): with pytest.warns(AstropyDeprecationWarning): misc.format_exception("this is deprecated") + + +def test_silence_stdout_file_protocol(): + import sys + + with misc.silence(): + assert sys.stdout.isatty() is False + sys.stdout.flush() + sys.stdout.write("ignored") diff --git a/docs/changes/utils/19594.bugfix.rst b/docs/changes/utils/19594.bugfix.rst new file mode 100644 index 000000000000..f3c85ec51eb5 --- /dev/null +++ b/docs/changes/utils/19594.bugfix.rst @@ -0,0 +1,4 @@ +The dummy file object used by ``astropy.utils.misc.silence`` to replace +``sys.stdout``/``sys.stderr`` now implements ``flush()`` and ``isatty()``, +so importing libraries that probe the stream (e.g. IPython 9.13 at import +time) no longer raises ``AttributeError`` under ``silence``. From 07988fe595c0f0e861178a5c8508139d690ed8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Sun, 26 Apr 2026 20:51:03 +0200 Subject: [PATCH 140/199] Backport PR #19082 to v7.2.x (TST/DEP: trully test our lowest requirement on matplotlib as declared) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 903a38adcc57..c6fdbd8efd6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,7 +142,7 @@ docs = [ "sphinx_design>=0.6.1", "Jinja2>=3.1.3", "sphinxcontrib-globalsubs >= 0.1.1", - "matplotlib>=3.9.1", # https://github.com/matplotlib/matplotlib/issues/28234 + "matplotlib!=3.9.0", # https://github.com/matplotlib/matplotlib/issues/28234 ] # These group together all the dependencies needed for developing in Astropy. dev = [ From 0ea0433e403fe4328bb99d4a5403c111fa67643c Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 27 Apr 2026 17:11:40 +0100 Subject: [PATCH 141/199] Backport PR #19591: Fix a couple of pickle-related issues in WCS --- astropy/wcs/tests/test_pickle.py | 32 +++++++++++++++++++++++++++++++ astropy/wcs/wcs.py | 7 ++++++- docs/changes/wcs/19591.bugfix.rst | 9 +++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 docs/changes/wcs/19591.bugfix.rst diff --git a/astropy/wcs/tests/test_pickle.py b/astropy/wcs/tests/test_pickle.py index 5d17ac43c1fa..07c26c710fc9 100644 --- a/astropy/wcs/tests/test_pickle.py +++ b/astropy/wcs/tests/test_pickle.py @@ -170,3 +170,35 @@ def test_alt_wcskey(): w2 = pickle.loads(pickle.dumps(w)) assert w2.wcs.alt == "A" + + +@pytest.mark.parametrize("preserve_units", [False, True]) +def test_preserve_units(preserve_units): + # Use non-SI units so that the world coordinates returned by + # ``wcs_pix2world`` differ between ``preserve_units=True`` (arcsec) and + # ``preserve_units=False`` (converted to deg). This ensures the pickle + # round-trip is exercising the actual transformation behavior, not just + # the stored flag. + header = fits.Header.fromstring( + "WCSAXES = 2\n" + "CTYPE1 = 'RA---TAN'\n" + "CTYPE2 = 'DEC--TAN'\n" + "CUNIT1 = 'arcsec'\n" + "CUNIT2 = 'arcsec'\n" + "CRVAL1 = 4\n" + "CRVAL2 = 6\n" + "CRPIX1 = 1\n" + "CRPIX2 = 1\n" + "CDELT1 = 4\n" + "CDELT2 = 2\n", + sep="\n", + ) + w = wcs.WCS(header, preserve_units=preserve_units) + assert w._preserve_units is preserve_units + + coords = np.array([[1.0, 2.0], [3.0, 4.0]]) + expected = w.wcs_pix2world(coords, 0) + + w2 = pickle.loads(pickle.dumps(w)) + assert w2._preserve_units is preserve_units + assert_array_almost_equal(w2.wcs_pix2world(coords, 0), expected) diff --git a/astropy/wcs/wcs.py b/astropy/wcs/wcs.py index 24dad12579f4..c9c0201451cd 100644 --- a/astropy/wcs/wcs.py +++ b/astropy/wcs/wcs.py @@ -529,6 +529,7 @@ def __init__( self._init_kwargs = { "keysel": copy.copy(keysel), "colsel": copy.copy(colsel), + "preserve_units": preserve_units, } if header is None: @@ -3352,7 +3353,11 @@ def __reduce__(self): buffer = io.BytesIO() hdulist.writeto(buffer) - dct = self.__dict__.copy() + # Exclude lazily-populated caches: they are pure functions of the + # WCS state, so unpickling can regenerate them on demand. Keeping + # them out of the pickle avoids bloating the payload and prevents + # non-picklable cache contents (e.g. closures) from breaking pickle. + dct = {k: v for k, v in self.__dict__.items() if not k.endswith("_cache")} dct["_alt_wcskey"] = self.wcs.alt return ( diff --git a/docs/changes/wcs/19591.bugfix.rst b/docs/changes/wcs/19591.bugfix.rst new file mode 100644 index 000000000000..9ebd28094d1f --- /dev/null +++ b/docs/changes/wcs/19591.bugfix.rst @@ -0,0 +1,9 @@ +Lazily-populated caches on a ``WCS`` (such as the internal +``world_axis_object_components``/``world_axis_object_classes`` cache) are no +longer included in the pickled state. They are regenerated on demand after +unpickling, which keeps pickling robust even when a cache entry holds a +non-picklable value. + +Fixed a bug where the ``preserve_units`` option passed to the ``WCS`` +constructor was silently reset to ``False`` when a ``WCS`` object was pickled +and unpickled. From e9bc94f643b75e9b0f58871afc8170f0318fb9ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:52:03 +0000 Subject: [PATCH 142/199] Backport PR #19602: Update credits to add new contributors --- docs/credits.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/credits.rst b/docs/credits.rst index ae5fd60688d2..cd27633dad29 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -337,6 +337,7 @@ Core Package Contributors * Luke G\. Bouma * Luke Kelley * Luz Paz +* M Bussonnier * M\. Atakan Gürkan * M\. S\. R\. Dinesh * Mabry Cervin @@ -369,7 +370,6 @@ Core Package Contributors * Matthew Petroff * Matthew Pitkin * Matthew Turk -* Matthias Bussonnier * Matthias Stein * Matthieu Bec * Mavani Bhautik @@ -453,6 +453,7 @@ Core Package Contributors * Peter Scicluna * Peter Teuben * Peter Yoachim +* Pierre Sassoulas * Pieter Eendebak * Piyush Sharma * Porter Averett From aeb8d432822f96a34b3d21a4fc0ccee8b4ddfcad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Wed, 29 Apr 2026 07:09:29 +0200 Subject: [PATCH 143/199] Backport PR 19610 to v7.2.x (TST: Add back ignoring socket warnings of yore) --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c6fdbd8efd6c..03c329cbb721 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -254,6 +254,12 @@ filterwarnings = [ # https://github.com/astropy/astropy/issues/18126 "ignore:'_UnionGenericAlias' is deprecated and slated for removal in Python 3.17:DeprecationWarning", # not PYTHON_LT_3_14 "ignore:The chararray class is deprecated and will be removed in a future release.:DeprecationWarning", # not NUMPY_LT_2_5 + # Weird warning from the vectorized do_format in Angle.to_string. low numpy/python? + "ignore:invalid value encountered in do_format:RuntimeWarning", + # https://github.com/astropy/astropy/issues/19511 + "ignore:Implicitly cleaning up Date: Thu, 30 Apr 2026 12:29:15 -0400 Subject: [PATCH 144/199] Backport PR #19614: DOC: fix incorrect return type as documented in `get_(cache|config)_dir` docstrings --- astropy/config/paths.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/astropy/config/paths.py b/astropy/config/paths.py index 1e313fb3c54f..03b41ab309b3 100644 --- a/astropy/config/paths.py +++ b/astropy/config/paths.py @@ -54,7 +54,7 @@ def get_config_dir_path(rootname: str = "astropy") -> Path: Returns ------- - configdir : Path + pathlib.Path The absolute path to the configuration directory. """ @@ -68,7 +68,7 @@ def get_config_dir(rootname: str = "astropy") -> str: if get_config_dir_path.__doc__ is not None: # guard against PYTHONOPTIMIZE mode get_config_dir.__doc__ = cleandoc( - get_config_dir_path.__doc__ + get_config_dir_path.__doc__.replace("pathlib.Path", "str") + """ See Also -------- @@ -97,7 +97,7 @@ def get_cache_dir_path(rootname: str = "astropy") -> Path: Returns ------- - cachedir : Path + pathlib.Path The absolute path to the cache directory. """ @@ -111,7 +111,7 @@ def get_cache_dir(rootname: str = "astropy") -> str: if get_cache_dir_path.__doc__ is not None: # guard against PYTHONOPTIMIZE mode get_cache_dir.__doc__ = cleandoc( - get_cache_dir_path.__doc__ + get_cache_dir_path.__doc__.replace("pathlib.Path", "str") + """ See Also -------- From 6b6d4df04444e4e7811a132d08699c24bf8cda40 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Fri, 1 May 2026 14:46:18 -0400 Subject: [PATCH 145/199] Backport PR #19637: DOC: Ignore another link blocked by provider --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index c7d80d16f149..002bb1cdf9fc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -407,6 +407,7 @@ def _custom_edit_url( "https://www.usno.navy.mil/USNO/time/gps/usno-gps-time-transfer", "https://aa.usno.navy.mil/publications/docs/Circular_179.php", "http://data.astropy.org", + "https://astropy-dei.orgmycology.com/", # 403 Client Error: Forbidden "https://doi.org/", # CI blocked by service provider "https://zenodo.org/", # 403 Client Error: Forbidden "https://ui.adsabs.harvard.edu", # CI blocked by service provider From b38077bce903a7ec87edff34c650c6d7b30ca477 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 May 2026 00:37:27 +0000 Subject: [PATCH 146/199] Update minimum required version of astropy-iers-data --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 03c329cbb721..ae44c8eb77f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ keywords = [ "ascii", ] dependencies = [ - "astropy-iers-data>=0.2026.4.1.15.5.49", + "astropy-iers-data>=0.2026.4.27.1.3.2", "numpy>=1.24, <2.7", "packaging>=22.0.0", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires From 9a26542acef461388b5b6bf0b61cb58de805a4ea Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 2 May 2026 22:54:03 +0100 Subject: [PATCH 147/199] Backport PR #19506: Fix hardcoded degree units in fitswcs APE 14 world to pixel --- astropy/wcs/wcsapi/fitswcs.py | 27 ++-- astropy/wcs/wcsapi/tests/helpers.py | 3 + astropy/wcs/wcsapi/tests/test_fitswcs.py | 79 +++++----- .../wcsapi/wrappers/tests/test_sliced_wcs.py | 141 +++++++----------- docs/changes/wcs/19506.bugfix.rst | 1 + 5 files changed, 117 insertions(+), 134 deletions(-) create mode 100644 astropy/wcs/wcsapi/tests/helpers.py create mode 100644 docs/changes/wcs/19506.bugfix.rst diff --git a/astropy/wcs/wcsapi/fitswcs.py b/astropy/wcs/wcsapi/fitswcs.py index 12342ef7fb70..de13ce446a2a 100644 --- a/astropy/wcs/wcsapi/fitswcs.py +++ b/astropy/wcs/wcsapi/fitswcs.py @@ -7,8 +7,8 @@ import numpy as np +import astropy.constants from astropy import units as u -from astropy.constants import c from astropy.coordinates import ICRS, Galactic, SpectralCoord from astropy.coordinates.spectral_coordinate import ( attach_zero_velocities, @@ -23,7 +23,7 @@ __all__ = ["FITSWCSAPIMixin", "SlicedFITSWCS", "custom_ctype_to_ucd_mapping"] -C_SI = c.si.value +C_SI = astropy.constants.c.si.value VELOCITY_FRAMES = { "GEOCENT": "gcrs", @@ -99,7 +99,7 @@ l=263.85 * u.deg, b=48.25 * u.deg, distance=0 * u.km, - radial_velocity=-(3.346e-3 / 2.725 * c).to(u.km / u.s), + radial_velocity=-(3.346e-3 / 2.725 * astropy.constants.c).to(u.km / u.s), ) @@ -438,16 +438,25 @@ def _get_components_and_classes(self): else: kwargs = {} kwargs["frame"] = celestial_frame - # Very occasionally (i.e. with TAB) wcs does not convert the units to degrees + # Very occasionally (i.e. with TAB) wcs does not convert the units + lon_unit = u.Unit(self.wcs.cunit[self.wcs.lng]) + lat_unit = u.Unit(self.wcs.cunit[self.wcs.lat]) kwargs["unit"] = ( - u.Unit(self.wcs.cunit[self.wcs.lng]), - u.Unit(self.wcs.cunit[self.wcs.lat]), + lon_unit, + lat_unit, ) classes["celestial"] = (SkyCoord, (), kwargs) - - components[self.wcs.lng] = ("celestial", 0, "spherical.lon.degree") - components[self.wcs.lat] = ("celestial", 1, "spherical.lat.degree") + components[self.wcs.lng] = ( + "celestial", + 0, + lambda c: c.spherical.lon.to_value(lon_unit), + ) + components[self.wcs.lat] = ( + "celestial", + 1, + lambda c: c.spherical.lat.to_value(lat_unit), + ) # Next, we check for spectral components diff --git a/astropy/wcs/wcsapi/tests/helpers.py b/astropy/wcs/wcsapi/tests/helpers.py new file mode 100644 index 000000000000..6a8acdb143b8 --- /dev/null +++ b/astropy/wcs/wcsapi/tests/helpers.py @@ -0,0 +1,3 @@ +def assert_celestial_component(component, index): + assert component[:2] == ("celestial", index) + assert callable(component[2]) diff --git a/astropy/wcs/wcsapi/tests/test_fitswcs.py b/astropy/wcs/wcsapi/tests/test_fitswcs.py index c6dc6898adac..ff9b6b1b9e14 100644 --- a/astropy/wcs/wcsapi/tests/test_fitswcs.py +++ b/astropy/wcs/wcsapi/tests/test_fitswcs.py @@ -32,6 +32,7 @@ from astropy.utils.exceptions import AstropyDeprecationWarning, AstropyUserWarning from astropy.wcs.wcs import WCS, WCSLIB_VERSION, FITSFixedWarning, NoConvergence, Sip from astropy.wcs.wcsapi.fitswcs import VELOCITY_FRAMES, custom_ctype_to_ucd_mapping +from astropy.wcs.wcsapi.tests.helpers import assert_celestial_component ############################################################################### # The following example is the simplest WCS with default values @@ -137,10 +138,10 @@ def test_simple_celestial(): assert_equal(wcs.axis_correlation_matrix, True) - assert wcs.world_axis_object_components == [ - ("celestial", 0, "spherical.lon.degree"), - ("celestial", 1, "spherical.lat.degree"), - ] + components = wcs.world_axis_object_components + assert len(components) == 2 + assert_celestial_component(components[0], 0) + assert_celestial_component(components[1], 1) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -259,18 +260,11 @@ def test_spectral_cube(): [[True, False, True], [False, True, False], [True, False, True]], ) - assert len(wcs.world_axis_object_components) == 3 - assert wcs.world_axis_object_components[0] == ( - "celestial", - 1, - "spherical.lat.degree", - ) - assert wcs.world_axis_object_components[1][:2] == ("spectral", 0) - assert wcs.world_axis_object_components[2] == ( - "celestial", - 0, - "spherical.lon.degree", - ) + components = wcs.world_axis_object_components + assert len(components) == 3 + assert_celestial_component(components[0], 1) + assert components[1][:2] == ("spectral", 0) + assert_celestial_component(components[2], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -379,18 +373,11 @@ def test_spectral_cube_nonaligned(): # again here because in the past this failed when non-aligned axes were # present, so this serves as a regression test. - assert len(wcs.world_axis_object_components) == 3 - assert wcs.world_axis_object_components[0] == ( - "celestial", - 1, - "spherical.lat.degree", - ) - assert wcs.world_axis_object_components[1][:2] == ("spectral", 0) - assert wcs.world_axis_object_components[2] == ( - "celestial", - 0, - "spherical.lon.degree", - ) + components = wcs.world_axis_object_components + assert len(components) == 3 + assert_celestial_component(components[0], 1) + assert components[1][:2] == ("spectral", 0) + assert_celestial_component(components[2], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -483,8 +470,8 @@ def test_time_cube(): ) components = wcs.world_axis_object_components - assert components[0] == ("celestial", 1, "spherical.lat.degree") - assert components[1] == ("celestial", 0, "spherical.lon.degree") + assert_celestial_component(components[0], 1) + assert_celestial_component(components[1], 0) assert components[2][:2] == ("time", 0) assert callable(components[2][2]) @@ -904,10 +891,10 @@ def test_caching_components_and_classes(): wcs = WCS_SIMPLE_CELESTIAL.deepcopy() - assert wcs.world_axis_object_components == [ - ("celestial", 0, "spherical.lon.degree"), - ("celestial", 1, "spherical.lat.degree"), - ] + components = wcs.world_axis_object_components + assert len(components) == 2 + assert_celestial_component(components[0], 0) + assert_celestial_component(components[1], 1) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -927,6 +914,30 @@ def test_caching_components_and_classes(): assert frame.equinox.jyear == 2010.0 +@pytest.mark.parametrize("unit", ["arcsec", "mas"]) +def test_world_to_pixel_preserves_non_degree_celestial_units(unit): + header = Header() + header["NAXIS"] = 2 + header["NAXIS1"] = 100 + header["NAXIS2"] = 100 + header["CTYPE1"] = "RA---TAN" + header["CTYPE2"] = "DEC--TAN" + header["CUNIT1"] = unit + header["CUNIT2"] = unit + header["CRPIX1"] = 1 + header["CRPIX2"] = 1 + header["CRVAL1"] = 0 + header["CRVAL2"] = 0 + header["CDELT1"] = 1 + header["CDELT2"] = 1 + + wcs = WCS(header, preserve_units=True) + sky = wcs.pixel_to_world(0, 56) + + assert wcs.world_axis_units == [unit, unit] + assert_allclose(wcs.world_to_pixel(sky), (0, 56), atol=1e-14) + + def test_sub_wcsapi_attributes(): # Regression test for a bug that caused some of the WCS attributes to be # incorrect when using WCS.sub or WCS.celestial (which is an alias for sub diff --git a/astropy/wcs/wcsapi/wrappers/tests/test_sliced_wcs.py b/astropy/wcs/wcsapi/wrappers/tests/test_sliced_wcs.py index bdad84df0426..5af013d46bf8 100644 --- a/astropy/wcs/wcsapi/wrappers/tests/test_sliced_wcs.py +++ b/astropy/wcs/wcsapi/wrappers/tests/test_sliced_wcs.py @@ -11,6 +11,7 @@ from astropy.time import Time from astropy.units import Quantity from astropy.wcs.wcs import WCS, FITSFixedWarning +from astropy.wcs.wcsapi.tests.helpers import assert_celestial_component from astropy.wcs.wcsapi.wrappers.sliced_wcs import ( SlicedLowLevelWCS, combine_slices, @@ -130,18 +131,11 @@ def test_ellipsis(): [[True, False, True], [False, True, False], [True, False, True]], ) - assert len(wcs.world_axis_object_components) == 3 - assert wcs.world_axis_object_components[0] == ( - "celestial", - 1, - "spherical.lat.degree", - ) - assert wcs.world_axis_object_components[1][:2] == ("spectral", 0) - assert wcs.world_axis_object_components[2] == ( - "celestial", - 0, - "spherical.lon.degree", - ) + components = wcs.world_axis_object_components + assert len(components) == 3 + assert_celestial_component(components[0], 1) + assert components[1][:2] == ("spectral", 0) + assert_celestial_component(components[2], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -219,10 +213,10 @@ def test_spectral_slice(): assert_equal(wcs.axis_correlation_matrix, [[True, True], [True, True]]) - assert wcs.world_axis_object_components == [ - ("celestial", 1, "spherical.lat.degree"), - ("celestial", 0, "spherical.lon.degree"), - ] + components = wcs.world_axis_object_components + assert len(components) == 2 + assert_celestial_component(components[0], 1) + assert_celestial_component(components[1], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -289,18 +283,11 @@ def test_spectral_range(): [[True, False, True], [False, True, False], [True, False, True]], ) - assert len(wcs.world_axis_object_components) == 3 - assert wcs.world_axis_object_components[0] == ( - "celestial", - 1, - "spherical.lat.degree", - ) - assert wcs.world_axis_object_components[1][:2] == ("spectral", 0) - assert wcs.world_axis_object_components[2] == ( - "celestial", - 0, - "spherical.lon.degree", - ) + components = wcs.world_axis_object_components + assert len(components) == 3 + assert_celestial_component(components[0], 1) + assert components[1][:2] == ("spectral", 0) + assert_celestial_component(components[2], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -369,18 +356,11 @@ def test_celestial_slice(): wcs.axis_correlation_matrix, [[False, True], [True, False], [False, True]] ) - assert len(wcs.world_axis_object_components) == 3 - assert wcs.world_axis_object_components[0] == ( - "celestial", - 1, - "spherical.lat.degree", - ) - assert wcs.world_axis_object_components[1][:2] == ("spectral", 0) - assert wcs.world_axis_object_components[2] == ( - "celestial", - 0, - "spherical.lon.degree", - ) + components = wcs.world_axis_object_components + assert len(components) == 3 + assert_celestial_component(components[0], 1) + assert components[1][:2] == ("spectral", 0) + assert_celestial_component(components[2], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -451,18 +431,11 @@ def test_celestial_range(): [[True, False, True], [False, True, False], [True, False, True]], ) - assert len(wcs.world_axis_object_components) == 3 - assert wcs.world_axis_object_components[0] == ( - "celestial", - 1, - "spherical.lat.degree", - ) - assert wcs.world_axis_object_components[1][:2] == ("spectral", 0) - assert wcs.world_axis_object_components[2] == ( - "celestial", - 0, - "spherical.lon.degree", - ) + components = wcs.world_axis_object_components + assert len(components) == 3 + assert_celestial_component(components[0], 1) + assert components[1][:2] == ("spectral", 0) + assert_celestial_component(components[2], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -542,18 +515,11 @@ def test_celestial_range_rot(): [[True, False, True], [False, True, False], [True, False, True]], ) - assert len(wcs.world_axis_object_components) == 3 - assert wcs.world_axis_object_components[0] == ( - "celestial", - 1, - "spherical.lat.degree", - ) - assert wcs.world_axis_object_components[1][:2] == ("spectral", 0) - assert wcs.world_axis_object_components[2] == ( - "celestial", - 0, - "spherical.lon.degree", - ) + components = wcs.world_axis_object_components + assert len(components) == 3 + assert_celestial_component(components[0], 1) + assert components[1][:2] == ("spectral", 0) + assert_celestial_component(components[2], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -645,18 +611,11 @@ def test_no_array_shape(): [[True, False, True], [False, True, False], [True, False, True]], ) - assert len(wcs.world_axis_object_components) == 3 - assert wcs.world_axis_object_components[0] == ( - "celestial", - 1, - "spherical.lat.degree", - ) - assert wcs.world_axis_object_components[1][:2] == ("spectral", 0) - assert wcs.world_axis_object_components[2] == ( - "celestial", - 0, - "spherical.lon.degree", - ) + components = wcs.world_axis_object_components + assert len(components) == 3 + assert_celestial_component(components[0], 1) + assert components[1][:2] == ("spectral", 0) + assert_celestial_component(components[2], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -754,11 +713,11 @@ def test_ellipsis_none_types(): [[True, False, True], [False, True, False], [True, False, True]], ) - assert wcs.world_axis_object_components == [ - ("celestial", 1, "spherical.lat.degree"), - ("world", 0, "value"), - ("celestial", 0, "spherical.lon.degree"), - ] + components = wcs.world_axis_object_components + assert len(components) == 3 + assert_celestial_component(components[0], 1) + assert components[1] == ("world", 0, "value") + assert_celestial_component(components[2], 0) assert wcs.world_axis_object_classes["celestial"][0] is SkyCoord assert wcs.world_axis_object_classes["celestial"][1] == () @@ -948,6 +907,7 @@ def test_dropped_dimensions(): dwd = sub.dropped_world_dimensions wao_classes = dwd.pop("world_axis_object_classes") + wao_components = dwd.pop("world_axis_object_components") validate_info_dict( dwd, { @@ -956,12 +916,11 @@ def test_dropped_dimensions(): "world_axis_names": ["Latitude", "Longitude"], "world_axis_units": ["deg", "deg"], "serialized_classes": False, - "world_axis_object_components": [ - ("celestial", 1, "spherical.lat.degree"), - ("celestial", 0, "spherical.lon.degree"), - ], }, ) + assert len(wao_components) == 2 + assert_celestial_component(wao_components[0], 1) + assert_celestial_component(wao_components[1], 0) assert wao_classes["celestial"][0] is SkyCoord assert wao_classes["celestial"][1] == () @@ -972,6 +931,7 @@ def test_dropped_dimensions(): dwd = sub.dropped_world_dimensions wao_classes = dwd.pop("world_axis_object_classes") + wao_components = dwd.pop("world_axis_object_components") validate_info_dict( dwd, { @@ -980,12 +940,11 @@ def test_dropped_dimensions(): "world_axis_names": ["Latitude", "Longitude"], "world_axis_units": ["deg", "deg"], "serialized_classes": False, - "world_axis_object_components": [ - ("celestial", 1, "spherical.lat.degree"), - ("celestial", 0, "spherical.lon.degree"), - ], }, ) + assert len(wao_components) == 2 + assert_celestial_component(wao_components[0], 1) + assert_celestial_component(wao_components[1], 0) assert wao_classes["celestial"][0] is SkyCoord assert wao_classes["celestial"][1] == () @@ -1017,8 +976,8 @@ def test_dropped_dimensions_4d(cube_4d_fitswcs): assert wao_classes["celestial"][2]["unit"] == (u.deg, u.deg) assert wao_classes["spectral"][0:3] == (u.Quantity, (), {}) - assert wao_components[0] == ("celestial", 0, "spherical.lon.degree") - assert wao_components[1] == ("celestial", 1, "spherical.lat.degree") + assert_celestial_component(wao_components[0], 0) + assert_celestial_component(wao_components[1], 1) assert wao_components[2][0:2] == ("spectral", 0) sub = SlicedLowLevelWCS(cube_4d_fitswcs, np.s_[12, 12]) diff --git a/docs/changes/wcs/19506.bugfix.rst b/docs/changes/wcs/19506.bugfix.rst new file mode 100644 index 000000000000..9c33379c049b --- /dev/null +++ b/docs/changes/wcs/19506.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug where degree units were hardcoded into the FITS WCS APE 14 ``world_to_pixel`` method. From 22f7ada3cae9bd54275b7ad5c4200b4fc04ed0a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Tue, 5 May 2026 17:26:24 +0200 Subject: [PATCH 148/199] Backport PR 19656 to v7.2.x (DEP: add a temporary constraint on pandas (<3) --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ae44c8eb77f7..0f896f78d841 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -771,4 +771,7 @@ required-environments = [ constraint-dependencies = [ "asdf>=2.12.0", "asdf-coordinates-schemas>=0.2.0", + + # https://github.com/astropy/astropy/issues/19651 + "pandas<3" ] From 97d4a3524199ba4a26ace9a352e7c573d0b83428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Wed, 6 May 2026 16:13:30 +0200 Subject: [PATCH 149/199] Backport PR #19672: BUG: fix an incorrect conditional (missing method call) --- astropy/utils/data.py | 2 +- docs/changes/utils/19672.bugfix.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/changes/utils/19672.bugfix.rst diff --git a/astropy/utils/data.py b/astropy/utils/data.py index cfa73fc7e5ec..5d20dcb93c2b 100644 --- a/astropy/utils/data.py +++ b/astropy/utils/data.py @@ -2198,7 +2198,7 @@ def cache_contents(pkgname="astropy"): return _NOTHING with os.scandir(dldir) as it: for entry in it: - if entry.is_dir: + if entry.is_dir(): url = get_file_contents( os.path.join(dldir, entry.name, "url"), encoding="utf-8" ) diff --git a/docs/changes/utils/19672.bugfix.rst b/docs/changes/utils/19672.bugfix.rst new file mode 100644 index 000000000000..5cec185fa9b9 --- /dev/null +++ b/docs/changes/utils/19672.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug where values returned by ``cache_contents`` could include files and symlinks, instead of just directories. From 18d17face52300bf79911a6c47d545fdbec119cf Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 6 May 2026 12:20:23 -0400 Subject: [PATCH 150/199] Backport PR #19680: DOC/TYP: fix incorrect return type on an internal function --- astropy/utils/data.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/astropy/utils/data.py b/astropy/utils/data.py index 5d20dcb93c2b..0cdd3b87e746 100644 --- a/astropy/utils/data.py +++ b/astropy/utils/data.py @@ -19,6 +19,7 @@ import urllib.request import zipfile from importlib import import_module +from pathlib import Path from tempfile import NamedTemporaryFile, TemporaryDirectory, gettempdir from types import MappingProxyType from warnings import warn @@ -1890,7 +1891,7 @@ def clear_download_cache(hashorurl=None, pkgname="astropy"): warn(CacheMissingWarning(msg + e.__class__.__name__ + estr)) -def _get_download_cache_loc(pkgname="astropy"): +def _get_download_cache_loc(pkgname: str = "astropy") -> Path: """Finds the path to the cache directory and makes them if they don't exist. Parameters @@ -1902,7 +1903,7 @@ def _get_download_cache_loc(pkgname="astropy"): Returns ------- - datadir : str + datadir : pathlib.Path The path to the data cache directory. """ try: From ff3add4ccc088fb0f8562c1554f52e9b9e33732c Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 6 May 2026 12:57:00 +0100 Subject: [PATCH 151/199] Merge pull request #19592 from saimn/fits-fix-19577 Prevent creating compressed files with (u)int64 data (cherry picked from commit 90aef04ce4fe740038383a5205e298fc0b482628) --- .../fits/hdu/compressed/_tiled_compression.py | 22 ++++++++ .../hdu/compressed/tests/test_compressed.py | 2 +- .../fits/hdu/compressed/tests/test_fitsio.py | 51 ++++++++++++++++++- docs/changes/io.fits/19592.bugfix.rst | 3 ++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 docs/changes/io.fits/19592.bugfix.rst diff --git a/astropy/io/fits/hdu/compressed/_tiled_compression.py b/astropy/io/fits/hdu/compressed/_tiled_compression.py index 24a76d9e570b..e27b64bde5f6 100644 --- a/astropy/io/fits/hdu/compressed/_tiled_compression.py +++ b/astropy/io/fits/hdu/compressed/_tiled_compression.py @@ -5,11 +5,13 @@ """ import sys +import warnings from math import prod import numpy as np from astropy.io.fits.hdu.base import BITPIX2DTYPE +from astropy.utils.exceptions import AstropyUserWarning from ._codecs import PLIO1, Gzip1, Gzip2, HCompress1, NoCompress, Rice1 from ._quantization import DITHER_METHODS, QuantizationFailedException, Quantize @@ -541,6 +543,26 @@ def compress_image_data( if not isinstance(image_data, np.ndarray): raise TypeError("Image data must be a numpy.ndarray") + if ( + compression_type in ("RICE_1", "PLIO_1") + and image_data.dtype.kind == "i" + and image_data.dtype.itemsize == 8 + ): + new_dt = f"{image_data.dtype.byteorder}{image_data.dtype.kind}4" + try: + image_data = image_data.astype(new_dt, casting="same_value") + compressed_header["ZBITPIX"] = 32 + warnings.warn( + f"{compression_type} compression doesn't support 64 integers, " + "data has been converted to 32 bits", + AstropyUserWarning, + ) + except ValueError: + raise ValueError( + f"{compression_type} compression doesn't support 64 integers, " + "but data cannot be converted to 32 bits without overflow", + ) + _check_compressed_header(compressed_header) # TODO: This implementation is memory inefficient as it generates all the diff --git a/astropy/io/fits/hdu/compressed/tests/test_compressed.py b/astropy/io/fits/hdu/compressed/tests/test_compressed.py index 9c1b205b6ad3..3220e150b7c8 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_compressed.py +++ b/astropy/io/fits/hdu/compressed/tests/test_compressed.py @@ -1481,7 +1481,7 @@ def test_reserved_keywords_stripped(tmp_path): # # See also https://github.com/astropy/astropy/issues/18067 - data = np.arange(6).reshape((2, 3)) + data = np.arange(6, dtype="int32").reshape((2, 3)) hdu = fits.CompImageHDU(data) hdu.writeto(tmp_path / "compressed.fits") diff --git a/astropy/io/fits/hdu/compressed/tests/test_fitsio.py b/astropy/io/fits/hdu/compressed/tests/test_fitsio.py index 0a1b4b446d5c..557a2dd44ef3 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_fitsio.py +++ b/astropy/io/fits/hdu/compressed/tests/test_fitsio.py @@ -10,13 +10,15 @@ """ import os +from contextlib import nullcontext import numpy as np import pytest from astropy.io import fits +from astropy.utils.exceptions import AstropyUserWarning -from .conftest import _expand, fitsio_param_to_astropy_param +from .conftest import COMPRESSION_TYPES, _expand, fitsio_param_to_astropy_param # This is so that tox can force this file to be run, and not be silently # skipped on CI, but in all other test runs it's skipped if fitsio isn't present. @@ -230,3 +232,50 @@ def test_compress( with fits.open(astropy_compressed_file_path) as hdul: np.testing.assert_allclose(data, hdul[1].data) + + +@pytest.mark.parametrize( + "nbytes,overflow", [(2, False), (4, False), (8, False), (8, True)] +) +@pytest.mark.parametrize("compression_type", COMPRESSION_TYPES) +def test_decompress_integers(nbytes, overflow, compression_type, tmp_path): + testfile = tmp_path / "test.fits.fz" + data = np.random.poisson(1000, size=(52, 57)).astype(f"i{nbytes}") + if overflow: + data += np.iinfo(np.int32).max + data_hdu = fits.PrimaryHDU(data=data) + compressed_hdu = fits.CompImageHDU(data=data, compression_type=compression_type) + + if compression_type in ("RICE_1", "PLIO_1") and nbytes == 8: + test_func = pytest.raises if overflow else pytest.warns + ctx = test_func( + ValueError if overflow else AstropyUserWarning, + match=f"{compression_type} compression doesn't support 64 integers.*", + ) + nbytes = 4 + else: + ctx = nullcontext() + + with ctx: + compressed_hdu.writeto(testfile) + + if overflow: + # nothing more to test + return + + with fits.open(testfile, disable_image_compression=True) as hdul: + assert hdul[1].header["ZCMPTYPE"] == compression_type + if compression_type == "RICE_1": + assert hdul[1].header["ZNAME2"] == "BYTEPIX" + assert hdul[1].header["ZVAL2"] == nbytes + + with fits.open(testfile) as hdul: + np.testing.assert_array_equal(data, hdul[1].data) + assert hdul[1].data.dtype.kind == np.dtype(data.dtype).kind + assert hdul[1].data.dtype.itemsize == nbytes + + if compression_type != "NOCOMPRESS" and nbytes != 8: + # fitsio does not support NOCOMPRESS or 64-bit data + fts = fitsio.FITS(testfile) + data2 = fts[1].read() + np.testing.assert_array_equal(data, data2) diff --git a/docs/changes/io.fits/19592.bugfix.rst b/docs/changes/io.fits/19592.bugfix.rst new file mode 100644 index 000000000000..3a9de9f60951 --- /dev/null +++ b/docs/changes/io.fits/19592.bugfix.rst @@ -0,0 +1,3 @@ +Prevent creating RICE_1 or PLIO_1 compressed files with (u)int64 data. If +possible without overflow data is converted to (u)int32, otherwise an error is +raised. From 3772d50a4623bf82b9203fa827de45f92d36cb75 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 6 May 2026 22:33:56 +0100 Subject: [PATCH 152/199] Backport PR #19670: Added round-tripping stress test for compression against fitsio for all different int types --- .../fits/hdu/compressed/_tiled_compression.py | 39 +++- .../io/fits/hdu/compressed/tests/conftest.py | 5 +- .../fits/hdu/compressed/tests/test_fitsio.py | 190 +++++++++++++++++- .../tests/test_tiled_compression.py | 6 + docs/changes/io.fits/19670.bugfix.rst | 8 + docs/io/fits/usage/unfamiliar.rst | 103 ++++++++++ 6 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 docs/changes/io.fits/19670.bugfix.rst diff --git a/astropy/io/fits/hdu/compressed/_tiled_compression.py b/astropy/io/fits/hdu/compressed/_tiled_compression.py index e27b64bde5f6..cffff7f0af7a 100644 --- a/astropy/io/fits/hdu/compressed/_tiled_compression.py +++ b/astropy/io/fits/hdu/compressed/_tiled_compression.py @@ -543,15 +543,39 @@ def compress_image_data( if not isinstance(image_data, np.ndarray): raise TypeError("Image data must be a numpy.ndarray") + # PLIO_1 encodes only non-negative integers, but astropy stores unsigned + # FITS data via the BZERO=2**(N-1) convention which produces negative + # values for the lower half of the range. Reject unsigned multi-byte + # input to avoid corrupted writes. if ( - compression_type in ("RICE_1", "PLIO_1") - and image_data.dtype.kind == "i" + compression_type == "PLIO_1" + and image_data.dtype.kind == "u" + and image_data.dtype.itemsize >= 2 + ): + raise ValueError( + "PLIO_1 compression does not support unsigned integers larger than 8 bits" + ) + + if ( + compression_type in ("RICE_1", "PLIO_1", "HCOMPRESS_1") + and image_data.dtype.kind in ("i", "u") and image_data.dtype.itemsize == 8 ): - new_dt = f"{image_data.dtype.byteorder}{image_data.dtype.kind}4" + # numpy's same_value check does not byteswap before comparing values, + # so non-native source data silently truncates instead of raising. + # Convert source to native byte order first. + native_source = image_data.astype( + image_data.dtype.newbyteorder("="), copy=False + ) + new_dt = f"{image_data.dtype.kind}4" try: - image_data = image_data.astype(new_dt, casting="same_value") + image_data = native_source.astype(new_dt, casting="same_value") compressed_header["ZBITPIX"] = 32 + if image_data.dtype.kind == "u": + # The input image header carries BZERO=2**63 for the original + # uint64 storage; after converting to uint32 the offset must + # be 2**31 so the FITS reader reconstructs the right values. + compressed_header["BZERO"] = 2**31 warnings.warn( f"{compression_type} compression doesn't support 64 integers, " "data has been converted to 32 bits", @@ -587,7 +611,12 @@ def compress_image_data( tile_data = image_data[tile_slices] if tile_data.dtype.kind == "u": - if tile_data.dtype.itemsize == 4: + if tile_data.dtype.itemsize == 8: + # Subtract 2**63 in wrap-around uint64 arithmetic, then + # reinterpret the bits as int64. This is equivalent to flipping + # the high bit and matches the FITS BZERO=2**63 convention. + tile_data = (tile_data - np.uint64(2**63)).view(np.int64) + elif tile_data.dtype.itemsize == 4: tile_data = (tile_data.astype(np.int64) - 2**31).astype(np.int32) elif tile_data.dtype.itemsize == 2: tile_data = (tile_data.astype(np.int32) - 2**15).astype(np.int16) diff --git a/astropy/io/fits/hdu/compressed/tests/conftest.py b/astropy/io/fits/hdu/compressed/tests/conftest.py index 6b4990f71076..07f37c2153a1 100644 --- a/astropy/io/fits/hdu/compressed/tests/conftest.py +++ b/astropy/io/fits/hdu/compressed/tests/conftest.py @@ -38,7 +38,10 @@ def _expand(*params): ALL_INTEGER_DTYPES = [ "".join(ele) - for ele in _expand([("<", ">"), ("i",), ("2", "4")], [("<", ">"), ("u",), ("1",)]) + for ele in _expand( + [("<", ">"), ("i",), ("2", "4")], + [("<", ">"), ("u",), ("1", "2", "4")], + ) ] ALL_FLOAT_DTYPES = ["".join(ele) for ele in _expand([("<", ">"), ("f",), ("4", "8")])] diff --git a/astropy/io/fits/hdu/compressed/tests/test_fitsio.py b/astropy/io/fits/hdu/compressed/tests/test_fitsio.py index 557a2dd44ef3..0d66f6189abc 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_fitsio.py +++ b/astropy/io/fits/hdu/compressed/tests/test_fitsio.py @@ -16,6 +16,7 @@ import pytest from astropy.io import fits +from astropy.utils import NumpyRNGContext from astropy.utils.exceptions import AstropyUserWarning from .conftest import COMPRESSION_TYPES, _expand, fitsio_param_to_astropy_param @@ -128,10 +129,25 @@ def fitsio_compressed_file_path( ): pytest.xfail("fitsio won't write these") + if compression_type == "HCOMPRESS_1" and "u2" in dtype: + # cfitsio's HCOMPRESS encoder underestimates the output buffer size + # for uint16 data on certain tile configurations, raising "encode: + # output buffer too small". astropy's encoder handles the same combos. + pytest.xfail("cfitsio HCOMPRESS encoder buffer-size bug for uint16 input") + if compression_type == "PLIO_1" and "f" in dtype: # fitsio fails with a compression error pytest.xfail("fitsio fails to write these") + if ( + compression_type == "PLIO_1" + and np.dtype(dtype).kind == "u" + and np.dtype(dtype).itemsize >= 2 + ): + # PLIO can't represent the BZERO-shifted unsigned values; cfitsio + # rejects 4/8-byte cases outright and segfaults on 2-byte ones. + pytest.xfail("PLIO_1 cannot encode unsigned multi-byte integers") + if compression_type == "NOCOMPRESS": pytest.xfail("fitsio does not support NOCOMPRESS") @@ -166,6 +182,13 @@ def astropy_compressed_file_path( compression_type, param, dtype = comp_param_dtype original_data = base_original_data.astype(dtype) + if ( + compression_type == "PLIO_1" + and np.dtype(dtype).kind == "u" + and np.dtype(dtype).itemsize >= 2 + ): + pytest.xfail("PLIO_1 cannot encode unsigned multi-byte integers") + tmp_path = tmp_path_factory.mktemp("astropy") filename = tmp_path / f"{compression_type}_{dtype}.fits" @@ -234,19 +257,26 @@ def test_compress( np.testing.assert_allclose(data, hdul[1].data) +@pytest.mark.parametrize("kind", ["i", "u"]) @pytest.mark.parametrize( "nbytes,overflow", [(2, False), (4, False), (8, False), (8, True)] ) @pytest.mark.parametrize("compression_type", COMPRESSION_TYPES) -def test_decompress_integers(nbytes, overflow, compression_type, tmp_path): +def test_decompress_integers(nbytes, overflow, compression_type, kind, tmp_path): + if kind == "u" and compression_type == "PLIO_1" and nbytes >= 2: + pytest.skip( + "PLIO_1 cannot encode unsigned multi-byte integers (covered elsewhere)" + ) + testfile = tmp_path / "test.fits.fz" - data = np.random.poisson(1000, size=(52, 57)).astype(f"i{nbytes}") + data = np.random.poisson(1000, size=(52, 57)).astype(f"{kind}{nbytes}") if overflow: - data += np.iinfo(np.int32).max + # push past the 32-bit limit so the conversion fallback fails + data += np.iinfo(np.int32).max if kind == "i" else np.uint64(2**32) data_hdu = fits.PrimaryHDU(data=data) compressed_hdu = fits.CompImageHDU(data=data, compression_type=compression_type) - if compression_type in ("RICE_1", "PLIO_1") and nbytes == 8: + if compression_type in ("RICE_1", "PLIO_1", "HCOMPRESS_1") and nbytes == 8: test_func = pytest.raises if overflow else pytest.warns ctx = test_func( ValueError if overflow else AstropyUserWarning, @@ -279,3 +309,155 @@ def test_decompress_integers(nbytes, overflow, compression_type, tmp_path): fts = fitsio.FITS(testfile) data2 = fts[1].read() np.testing.assert_array_equal(data, data2) + + +INTEGER_DTYPES_FULL_RANGE = [ + "i2", + "i4", + "i8", + "u1", + "u2", + "u4", + "u8", +] + + +def _full_range_integer_data(dtype, shape=(32, 32)): + info = np.iinfo(dtype) + sentinels = np.array( + [ + info.min, + info.min + 1, + -1 if info.min < 0 else 0, + 0, + 1, + info.max - 1, + info.max, + ], + dtype=dtype, + ) + with NumpyRNGContext(0): + data = np.random.randint( + info.min, + info.max + 1, + size=shape, + dtype=dtype.lstrip("<>"), + ).astype(dtype) + data.ravel()[: sentinels.size] = sentinels + return data + + +@pytest.mark.parametrize("dtype", INTEGER_DTYPES_FULL_RANGE) +@pytest.mark.parametrize("compression_type", COMPRESSION_TYPES) +def test_integer_full_range_roundtrip(compression_type, dtype, tmp_path): + """Cross-check exact round-tripping of boundary-spanning integer data + between astropy and fitsio for every standard FITS integer dtype and + every compression algorithm that supports it.""" + data = _full_range_integer_data(dtype) + np_dtype = np.dtype(dtype) + info = np.iinfo(dtype) + astropy_path = tmp_path / "astropy.fits" + hdu = fits.CompImageHDU(data=data, compression_type=compression_type) + + # 64-bit integer data with full-range sentinels overflows the 32-bit + # conversion that RICE_1 and HCOMPRESS_1 fall back to. (PLIO_1 + i8 also + # lands here; PLIO_1 + u8 is caught by the unsigned-multi-byte rejection + # below.) For these combos cfitsio also rejects the input outright, so + # cross-check that fitsio raises too. + if ( + compression_type in ("RICE_1", "HCOMPRESS_1") + and np_dtype.kind in ("i", "u") + and np_dtype.itemsize == 8 + ) or ( + compression_type == "PLIO_1" and np_dtype.kind == "i" and np_dtype.itemsize == 8 + ): + with pytest.raises( + ValueError, + match=( + f"{compression_type} compression doesn't support 64 integers, " + "but data cannot be converted to 32 bits without overflow" + ), + ): + hdu.writeto(astropy_path) + if compression_type == "HCOMPRESS_1": + with pytest.raises( + OSError, + match=r"writing T(U)?LONGLONG to compressed image is not supported", + ): + with fitsio.FITS(tmp_path / "fitsio.fits", "rw") as fts: + fts.write(data, compress=compression_type) + return + + # PLIO_1 cannot encode unsigned multi-byte integers because the FITS + # BZERO=2**(N-1) offset astropy applies to unsigned data produces negative + # values that PLIO rejects. Astropy raises a clean ValueError for all + # unsigned itemsize >= 2; cfitsio raises only for itemsize >= 4 and + # segfaults on itemsize == 2 with full-range data, so the cross-check + # only runs for the 4/8-byte cases. + if compression_type == "PLIO_1" and np_dtype.kind == "u" and np_dtype.itemsize >= 2: + with pytest.raises( + ValueError, + match=r"PLIO_1 compression does not support unsigned integers", + ): + hdu.writeto(astropy_path) + if np_dtype.itemsize >= 4: + with pytest.raises( + ValueError, + match=r"Unsigned 4/8-byte integers currently not allowed", + ): + with fitsio.FITS(tmp_path / "fitsio.fits", "rw") as fts: + fts.write(data, compress=compression_type) + return + + # PLIO_1 also can't encode signed values outside [0, 2**24 - 1]. + if compression_type == "PLIO_1" and (info.min < 0 or info.max > 2**24 - 1): + with pytest.raises( + ValueError, + match=r"data out of range for PLIO compression", + ): + hdu.writeto(astropy_path) + return + + # 1. astropy writes a compressed file. + hdu.writeto(astropy_path) + + # 2. astropy reads its own compressed file. + with fits.open(astropy_path) as hdul: + rt = hdul[1].data + assert rt.dtype.kind == np_dtype.kind + assert rt.dtype.itemsize == np_dtype.itemsize + np.testing.assert_array_equal(rt, data) + + # fitsio doesn't support NOCOMPRESS, so the cross-checks stop here. + # cfitsio also refuses to read or write GZIP-compressed 64-bit integer + # images even though the FITS Tile Compression Convention permits them, + # so astropy's standard-compliant output cannot be cross-validated for + # those combinations. + if compression_type == "NOCOMPRESS" or ( + compression_type in ("GZIP_1", "GZIP_2") and np_dtype.itemsize == 8 + ): + return + + # 3. fitsio reads astropy's compressed file. + with fitsio.FITS(astropy_path) as fts: + rt_fitsio = fts[1].read() + assert rt_fitsio.dtype.kind == np_dtype.kind + assert rt_fitsio.dtype.itemsize == np_dtype.itemsize + np.testing.assert_array_equal(rt_fitsio, data) + + # 4. astropy reads a file fitsio compressed. + fitsio_path = tmp_path / "fitsio.fits" + with fitsio.FITS(fitsio_path, "rw") as fts: + fts.write(data, compress=compression_type) + with fits.open(fitsio_path) as hdul: + rt_astropy = hdul[1].data + assert rt_astropy.dtype.kind == np_dtype.kind + assert rt_astropy.dtype.itemsize == np_dtype.itemsize + np.testing.assert_array_equal(rt_astropy, data) diff --git a/astropy/io/fits/hdu/compressed/tests/test_tiled_compression.py b/astropy/io/fits/hdu/compressed/tests/test_tiled_compression.py index b72336403f9a..695130b9c5ed 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_tiled_compression.py +++ b/astropy/io/fits/hdu/compressed/tests/test_tiled_compression.py @@ -203,6 +203,12 @@ def test_roundtrip_high_D( np.count_nonzero(np.array(shape[:2]) % tile_shape[:2]) != 0 ): pytest.xfail("HCOMPRESS requires 2D tiles.") + if ( + compression_type == "PLIO_1" + and np.dtype(dtype).kind == "u" + and np.dtype(dtype).itemsize >= 2 + ): + pytest.xfail("PLIO_1 cannot encode unsigned multi-byte integers") random = numpy_rng.uniform(high=255, size=shape) # Set first value to be exactly zero as zero values require special treatment # for SUBTRACTIVE_DITHER_2 diff --git a/docs/changes/io.fits/19670.bugfix.rst b/docs/changes/io.fits/19670.bugfix.rst new file mode 100644 index 000000000000..5c037ccd5bc8 --- /dev/null +++ b/docs/changes/io.fits/19670.bugfix.rst @@ -0,0 +1,8 @@ +Fix a bug that caused uint64 data to be silently shifted by 2**63 when +round-tripping through compressed FITS files (the FITS BZERO=2**63 offset was +missing at compression time but still applied on read). Extend the existing +RICE_1 and PLIO_1 64-bit-to-32-bit conversion fallback to also cover uint64 +input and HCOMPRESS_1, and fix big-endian 64-bit input being silently truncated +by the conversion check instead of raising. Reject PLIO_1 with unsigned +multi-byte input outright, since the BZERO convention used to store unsigned +FITS data produces negative values that PLIO cannot encode. diff --git a/docs/io/fits/usage/unfamiliar.rst b/docs/io/fits/usage/unfamiliar.rst index 707657a2aa1a..ceaf6a8bce7e 100644 --- a/docs/io/fits/usage/unfamiliar.rst +++ b/docs/io/fits/usage/unfamiliar.rst @@ -597,3 +597,106 @@ describes the possible options for constructing a :class:`CompImageHDU` object. .. EXAMPLE END + + +Supported Integer Data Types +---------------------------- + +Not every compression algorithm can be used with every integer data type. The +table below summarizes which combinations work, including the cases where +``astropy`` accepts the input only when the values lie within a more +restricted range. + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + + * - Compression + - ``int16`` + - ``int32`` + - ``int64`` + - ``uint8`` + - ``uint16`` + - ``uint32`` + - ``uint64`` + * - ``GZIP_1`` + - ✅ + - ✅ + - ⚠️ [1]_ + - ✅ + - ✅ + - ✅ + - ⚠️ [1]_ + * - ``GZIP_2`` + - ✅ + - ✅ + - ⚠️ [1]_ + - ✅ + - ✅ + - ✅ + - ⚠️ [1]_ + * - ``RICE_1`` + - ✅ + - ✅ + - 🟡 [2]_ + - ✅ + - ✅ + - ✅ + - 🟡 [2]_ + * - ``HCOMPRESS_1`` + - ✅ + - ✅ + - 🟡 [2]_ + - ✅ + - ✅ + - ✅ + - 🟡 [2]_ + * - ``PLIO_1`` + - 🟡 [3]_ + - 🟡 [3]_ + - 🟡 [2]_ [3]_ + - ✅ + - ❌ [4]_ + - ❌ [4]_ + - ❌ [4]_ + * - ``NOCOMPRESS`` + - ✅ + - ✅ + - ✅ + - ✅ + - ✅ + - ✅ + - ✅ + +Legend: + +* ✅ Full support: any value within the type's range round-trips losslessly. +* 🟡 Partial support: works only when input values satisfy the numeric + restriction in the corresponding footnote; a ``ValueError`` is raised + otherwise. +* ⚠️ Caveat: round-trips correctly within ``astropy``, but the resulting + file may not be readable by other FITS libraries (see footnote). +* ❌ Not supported: writing the data raises a ``ValueError``. + +.. [1] ``astropy`` writes a standards-compliant file, but ``cfitsio`` and + tools built on top of it (including ``funpack``, ``fitsio``, and DS9) do + not currently support reading 64-bit integer images compressed with + ``GZIP_1`` or ``GZIP_2``. The file round-trips correctly when read by + ``astropy`` itself. + +.. [2] 64-bit integer input is converted to a 32-bit type on write. The + conversion succeeds only if every input value fits in the corresponding + 32-bit range: ``[-2**31, 2**31 - 1]`` for signed and ``[0, 2**32 - 1]`` + for unsigned. Otherwise a ``ValueError`` is raised. When the conversion + succeeds an ``AstropyUserWarning`` is emitted to signal the precision + change. + +.. [3] ``PLIO_1`` is designed for pixel masks and supports only non-negative + integer values up to ``2**24 - 1`` (``16777215``). Negative values or + values above this limit cause a ``ValueError`` at write time. For + ``int64`` input both this restriction and the 32-bit conversion in + footnote [2]_ apply. + +.. [4] ``PLIO_1`` cannot store unsigned 16-, 32-, or 64-bit integers. Use + ``RICE_1``, ``HCOMPRESS_1``, ``GZIP_1``, or ``GZIP_2`` for unsigned data + that does not fit in ``uint8``. From b7d18b26417f8d4665d58cd52495630ec2edeb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 7 May 2026 14:08:35 +0200 Subject: [PATCH 153/199] Backport PR #19700: BUG: add missing stacklevel arguments in warnings emitted from astropy.utils.data APIs --- astropy/utils/data.py | 19 +++++++++++-------- docs/changes/utils/19700.bugfix.rst | 1 + 2 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 docs/changes/utils/19700.bugfix.rst diff --git a/astropy/utils/data.py b/astropy/utils/data.py index 0cdd3b87e746..01d3d8d07d10 100644 --- a/astropy/utils/data.py +++ b/astropy/utils/data.py @@ -1058,6 +1058,7 @@ def get_pkg_data_path(*path, package=None): f"use astropy.utils.iers.{_IERS_DATA_REDIRECTS[filename][0]} " "instead.", AstropyDeprecationWarning, + stacklevel=2, ) return _IERS_DATA_REDIRECTS[filename][1] @@ -1269,7 +1270,7 @@ def _try_url_open( raise urllib.error.URLError(msg) else: msg += ". Re-trying with allow_insecure=True." - warn(msg, AstropyWarning) + warn(msg, AstropyWarning, stacklevel=2) # Try again with a new urlopener allowing insecure connections urlopener = _build_urlopener( ftp_tls=ftp_tls, ssl_context=ssl_context, allow_insecure=True @@ -1607,7 +1608,7 @@ def download_file( # FIXME: other kinds of cache problem can occur? if missing_cache: - warn(CacheMissingWarning(missing_cache, f_name)) + warn(CacheMissingWarning(missing_cache, f_name), stacklevel=2) if conf.delete_temporary_downloads_at_exit: _tempfilestodel.append(f_name) return os.path.abspath(f_name) @@ -1760,6 +1761,7 @@ def download_files_in_parallel( 'it will be set to ``"update"``. You may need to manually remove ' "the cached files with clear_download_cache() afterwards.", AstropyWarning, + stacklevel=2, ) cache = "update" @@ -1852,7 +1854,7 @@ def clear_download_cache(hashorurl=None, pkgname="astropy"): # Just a warning, though msg = "Not clearing data cache - cache inaccessible due to " estr = "" if len(e.args) < 1 else (": " + str(e)) - warn(CacheMissingWarning(msg + e.__class__.__name__ + estr)) + warn(CacheMissingWarning(msg + e.__class__.__name__ + estr), stacklevel=2) return try: if hashorurl is None: @@ -1888,7 +1890,7 @@ def clear_download_cache(hashorurl=None, pkgname="astropy"): except OSError as e: msg = "Not clearing data from cache - problem arose " estr = "" if len(e.args) < 1 else (": " + str(e)) - warn(CacheMissingWarning(msg + e.__class__.__name__ + estr)) + warn(CacheMissingWarning(msg + e.__class__.__name__ + estr), stacklevel=2) def _get_download_cache_loc(pkgname: str = "astropy") -> Path: @@ -1922,7 +1924,7 @@ def _get_download_cache_loc(pkgname: str = "astropy") -> Path: except OSError as e: msg = "Remote data cache could not be accessed due to " estr = "" if len(e.args) < 1 else (": " + str(e)) - warn(CacheMissingWarning(msg + e.__class__.__name__ + estr)) + warn(CacheMissingWarning(msg + e.__class__.__name__ + estr), stacklevel=2) raise @@ -2060,12 +2062,13 @@ def _rmtree(path, replace=None): f"Unable to remove directory {path} because a file in it " "is in use and you are on Windows", path, - ) + ), + stacklevel=2, ) raise except OSError as e: if e.errno == errno.EXDEV: - warn(e.strerror, AstropyWarning) + warn(e.strerror or "", AstropyWarning, stacklevel=2) shutil.move(path, os.path.join(d, "to-zap")) else: raise @@ -2081,7 +2084,7 @@ def _rmtree(path, replace=None): # already there, fine pass elif e.errno == errno.EXDEV: - warn(e.strerror, AstropyWarning) + warn(e.strerror or "", AstropyWarning, stacklevel=2) shutil.move(replace, path) else: raise diff --git a/docs/changes/utils/19700.bugfix.rst b/docs/changes/utils/19700.bugfix.rst new file mode 100644 index 000000000000..b44f48635547 --- /dev/null +++ b/docs/changes/utils/19700.bugfix.rst @@ -0,0 +1 @@ +Add missing ``stacklevel`` arguments in warnings emitted from ``astropy.utils.data`` APIs From d3aa7afc936152d4bfa0654172febf88751078ac Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 7 May 2026 10:25:51 -0400 Subject: [PATCH 154/199] Backport PR #19705: DOC: fix a typo in a docstring --- astropy/coordinates/matrix_utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astropy/coordinates/matrix_utilities.py b/astropy/coordinates/matrix_utilities.py index ac1eea3b1f50..5f17e4871ac2 100644 --- a/astropy/coordinates/matrix_utilities.py +++ b/astropy/coordinates/matrix_utilities.py @@ -216,7 +216,7 @@ def is_rotation(matrix, allow_improper=False, atol=None): See Also -------- - astopy.coordinates.matrix_utilities.is_O3 : + astropy.coordinates.matrix_utilities.is_O3 : For the less restrictive check that a matrix is in the group O(3). Notes From 79cebc025ae7f2214d4172b24f70e326cccad3d2 Mon Sep 17 00:00:00 2001 From: Wang Rui <55612496+wr-web@users.noreply.github.com> Date: Tue, 12 May 2026 04:38:36 +0800 Subject: [PATCH 155/199] Backport PR #19720: Fix incorrect `Py_DECREF` call on stolen reference after `PyList_SetItem` failure --- astropy/wcs/src/pyutil.c | 2 -- astropy/wcs/src/wcslib_wrap.c | 3 --- docs/changes/wcs/19720.bugfix.rst | 1 + 3 files changed, 1 insertion(+), 5 deletions(-) create mode 100644 docs/changes/wcs/19720.bugfix.rst diff --git a/astropy/wcs/src/pyutil.c b/astropy/wcs/src/pyutil.c index 5418de2da75d..504d4f8f0625 100644 --- a/astropy/wcs/src/pyutil.c +++ b/astropy/wcs/src/pyutil.c @@ -735,7 +735,6 @@ get_pscards( } if (PyList_SetItem(result, i, subresult)) { - Py_DECREF(subresult); Py_DECREF(result); return NULL; } @@ -846,7 +845,6 @@ get_pvcards( } if (PyList_SetItem(result, i, subresult)) { - Py_DECREF(subresult); Py_DECREF(result); return NULL; } diff --git a/astropy/wcs/src/wcslib_wrap.c b/astropy/wcs/src/wcslib_wrap.c index b0c8a8027266..ce50954f2f32 100755 --- a/astropy/wcs/src/wcslib_wrap.c +++ b/astropy/wcs/src/wcslib_wrap.c @@ -930,7 +930,6 @@ PyWcsprm_find_all_wcs( } if (PyList_SetItem(result, i, (PyObject *)subresult) == -1) { - Py_DECREF(subresult); Py_DECREF(result); wcsvfree(&nwcs, &wcs); return NULL; @@ -4305,7 +4304,6 @@ PyWcsprm_get_tab( } if (PyList_SetItem(result, i, subresult) == -1) { - Py_DECREF(subresult); Py_DECREF(result); return NULL; } @@ -4657,7 +4655,6 @@ int add_prj_codes(PyObject* module) for (k = 0; k < prj_ncode; k++) { code = PyUnicode_FromString(prj_codes[k]); if (PyList_SetItem(list, k, code)) { - Py_DECREF(code); Py_DECREF(list); return -1; } diff --git a/docs/changes/wcs/19720.bugfix.rst b/docs/changes/wcs/19720.bugfix.rst new file mode 100644 index 000000000000..904186bcbf61 --- /dev/null +++ b/docs/changes/wcs/19720.bugfix.rst @@ -0,0 +1 @@ +Fix reference-count handling after ``PyList_SetItem`` steals references. From f04aa13eae9cca790d6e0a140e3a55b9838dcf97 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 12 May 2026 07:03:11 +0100 Subject: [PATCH 156/199] Backport PR #19722: Update credits to add new contributors --- docs/credits.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/credits.rst b/docs/credits.rst index cd27633dad29..2848ccdc305a 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -38,6 +38,7 @@ Core Package Contributors * Alfio Puglisi * Alpha-Ursae-Minoris * Amit Kumar +* Ana Clara Galvão * Ana Posses * Anany Shrey Jain * Anchit Jain @@ -559,6 +560,7 @@ Core Package Contributors * Sushobhana Patra * Suyog Garg * Swapnil Sharma +* Syn Pu * T\. Carl Beery * T\. E\. Pickering * Tanuj Rastogi From 56774a0cadd0e2b1dc9ef02ab5688b4490f9b0c3 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 15 May 2026 13:04:37 +0100 Subject: [PATCH 157/199] Backport PR #19738: Fix round-tripping of i1/int8 values in compressed FITS --- .../io/fits/hdu/compressed/_tiled_compression.py | 5 +++++ astropy/io/fits/hdu/compressed/tests/conftest.py | 2 +- .../io/fits/hdu/compressed/tests/test_fitsio.py | 15 +++++++++++++-- docs/changes/io.fits/19738.bugfix.rst | 4 ++++ 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 docs/changes/io.fits/19738.bugfix.rst diff --git a/astropy/io/fits/hdu/compressed/_tiled_compression.py b/astropy/io/fits/hdu/compressed/_tiled_compression.py index cffff7f0af7a..e7ae2fc7a624 100644 --- a/astropy/io/fits/hdu/compressed/_tiled_compression.py +++ b/astropy/io/fits/hdu/compressed/_tiled_compression.py @@ -620,6 +620,11 @@ def compress_image_data( tile_data = (tile_data.astype(np.int64) - 2**31).astype(np.int32) elif tile_data.dtype.itemsize == 2: tile_data = (tile_data.astype(np.int32) - 2**15).astype(np.int16) + elif tile_data.dtype.kind == "i" and tile_data.dtype.itemsize == 1: + # FITS BITPIX=8 storage is unsigned, so int8 input is recorded via + # BZERO=-128 and the stored bytes must be shifted by +128. Without + # this the BZERO=-128 applied on read offsets every value by 128. + tile_data = (tile_data.astype(np.int16) + 128).astype(np.uint8) settings = _update_tile_settings(settings, compression_type, tile_data.shape) diff --git a/astropy/io/fits/hdu/compressed/tests/conftest.py b/astropy/io/fits/hdu/compressed/tests/conftest.py index 07f37c2153a1..f5acee9a27eb 100644 --- a/astropy/io/fits/hdu/compressed/tests/conftest.py +++ b/astropy/io/fits/hdu/compressed/tests/conftest.py @@ -39,7 +39,7 @@ def _expand(*params): ALL_INTEGER_DTYPES = [ "".join(ele) for ele in _expand( - [("<", ">"), ("i",), ("2", "4")], + [("<", ">"), ("i",), ("1", "2", "4")], [("<", ">"), ("u",), ("1", "2", "4")], ) ] diff --git a/astropy/io/fits/hdu/compressed/tests/test_fitsio.py b/astropy/io/fits/hdu/compressed/tests/test_fitsio.py index 0d66f6189abc..07fde0daa05d 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_fitsio.py +++ b/astropy/io/fits/hdu/compressed/tests/test_fitsio.py @@ -135,6 +135,10 @@ def fitsio_compressed_file_path( # output buffer too small". astropy's encoder handles the same combos. pytest.xfail("cfitsio HCOMPRESS encoder buffer-size bug for uint16 input") + if compression_type == "HCOMPRESS_1" and "i1" in dtype: + # Same buffer-sizing limitation in cfitsio applies to int8 input. + pytest.xfail("cfitsio HCOMPRESS encoder buffer-size bug for int8 input") + if compression_type == "PLIO_1" and "f" in dtype: # fitsio fails with a compression error pytest.xfail("fitsio fails to write these") @@ -312,6 +316,8 @@ def test_decompress_integers(nbytes, overflow, compression_type, kind, tmp_path) INTEGER_DTYPES_FULL_RANGE = [ + "i1", "i2", " 2**24 - 1): + # PLIO_1 also can't encode signed values outside [0, 2**24 - 1]. int8 is + # exempt because the BZERO=-128 transform leaves the stored bytes in + # [0, 255], which PLIO_1 encodes fine. + is_int8 = np_dtype.kind == "i" and np_dtype.itemsize == 1 + if compression_type == "PLIO_1" and ( + (not is_int8 and info.min < 0) or info.max > 2**24 - 1 + ): with pytest.raises( ValueError, match=r"data out of range for PLIO compression", diff --git a/docs/changes/io.fits/19738.bugfix.rst b/docs/changes/io.fits/19738.bugfix.rst new file mode 100644 index 000000000000..5839e96cb25b --- /dev/null +++ b/docs/changes/io.fits/19738.bugfix.rst @@ -0,0 +1,4 @@ +Fix a bug that caused int8 data to be shifted by 128 when round-tripping +through compressed FITS files (the FITS BZERO=-128 offset was missing at +compression time but still applied on read), so writing ``[0, 1]`` and +reading back returned ``[-128, -127]``. From be044af979a46d350bdbfc49dbce9c355e23125f Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 15 May 2026 20:58:18 +0100 Subject: [PATCH 158/199] Backport PR #19748: Make class-scoped cosmology test fixtures @classmethod for pytest 9.1 --- .../cosmology/_src/tests/flrw/test_base.py | 3 +- .../_src/tests/funcs/test_comparison.py | 29 +++++++----- astropy/cosmology/_src/tests/io/base.py | 44 ++++++++++++------- .../cosmology/_src/tests/io/test_connect.py | 12 +++-- astropy/cosmology/_src/tests/io/test_json.py | 3 +- astropy/cosmology/_src/tests/io/test_model.py | 3 +- astropy/cosmology/_src/tests/io/test_yaml.py | 3 +- .../_src/tests/parameter/test_parameter.py | 14 +++--- astropy/cosmology/_src/tests/test_core.py | 14 ++++-- astropy/cosmology/_src/tests/test_units.py | 3 +- 10 files changed, 84 insertions(+), 44 deletions(-) diff --git a/astropy/cosmology/_src/tests/flrw/test_base.py b/astropy/cosmology/_src/tests/flrw/test_base.py index 01a77803f30b..48cc07ebbe4c 100644 --- a/astropy/cosmology/_src/tests/flrw/test_base.py +++ b/astropy/cosmology/_src/tests/flrw/test_base.py @@ -75,7 +75,8 @@ def setup_class(self): ) @pytest.fixture(scope="class") - def nonflatcosmo(self): + @classmethod + def nonflatcosmo(cls): """A non-flat cosmology used in equivalence tests.""" return LambdaCDM(70, 0.4, 0.8) diff --git a/astropy/cosmology/_src/tests/funcs/test_comparison.py b/astropy/cosmology/_src/tests/funcs/test_comparison.py index 80999a7bc81d..8ecb0cd55f2a 100644 --- a/astropy/cosmology/_src/tests/funcs/test_comparison.py +++ b/astropy/cosmology/_src/tests/funcs/test_comparison.py @@ -33,11 +33,13 @@ class ComparisonFunctionTestBase(ToFromTestMixinBase): """ @pytest.fixture(scope="class") - def cosmo(self): + @classmethod + def cosmo(cls): return Planck18 @pytest.fixture(scope="class") - def cosmo_eqvxflat(self, cosmo): + @classmethod + def cosmo_eqvxflat(cls, cosmo): if isinstance(cosmo, FlatCosmologyMixin): return cosmo.nonflat @@ -51,23 +53,27 @@ def cosmo_eqvxflat(self, cosmo): {k for k, _ in convert_registry._readers.keys()} - {"astropy.cosmology"} ), ) - def format(self, request): + @classmethod + def format(cls, request): return request.param @pytest.fixture(scope="class") - def xfail_cant_autoidentify(self, format): + @classmethod + def xfail_cant_autoidentify(cls, format): """`pytest.fixture` form of method ``can_autoidentify`.""" - if not self.can_autodentify(format): + if not cls.can_autodentify(format): pytest.xfail("cannot autoidentify") @pytest.fixture(scope="class") - def converted(self, to_format, format): + @classmethod + def converted(cls, to_format, format): if format == "astropy.model": # special case Model return to_format(format, method="comoving_distance") return to_format(format) @pytest.fixture(scope="class") - def pert_cosmo(self, cosmo): + @classmethod + def pert_cosmo(cls, cosmo): # change one parameter p, v = next(iter(cosmo.parameters.items())) return cosmo.clone( @@ -75,7 +81,8 @@ def pert_cosmo(self, cosmo): ) @pytest.fixture(scope="class") - def pert_cosmo_eqvxflat(self, pert_cosmo): + @classmethod + def pert_cosmo_eqvxflat(cls, pert_cosmo): if isinstance(pert_cosmo, FlatCosmologyMixin): return pert_cosmo.nonflat @@ -84,7 +91,8 @@ def pert_cosmo_eqvxflat(self, pert_cosmo): ) @pytest.fixture(scope="class") - def pert_converted(self, pert_cosmo, format): + @classmethod + def pert_converted(cls, pert_cosmo, format): if format == "astropy.model": # special case Model return pert_cosmo.to_format(format, method="comoving_distance") return pert_cosmo.to_format(format) @@ -94,7 +102,8 @@ class Test_parse_format(ComparisonFunctionTestBase): """Test functions ``_parse_format``.""" @pytest.fixture(scope="class") - def converted(self, to_format, format): + @classmethod + def converted(cls, to_format, format): if format == "astropy.model": # special case Model return to_format(format, method="comoving_distance") diff --git a/astropy/cosmology/_src/tests/io/base.py b/astropy/cosmology/_src/tests/io/base.py index 5ca1d4203bb6..26c3385ba6eb 100644 --- a/astropy/cosmology/_src/tests/io/base.py +++ b/astropy/cosmology/_src/tests/io/base.py @@ -36,16 +36,19 @@ class ToFromTestMixinBase(IOTestBase): """ @pytest.fixture(scope="class") - def from_format(self): + @classmethod + def from_format(cls): """Convert to Cosmology using ``Cosmology.from_format()``.""" return Cosmology.from_format @pytest.fixture(scope="class") - def to_format(self, cosmo): + @classmethod + def to_format(cls, cosmo): """Convert Cosmology instance using ``.to_format()``.""" return cosmo.to_format - def can_autodentify(self, format): + @staticmethod + def can_autodentify(format): """Check whether a format can auto-identify.""" return format in Cosmology.from_format.registry._identifiers @@ -61,12 +64,14 @@ class ReadWriteTestMixinBase(IOTestBase): """ @pytest.fixture(scope="class") - def read(self): + @classmethod + def read(cls): """Read Cosmology instance using ``Cosmology.read()``.""" return Cosmology.read @pytest.fixture(scope="class") - def write(self, cosmo): + @classmethod + def write(cls, cosmo): """Write Cosmology using ``.write()``.""" return cosmo.write @@ -95,7 +100,8 @@ class IODirectTestBase(IOTestBase): """ @pytest.fixture(scope="class", autouse=True) - def setup(self): + @classmethod + def setup(cls): """Setup and teardown for tests.""" @dataclass_decorator @@ -115,14 +121,16 @@ def __init__( _COSMOLOGY_CLASSES.pop(CosmologyWithKwargs.__qualname__, None) @pytest.fixture(scope="class", params=cosmo_instances) - def cosmo(self, request): + @classmethod + def cosmo(cls, request): """Cosmology instance.""" if isinstance(request.param, str): # CosmologyWithKwargs return _COSMOLOGY_CLASSES[request.param](Tcmb0=3) return request.param @pytest.fixture(scope="class") - def cosmo_cls(self, cosmo): + @classmethod + def cosmo_cls(cls, cosmo): """Cosmology classes.""" return cosmo.__class__ @@ -146,21 +154,23 @@ class ToFromDirectTestBase(IODirectTestBase, ToFromTestMixinBase): """ @pytest.fixture(scope="class") - def from_format(self): + @classmethod + def from_format(cls): """Convert to Cosmology using function ``from``.""" def use_from_format(*args, **kwargs): kwargs.pop("format", None) # specific to Cosmology.from_format - return self.functions["from"](*args, **kwargs) + return cls.functions["from"](*args, **kwargs) return use_from_format @pytest.fixture(scope="class") - def to_format(self, cosmo): + @classmethod + def to_format(cls, cosmo): """Convert Cosmology to format using function ``to``.""" def use_to_format(*args, **kwargs): - return self.functions["to"](cosmo, *args, **kwargs) + return cls.functions["to"](cosmo, *args, **kwargs) return use_to_format @@ -184,20 +194,22 @@ class ReadWriteDirectTestBase(IODirectTestBase, ToFromTestMixinBase): """ @pytest.fixture(scope="class") - def read(self): + @classmethod + def read(cls): """Read Cosmology from file using function ``read``.""" def use_read(*args, **kwargs): kwargs.pop("format", None) # specific to Cosmology.from_format - return self.functions["read"](*args, **kwargs) + return cls.functions["read"](*args, **kwargs) return use_read @pytest.fixture(scope="class") - def write(self, cosmo): + @classmethod + def write(cls, cosmo): """Write Cosmology to file using function ``write``.""" def use_write(*args, **kwargs): - return self.functions["write"](cosmo, *args, **kwargs) + return cls.functions["write"](cosmo, *args, **kwargs) return use_write diff --git a/astropy/cosmology/_src/tests/io/test_connect.py b/astropy/cosmology/_src/tests/io/test_connect.py index 34b9c8e527a0..8c3df96e6783 100644 --- a/astropy/cosmology/_src/tests/io/test_connect.py +++ b/astropy/cosmology/_src/tests/io/test_connect.py @@ -135,11 +135,13 @@ class TestCosmologyReadWrite(ReadWriteTestMixin): """Test the classes CosmologyRead/Write.""" @pytest.fixture(scope="class", params=cosmo_instances) - def cosmo(self, request): + @classmethod + def cosmo(cls, request): return getattr(cosmology.realizations, request.param) @pytest.fixture(scope="class") - def cosmo_cls(self, cosmo): + @classmethod + def cosmo_cls(cls, cosmo): return cosmo.__class__ # ============================================================== @@ -267,11 +269,13 @@ class TestCosmologyToFromFormat(ToFromFormatTestMixin): """Test Cosmology[To/From]Format classes.""" @pytest.fixture(scope="class", params=cosmo_instances) - def cosmo(self, request): + @classmethod + def cosmo(cls, request): return getattr(cosmology.realizations, request.param) @pytest.fixture(scope="class") - def cosmo_cls(self, cosmo): + @classmethod + def cosmo_cls(cls, cosmo): return cosmo.__class__ # ============================================================== diff --git a/astropy/cosmology/_src/tests/io/test_json.py b/astropy/cosmology/_src/tests/io/test_json.py index d6bc745eb253..4f04f6329a82 100644 --- a/astropy/cosmology/_src/tests/io/test_json.py +++ b/astropy/cosmology/_src/tests/io/test_json.py @@ -95,7 +95,8 @@ class ReadWriteJSONTestMixin(ReadWriteTestMixinBase): """ @pytest.fixture(scope="class", autouse=True) - def register_and_unregister_json(self): + @classmethod + def register_and_unregister_json(cls): """Setup & teardown for JSON read/write tests.""" # Register readwrite_registry.register_reader("json", Cosmology, read_json, force=True) diff --git a/astropy/cosmology/_src/tests/io/test_model.py b/astropy/cosmology/_src/tests/io/test_model.py index 1047649861e6..4fb0a64a93ef 100644 --- a/astropy/cosmology/_src/tests/io/test_model.py +++ b/astropy/cosmology/_src/tests/io/test_model.py @@ -32,7 +32,8 @@ class ToFromModelTestMixin(ToFromTestMixinBase): """ @pytest.fixture(scope="class") - def method_name(self, cosmo): + @classmethod + def method_name(cls, cosmo): # get methods, ignoring private and dunder methods = get_redshift_methods(cosmo, include_private=False, include_z2=True) diff --git a/astropy/cosmology/_src/tests/io/test_yaml.py b/astropy/cosmology/_src/tests/io/test_yaml.py index 0e2a70ceceac..4ddfc8f6f6be 100644 --- a/astropy/cosmology/_src/tests/io/test_yaml.py +++ b/astropy/cosmology/_src/tests/io/test_yaml.py @@ -168,7 +168,8 @@ def setup_class(self): self.functions = {"to": to_yaml, "from": from_yaml} @pytest.fixture(scope="class", autouse=True) - def setup(self): + @classmethod + def setup(cls): """ Setup and teardown for tests. This overrides from super because `ToFromDirectTestBase` adds a custom diff --git a/astropy/cosmology/_src/tests/parameter/test_parameter.py b/astropy/cosmology/_src/tests/parameter/test_parameter.py index e7b615748023..a1646a694578 100644 --- a/astropy/cosmology/_src/tests/parameter/test_parameter.py +++ b/astropy/cosmology/_src/tests/parameter/test_parameter.py @@ -225,22 +225,26 @@ def teardown_class(self): _COSMOLOGY_CLASSES.pop(cls.__qualname__, None) @pytest.fixture(scope="class", params=["Example1", "Example2"]) - def cosmo_cls(self, request): + @classmethod + def cosmo_cls(cls, request): """Cosmology class.""" - return self.classes[request.param] + return cls.classes[request.param] @pytest.fixture(scope="class") - def cosmo(self, cosmo_cls): + @classmethod + def cosmo(cls, cosmo_cls): """Cosmology instance""" return cosmo_cls() @pytest.fixture(scope="class") - def param(self, cosmo_cls): + @classmethod + def param(cls, cosmo_cls): """Get Parameter 'param' from cosmology class.""" return cosmo_cls.parameters["param"] @pytest.fixture(scope="class") - def param_cls(self, param): + @classmethod + def param_cls(cls, param): """Get Parameter class from cosmology class.""" return type(param) diff --git a/astropy/cosmology/_src/tests/test_core.py b/astropy/cosmology/_src/tests/test_core.py index 66b36eb9afe0..6f3dcc262150 100644 --- a/astropy/cosmology/_src/tests/test_core.py +++ b/astropy/cosmology/_src/tests/test_core.py @@ -119,9 +119,10 @@ def cls_args(self): return tuple(self._cls_args.values()) @pytest.fixture(scope="class") - def cosmo_cls(self): + @classmethod + def cosmo_cls(cls): """The Cosmology class as a :func:`pytest.fixture`.""" - return self.cls + return cls.cls @pytest.fixture(scope="function") # ensure not cached. def ba(self): @@ -131,9 +132,14 @@ def ba(self): return ba @pytest.fixture(scope="class") - def cosmo(self, cosmo_cls): + @classmethod + def cosmo(cls, cosmo_cls): """The cosmology instance with which to test.""" - ba = inspect.signature(self.cls).bind(*self.cls_args, **self.cls_kwargs) + # `cls_args` is a @property on the test class, which doesn't fire + # when accessed via cls (only via self), so dereference _cls_args + # directly here. + cls_args = tuple(cls._cls_args.values()) + ba = inspect.signature(cls.cls).bind(*cls_args, **cls.cls_kwargs) ba.apply_defaults() return cosmo_cls(*ba.args, **ba.kwargs) diff --git a/astropy/cosmology/_src/tests/test_units.py b/astropy/cosmology/_src/tests/test_units.py index 72497562de19..2c67498f4a9f 100644 --- a/astropy/cosmology/_src/tests/test_units.py +++ b/astropy/cosmology/_src/tests/test_units.py @@ -172,7 +172,8 @@ class Test_with_redshift: """Test `astropy.cosmology.units.with_redshift`.""" @pytest.fixture(scope="class") - def cosmo(self): + @classmethod + def cosmo(cls): """Test cosmology.""" return Planck13.clone(Tcmb0=3 * u.K) From d3033df1c631dd7cfe15b7257a5d4586c157c45f Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 18 May 2026 08:26:11 -0400 Subject: [PATCH 159/199] Backport PR #19757: Update credits to add new contributors --- docs/credits.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/credits.rst b/docs/credits.rst index 2848ccdc305a..e2440429a6e5 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -599,6 +599,7 @@ Core Package Contributors * Vital Fernández * Volodymyr Savchenko * VSN Reddy Janga +* Wang Rui * Wilfred Tyler Gee * William Jamieson * Wolfgang Kerzendorf From b7ef80cd19ec6f06bd787c7ae249e7121f5304cd Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 18 May 2026 15:32:56 +0000 Subject: [PATCH 160/199] Update minimum required version of astropy-iers-data --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0f896f78d841..edd93978dd37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ keywords = [ "ascii", ] dependencies = [ - "astropy-iers-data>=0.2026.4.27.1.3.2", + "astropy-iers-data>=0.2026.5.18.1.11.28", "numpy>=1.24, <2.7", "packaging>=22.0.0", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires From 49fb5bf9d04945b726a275e57183a33acf3a4411 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 21 May 2026 12:25:51 -0400 Subject: [PATCH 161/199] Backport PR #19787: DOC: fix duplicate word in access_table.rst --- docs/table/access_table.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/table/access_table.rst b/docs/table/access_table.rst index 45f753ed45e5..3c87b53ad51f 100644 --- a/docs/table/access_table.rst +++ b/docs/table/access_table.rst @@ -817,7 +817,7 @@ Structured array columns .. EXAMPLE START: Creating a formatted Astropy Table with a Structured Column -For columns which are structured arrays, the format string must be a a string +For columns which are structured arrays, the format string must be a string that uses `"new style" format strings `_ with parameter substitutions corresponding to the field names in the structured From 04c9b0264bbaf8fe0978e7a42ba97d671b8635af Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Fri, 22 May 2026 09:06:06 -0400 Subject: [PATCH 162/199] Backport PR #19780: DOC: Replace URL in broken remote doctest --- docs/io/unified_table.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/io/unified_table.rst b/docs/io/unified_table.rst index 584b7a47c7e0..c94d6c0679c0 100644 --- a/docs/io/unified_table.rst +++ b/docs/io/unified_table.rst @@ -57,15 +57,12 @@ the file format, for instance ``'ascii.daophot'``: Reading a Table directly from the Internet It is possible to load tables directly from the Internet using URLs. For -example, download tables from Vizier catalogues in CDS format -(``'ascii.cds'``): +example, download an example Spitzer catalog (a :ref:`VOTable `): .. doctest-remote-data:: >>> from astropy.table import Table - >>> t = Table.read("ftp://cdsarc.unistra.fr/pub/cats/VII/253/snrs.dat", - ... readme="ftp://cdsarc.unistra.fr/pub/cats/VII/253/ReadMe", - ... format="ascii.cds") + >>> t = Table.read("http://www.astropy.org/astropy-data/photometry/spitzer_example_catalog.xml", format="votable") For certain file formats the format can be automatically detected, for example, from the filename extension:: From 37e5ba35ad1de063cbf8502157c94c205df81ee6 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 28 May 2026 10:56:59 -0400 Subject: [PATCH 163/199] Backport PR #19820: ECVS -> ECSV typo in io docs --- docs/io/misc.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/io/misc.rst b/docs/io/misc.rst index 587aa3ea2f3a..803fd18eb18c 100644 --- a/docs/io/misc.rst +++ b/docs/io/misc.rst @@ -1,6 +1,6 @@ -************************************************************** -ECVS, HDF5, Parquet, PyArrow CSV, YAML (`astropy.io.misc`) -************************************************************** +********************************************************** +ECSV, HDF5, Parquet, PyArrow CSV, YAML (`astropy.io.misc`) +********************************************************** The `astropy.io.misc` module contains miscellaneous input/output routines that do not fit elsewhere, and are often used by other ``astropy`` sub-packages. For From 3f3aaa0f57341e34c965ada27755bf06f57ffa6d Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 28 May 2026 15:05:49 +0100 Subject: [PATCH 164/199] Backport PR #19819 to v7.2.x (Fix multi-threading issues in astropy.wcs) (cherry picked from commit cc836205dcd820469db3751cfce29232f9202b10) --- astropy/wcs/src/astropy_wcs.c | 30 ++++++++--- astropy/wcs/src/wcslib_wrap.c | 84 +++++++++++++++++++++++-------- docs/changes/wcs/19819.bugfix.rst | 3 ++ 3 files changed, 89 insertions(+), 28 deletions(-) create mode 100644 docs/changes/wcs/19819.bugfix.rst diff --git a/astropy/wcs/src/astropy_wcs.c b/astropy/wcs/src/astropy_wcs.c index e98e9b7c4172..b72ec6acb76a 100644 --- a/astropy/wcs/src/astropy_wcs.c +++ b/astropy/wcs/src/astropy_wcs.c @@ -249,24 +249,38 @@ Wcs_all_pix2world( goto exit; } - // Here we force a call to wcsset. Normally, WCSLIB will call wcsset automatically when - // calling wcsp2s, but we need to call it ourselves using PyWcsprm_cset so that we can - // catch cases where the units might change if e.g. they are not in SI to start with. - /* Force a call to wcsset here*/ - if (((PyWcsprm*)(self->py_wcsprm))->preserve_units && PyWcsprm_cset(((PyWcsprm*)(self->py_wcsprm)), 1)) { + // Force a call to wcsset via Wcsprm_cset before entering the parallel + // region. This serves two purposes: (1) preserve_units unit-change + // detection (the original reason), and (2) ensuring wcs->flag == WCSSET + // so the wcsp2s call below will not invoke wcsset itself. wcsset is the + // only writer wcsp2s would otherwise trigger, and by running it eagerly + // under the GIL here (short-circuited by wcsenq on subsequent calls) we + // can safely drop the wcsprm_python2c / wcsprm_c2python round-trip from + // around pipeline_all_pixel2world. + if (PyWcsprm_cset(((PyWcsprm*)(self->py_wcsprm)), 1)) { return NULL; } - /* Make the call */ + /* Make the call. + * + * The pipeline (det2im / SIP / distortion / wcsp2s stages) is read-only + * on the wcsprm struct -- after wcsset has run (now hoisted into + * Wcsprm_cset above), the transform routines consume the precomputed + * sub-structs wcs->lin / wcs->cel / wcs->spc plus wcs->crval[i], and + * never re-read the raw arrays (cd, cdelt, crpix, crota, obsgeo, + * mjdobs, ...) that the wcsprm_python2c / wcsprm_c2python pair was + * rewriting NaN <-> UNDEFINED in place. The round-trip can therefore + * be dropped here without changing the transform's output, and + * concurrent threads no longer race on those raw arrays + *. + */ Py_BEGIN_ALLOW_THREADS preoffset_array(pixcrd, origin); - wcsprm_python2c(self->x.wcs); status = pipeline_all_pixel2world(&self->x, (unsigned int)ncoord, (unsigned int)nelem, (double*)PyArray_DATA(pixcrd), (double*)PyArray_DATA(world)); - wcsprm_c2python(self->x.wcs); unoffset_array(pixcrd, origin); Py_END_ALLOW_THREADS /* unoffset_array(world, origin); */ diff --git a/astropy/wcs/src/wcslib_wrap.c b/astropy/wcs/src/wcslib_wrap.c index ce50954f2f32..bb2acf193d32 100755 --- a/astropy/wcs/src/wcslib_wrap.c +++ b/astropy/wcs/src/wcslib_wrap.c @@ -1500,10 +1500,18 @@ PyWcsprm_mix( goto exit; } - /* Convert pixel coordinates to 1-based */ + /* Force a call to wcsset via PyWcsprm_cset before entering the parallel + * region (see the matching note in Wcs_all_pix2world). This ensures + * wcs->flag == WCSSET so that wcsmix below does not invoke wcsset + * itself, allowing the wcsprm_python2c / wcsprm_c2python round-trip + * to be dropped. */ + if (PyWcsprm_cset(self, 1)) { + goto exit; + } + + /* Convert pixel coordinates to 1-based. */ Py_BEGIN_ALLOW_THREADS preoffset_array(pixcrd, origin); - wcsprm_python2c(&self->x); status = wcsmix( &self->x, mixpix, @@ -1516,7 +1524,6 @@ PyWcsprm_mix( (double*)PyArray_DATA(theta), (double*)PyArray_DATA(imgcrd), (double*)PyArray_DATA(pixcrd)); - wcsprm_c2python(&self->x); unoffset_array(pixcrd, origin); unoffset_array(imgcrd, origin); Py_END_ALLOW_THREADS @@ -1632,18 +1639,29 @@ PyWcsprm_p2s( goto exit; } - // Here we force a call to wcsset. Normally, WCSLIB will call wcsset automatically when - // calling wcsp2s, but we need to call it ourselves using PyWcsprm_cset so that we can - // catch cases where the units might change if e.g. they are not in SI to start with. - /* Force a call to wcsset here*/ - if (self->preserve_units && PyWcsprm_cset(self, 1)) { + // Force a call to wcsset via PyWcsprm_cset before entering the parallel + // region (see the matching note in Wcs_all_pix2world). This ensures + // wcs->flag == WCSSET so that wcsp2s below does not invoke wcsset + // itself -- with wcsset moved out of the parallel region, wcsp2s is + // read-only on the wcsprm struct, and the wcsprm_python2c / + // wcsprm_c2python round-trip can be dropped. + if (PyWcsprm_cset(self, 1)) { return NULL; } - /* Make the call */ + /* Make the call. + * + * After wcsset has run (now hoisted into PyWcsprm_cset above), wcsp2s is + * read-only on the wcsprm struct: it consumes the precomputed sub-structs + * wcs->lin / wcs->cel / wcs->spc plus wcs->crval[i], and never re-reads + * the raw arrays (cd, cdelt, crpix, crota, obsgeo, mjdobs, ...) that the + * wcsprm_python2c / wcsprm_c2python pair was rewriting NaN <-> UNDEFINED + * in place. The round-trip can therefore be dropped here without + * changing the transform's output, and concurrent threads no longer + * race on those raw arrays. + */ Py_BEGIN_ALLOW_THREADS preoffset_array(pixcrd, origin); - wcsprm_python2c(&self->x); status = wcsp2s( &self->x, ncoord, @@ -1654,7 +1672,6 @@ PyWcsprm_p2s( (double*)PyArray_DATA(theta), (double*)PyArray_DATA(world), (int*)PyArray_DATA(stat)); - wcsprm_c2python(&self->x); unoffset_array(pixcrd, origin); /* unoffset_array(world, origin); */ unoffset_array(imgcrd, origin); @@ -1762,12 +1779,13 @@ PyWcsprm_s2p( goto exit; } - // Here we force a call to wcsset. Normally, WCSLIB will call wcsset automatically when - // calling wcsp2s, but we need to call it ourselves using PyWcsprm_cset so that we can - // catch cases where the units might change if e.g. they are not in SI to start with. - /* Force a call to wcsset here*/ + // Force a call to wcsset via PyWcsprm_cset before entering the parallel + // region (see the matching note in Wcs_all_pix2world). This ensures + // wcs->flag == WCSSET so that wcss2p below does not invoke wcsset + // itself, allowing the wcsprm_python2c / wcsprm_c2python round-trip to + // be dropped. - if (self->preserve_units && PyWcsprm_cset(self, 1)) { + if (PyWcsprm_cset(self, 1)) { return NULL; } @@ -1829,10 +1847,13 @@ PyWcsprm_s2p( goto exit; } - /* Make the call */ + /* Make the call. See the comment in Wcsprm_p2s for the rationale -- + * wcss2p is read-only on the wcsprm struct after wcsset has run, so + * the wcsprm_python2c / wcsprm_c2python round-trip can be dropped and + * concurrent transforms no longer race on the raw arrays + *. */ Py_BEGIN_ALLOW_THREADS /* preoffset_array(world, origin); */ - wcsprm_python2c(&self->x); status = wcss2p( &self->x, ncoord, @@ -1843,7 +1864,6 @@ PyWcsprm_s2p( (double*)PyArray_DATA(imgcrd), (double*)PyArray_DATA(pixcrd), (int*)PyArray_DATA(stat)); - wcsprm_c2python(&self->x); /* unoffset_array(world, origin); */ unoffset_array(pixcrd, origin); unoffset_array(imgcrd, origin); @@ -1911,6 +1931,26 @@ PyWcsprm_cset( return 0; } +#ifdef Py_GIL_DISABLED + // On a free-threaded build the GIL no longer serialises concurrent callers + // of PyWcsprm_cset, so the wcsenq guard alone cannot prevent two threads from + // running wcsset simultaneously on the same struct (which is not thread + // safe). A single process-wide PyMutex around the wcsset path is enough: + // the fast path (wcsenq-succeeds) above is lock-free, so contention only + // happens the first time a WCS is set or after a user mutation, which is + // rare. On a GIL build this macro is undefined and the lock is compiled + // out -- the GIL itself serialises us since PyWcsprm_cset never releases it. + static PyMutex wcsset_mutex; + PyMutex_Lock(&wcsset_mutex); + // Double-checked locking: re-check under the lock so that if two threads + // both missed the fast path above, only the first one actually runs + // wcsset and the second returns immediately. + if (wcsenq(&self->x, WCSENQ_CHK)) { + PyMutex_Unlock(&wcsset_mutex); + return 0; + } +#endif + initialize_preserve_units(self); if (convert) wcsprm_python2c(&self->x); @@ -1919,6 +1959,10 @@ PyWcsprm_cset( check_unit_changes(self); +#ifdef Py_GIL_DISABLED + PyMutex_Unlock(&wcsset_mutex); +#endif + if (status == 0) { return 0; } else { @@ -2378,7 +2422,7 @@ PyWcsprm_to_header( // If the user has requested to preserve the original units, then we // could try and edit the header returned by wcshdo - however, this // might be tricky and not robust to future WCSLIB changes. Instead, - // we make a copy of the Wcsprm object, change the units on that and + // we make a copy of the PyWcsprm object, change the units on that and // prevent WCSLIB fixing the units, then convert to a header. if (self->unit_scaling != NULL) { diff --git a/docs/changes/wcs/19819.bugfix.rst b/docs/changes/wcs/19819.bugfix.rst new file mode 100644 index 000000000000..0e57b87b18da --- /dev/null +++ b/docs/changes/wcs/19819.bugfix.rst @@ -0,0 +1,3 @@ +Made the WCS coordinate transform paths (``pixel_to_world``, ``world_to_pixel``, +``all_pix2world``, ``wcs_pix2world``, ``wcs_world2pix``, ``mix``) safe to call +concurrently on a shared ``WCS`` instance from multiple threads. From 55f15883b9678e338c2bcdd2ac3b06f4256b8b1a Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Fri, 29 May 2026 13:17:26 -0400 Subject: [PATCH 165/199] Backport PR #19829: Fix: unexpected behavior in Cutout2D.plot_on_original --- astropy/nddata/tests/test_utils.py | 36 ++++++++++++++++++++++++++++ astropy/nddata/utils.py | 14 ++++++++--- docs/changes/nddata/19829.bugfix.rst | 1 + 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 docs/changes/nddata/19829.bugfix.rst diff --git a/astropy/nddata/tests/test_utils.py b/astropy/nddata/tests/test_utils.py index b0a1d3319468..dd9430dba08b 100644 --- a/astropy/nddata/tests/test_utils.py +++ b/astropy/nddata/tests/test_utils.py @@ -19,6 +19,7 @@ subpixel_indices, ) from astropy.tests.helper import assert_quantity_allclose +from astropy.utils.compat.optional_deps import HAS_MATPLOTLIB from astropy.wcs import WCS, Sip from astropy.wcs.utils import proj_plane_pixel_area @@ -700,6 +701,41 @@ def test_crpix_maps_to_crval(self): w.all_pix2world(*w.wcs.crpix, 1), w.wcs.crval, rtol=0.0, atol=1e-6 * pscale ) + @pytest.mark.skipif(not HAS_MATPLOTLIB, reason="requires matplotlib") + @pytest.mark.parametrize( + "position, size", + [ + ((2, 2), (4, 4)), # even-sized cutout + ((2, 2), (3, 3)), # odd-sized cutout + ((2.1, 1.9), (3, 5)), # non-integer position, asymmetric size + ], + ) + def test_plot_on_original(self, position, size): + """ + Regression test to ensure plot_on_original draws a rectangle + that matches the actual pixel extent of the cutout. + """ + import matplotlib.pyplot as plt + + cutout = Cutout2D(self.data, position, size) + + fig, ax = plt.subplots() + cutout.plot_on_original(ax=ax) + patch = ax.patches[0] + + # The rectangle should span from (xmin - 0.5) to (xmax + 0.5) + # and (ymin - 0.5) to (ymax + 0.5) in pixel coordinates. + x0, y0 = patch.get_xy() + width = patch.get_width() + height = patch.get_height() + + assert_allclose(x0, cutout.xmin_original - 0.5) + assert_allclose(y0, cutout.ymin_original - 0.5) + assert_allclose(width, cutout.xmax_original - cutout.xmin_original + 1) + assert_allclose(height, cutout.ymax_original - cutout.ymin_original + 1) + + plt.close(fig) + def test_cutout_with_nddata_as_input(self): # This is essentially a copy/paste of test_skycoord with the # input a ccd with wcs attribute instead of passing the diff --git a/astropy/nddata/utils.py b/astropy/nddata/utils.py index cbc69b192a2d..84715cbd842c 100644 --- a/astropy/nddata/utils.py +++ b/astropy/nddata/utils.py @@ -785,11 +785,19 @@ def plot_on_original(self, ax=None, fill=False, **kwargs): if ax is None: ax = plt.gca() - height, width = self.shape - hw, hh = width / 2.0, height / 2.0 - pos_xy = self.position_original - np.array([hw, hh]) + xmin = self.xmin_original - 0.5 + xmax = self.xmax_original + 0.5 + + ymin = self.ymin_original - 0.5 + ymax = self.ymax_original + 0.5 + + pos_xy = (xmin, ymin) + width = xmax - xmin + height = ymax - ymin + patch = mpatches.Rectangle(pos_xy, width, height, angle=0.0, **kwargs) ax.add_patch(patch) + return ax @staticmethod diff --git a/docs/changes/nddata/19829.bugfix.rst b/docs/changes/nddata/19829.bugfix.rst new file mode 100644 index 000000000000..4abf07fbe9f8 --- /dev/null +++ b/docs/changes/nddata/19829.bugfix.rst @@ -0,0 +1 @@ +Fix unexpected behavior in ``Cutout2D.plot_on_original()``. The displayed region spanned pixels not included in the cutout array. From 49c8bceb5112a1cc4b23e794b65e46e0fbde0d8f Mon Sep 17 00:00:00 2001 From: Om S Habib Date: Mon, 1 Jun 2026 20:52:20 +0530 Subject: [PATCH 166/199] Backport PR #19307: coords: add comprehensive docstring to CIRS frame (fixes #18981) --- astropy/coordinates/builtin_frames/cirs.py | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/astropy/coordinates/builtin_frames/cirs.py b/astropy/coordinates/builtin_frames/cirs.py index 9df0eb0ae9c2..598318a64b8e 100644 --- a/astropy/coordinates/builtin_frames/cirs.py +++ b/astropy/coordinates/builtin_frames/cirs.py @@ -29,6 +29,60 @@ class CIRS(BaseRADecFrame): """ A coordinate or frame in the Celestial Intermediate Reference System (CIRS). + CIRS is a geocentric reference system that serves as the *intermediate* + step between the quasi-inertial `~astropy.coordinates.ICRS` (fixed to + distant quasars) and the Earth-fixed `~astropy.coordinates.ITRS`. It was + introduced by the International Astronomical Union (IAU) 2000 and 2006 + resolutions to replace the older equinox-based "apparent place" systems + with a cleaner, higher-precision framework. + + **Definition** + + CIRS is defined by two fundamental concepts: + + * **Celestial Intermediate Pole (CIP)** — the pole of the system. Its + motion in the `~astropy.coordinates.GCRS` is described by the IAU 2006 + precession model combined with the IAU 2000A nutation model, giving + micro-arcsecond accuracy. + + * **Celestial Intermediate Origin (CIO)** — the origin of right ascension + in CIRS. Unlike the classical vernal equinox (which is defined by the + intersection of the Earth's equator and the ecliptic and therefore + "swings" with precession and nutation), the CIO is a *non-rotating + origin*: it is defined so that it has no rotation component around the + CIP. This cleanly separates the Earth's spin (described by the + **Earth Rotation Angle**, ERA) from its wobble (precession and nutation). + + **Relation to other frames** + + * **ICRS → CIRS**: applying the IAU precession-nutation model rotates the + ICRS axes to the intermediate (CIP/CIO) axes. Astropy performs this + via the ERFA ``apco``/``atciqz`` routines. + + * **CIRS → ITRS**: a single rotation by the Earth Rotation Angle (ERA) + around the CIP axis. ERA is a linear function of UT1 and replaces the + more complex "equation of the equinoxes" needed in the older + equinox-based systems. + + * **CIRS vs. GCRS**: both share the same pole (CIP) and the same + geocentric origin, but they differ in their right-ascension origin. + GCRS uses the equinox; CIRS uses the CIO. The angular offset between + the two origins along the intermediate equator is called the + *equation of the origins* (EO). + + **Astropy implementation** + + Astropy implements CIRS using the `ERFA + `_ library (the open-source version of + IAU SOFA), which provides the IAU 2006/2000A precession-nutation model. + The default ``location`` is the geocentre; supply an + `~astropy.coordinates.EarthLocation` to get the topocentric variant + (which additionally accounts for diurnal aberration). + + For more background see Section 2.6 of `USNO Circular 179 + `_ and the references in the + :ref:`astropy:astropy-coordinates-seealso` section of the documentation. + The frame attributes are listed under **Other Parameters**. """ From 1c3f396d93e976b0d4d8ecc78885594167a15823 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:26:57 -0400 Subject: [PATCH 167/199] Backport PR #19057: Improved placement of WCSAxes tick labels for non-perpendicular grids or ticks --- .../figures/py311-test-image-mpl380-cov.json | 66 ++++---- .../figures/py311-test-image-mpldev-cov.json | 48 +++--- .../wcsaxes/coordinate_helpers.py | 39 +++-- .../visualization/wcsaxes/tests/test_misc.py | 2 +- astropy/visualization/wcsaxes/ticklabels.py | 143 ++++++++---------- docs/changes/visualization/19057.bugfix.rst | 1 + 6 files changed, 144 insertions(+), 155 deletions(-) create mode 100644 docs/changes/visualization/19057.bugfix.rst diff --git a/astropy/tests/figures/py311-test-image-mpl380-cov.json b/astropy/tests/figures/py311-test-image-mpl380-cov.json index 2fccead9d1fa..d46b5bc1a7aa 100644 --- a/astropy/tests/figures/py311-test-image-mpl380-cov.json +++ b/astropy/tests/figures/py311-test-image-mpl380-cov.json @@ -1,9 +1,9 @@ { - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "cceb87beabe25ac4187b2ade40b1d4af551dbb77a35f71b59d41cbfb85a2c769", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "ed4d134ec6599ed1343f94c7099ab5d4d56ace2c950507017e597bc74c84d208", "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_rectangular": "30e13643c770a26b2707745143f50a735daa6f37a6a8258e733ae35338a4a1bb", - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_nonrectangular": "37de34740cef2897effc9de5e6726ef3955a3449d0fa6a929350301500557b8e", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_nonrectangular": "c13d765eb65803590b89bb71adfc08065bb74e3db4eb2ccad62a18b14a83ad20", "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_change_wcs": "c68e961f0a21cc0dcc43c523cee1c564d94bd96c795f976d51e5198fc52f83cc", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tight_layout": "80f167d1d14c8c22f1ffc2f403951e60397a4dcf35b9ecfd3ebba297d3fe0ff7", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tight_layout": "b07a6595053b6931cb70f23d18a27897fd9fa48af173bb87f5f4f7675d560d48", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_image_plot": "4fe4c89d089e8f1f9584584826f25d3c884d4914f76a863e1a624f9ef2295b07", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axes_off": "3580258468fc0ff0fa6d140299b79f1f05c16c3e222447de38228e01983fbdd1", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[True]": "5364e39cab14f28b08073b31cce3dae8501c0431b1a49985810b764902747c8e", @@ -12,33 +12,33 @@ "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contour_overlay": "8679305e8a8b5b3f5b896343f4e16dfc65e9289c31505e3b35ef773d7837c8ea", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contourf_overlay": "fa928ab57291e8a128009320323ffb8955cd75d28b2a589074defb7b0889b3eb", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_overlay_features_image": "30d5562cb7a2484db0b898bc886253829de884fbf92bc0e6575f091438ec5f32", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_curvilinear_grid_patches_image": "ad0a985a324a73b5bea839a231f7537075306c874c279c6a6ea2cfe16529d815", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image": "822d96a93eb4ab59b1a0095384bb315b025b481a18821b3c558e4a937c77f489", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image_lonlat": "224f7e2f7d106ab028584bdc51c73296e277a211b7f3ed36a0452eb4842afa5b", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_coord": "9352d254af8add2918f98fb8a8d6d220bb95ab8096a1c56584d6c80ff3d5de39", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scatter_coord": "9352d254af8add2918f98fb8a8d6d220bb95ab8096a1c56584d6c80ff3d5de39", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_text_coord": "c6e7575becb6381f45fe549ea7415bd038778406e43b44e8a72f8c6bce82e466", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_line": "74ce73bcbb1dd170ae36d38a1ad4a62c626517a84e05922dab0f989a12a27758", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_changed_axis_units": "056ec7123c67fa7ee6269bec47641bfe4932630a79d02029e71164b522fadacd", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_curvilinear_grid_patches_image": "3be3f50e37e40b67e482ac6a418c95209d3d74db472a6960571c77d9ca3a4651", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image": "a0d13538d6c6aad4a44d86c094c62f2c74a669050815332a1d2e547dfeab84ef", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image_lonlat": "a137066f5472305b53cf69e9fa32dd452bba9e7d28d6ce3b9c8d668e85ea6ca9", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_coord": "357c431732a20f850aeb7f395344a787b3828de0c0874e7cc67c36585ebc26cb", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scatter_coord": "357c431732a20f850aeb7f395344a787b3828de0c0874e7cc67c36585ebc26cb", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_text_coord": "c0c6b1153494509df8a8fb9a041b0749cc0143b2381263b1409969d0a3187f48", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_line": "519133d4b90a83b602b059d8ce6df525236abdad347bb68c1820d67edf8e182a", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_changed_axis_units": "8692bdad0bab9889f22cc725c288c30f659082b537bb42d87748f76b76b706ad", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_minor_ticks": "5224f8fa725901ff74a4558987ceaae0363f98894d2c6a12d8c557db500ff7fe", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_labels": "f4944296c37a6e8ea9d30126709da3567a25908c1665e8cad10e894dd87cd54d", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_labels": "98f5d686d52be75b6a8e114a9d0d0deb5db32b486e50dff70f618e54806e9d75", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_no_ticks": "5836f6d36ce6ce89156b251055e5b00b6063a35bff4b7ca1b2da725da14a67ad", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_rcparams": "1af0c66ba343df4f03efc18e707f5b0c2a54dbce7351e0f9024154937a30b371", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_rcparams": "8fde44ec7f3b5190219c0712bb864b41c261c764ca90713ce558b1f17a2d3435", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles": "11ef218080920301ada1ef9cea558599ca110ba49e3dc4e9dc547c013b87fcc7", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles_non_square_axes": "2ffe1157db233bc80e0eda08a30b916cc56a8ff0a7f34c3a0a1b1037695fe1a5", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_set_coord_type": "ee5e873fd467292f7e76f0890051f33762b399fa3936357797e7338f80e914eb", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles_non_square_axes": "5e4275c37e0d70fd04fb763b31bdc0512ebc4c89fe4016a4e5c672b23c0c7eda", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_set_coord_type": "b14d974eb1b498cd1bfd1adf6f425b855d1df67241c28ebc2b57785f2748cca0", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_regression": "d295b08d88c6f8cd0eb56f9aa9c1892b6e72e6819a0f5b6265f3a29acb0a1545", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axislabels_regression": "431b65a14f107dd47c3ea56240b92e246ba25be682f6487c8e8565b78f1fc0c8", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_noncelestial_angular": "cafe2fe3ca40e358ad8e92040c698c1e7f2913968f226a12b635fc47c0acc063", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axislabels_regression": "d91389c6b91df92f26326648bfd63e0e970c9eea582797d870fff926e98cf126", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_noncelestial_angular": "a4b11994128d6a31a224019e46a3ea9b513d8667714ebc4e19081eecf86c6fb3", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_patches_distortion": "704af668b56837628f11b3dedbba60c0d11fda2e896ddbba9dee0e8acb5b99d7", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_quadrangle": "0223dba7d207f37c644c5c0860f6ef18b11b8013438030c89d88d700ea2ed437", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_args": "5bb6436777ab08013e747807f7db670b068229d6fb2b6b250787e44eaac36dab", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_header": "d7309b91db792bd42fdccd53ebe0738611c7cec56adb2972f0d684d83c8c75f6", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scalebar": "29931954f6c7c5131a63bdfabbe4c220ee255d2efde756e31977d13a3c69af98", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_elliptical_frame": "3aee6e8bcb0b1283993d6743683f13077aab179a85540e3dcb301fef8c66aaf7", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_hms_labels": "1047e944e3cb798a39702be44c9612cb27a26e4fd246a5cf0abe047a51985d5c", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_latex_labels": "22c900ff73a60d58fbc29ee61485cad76aa483e3c8efa89bffbdf6d660d0bf9f", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_params": "78bdce5072b3e9ac87b7e76832f3fc889c115aef3f894004a50b7eeba0d32ebd", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_args": "71af38927a3d76c39833266f715d0beebcde62427e16c567a010b261b36893c8", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_header": "1b91ff2ca8ba76447ca510f511a190e163ee3602563dd27c675f38a5e2cf8c12", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scalebar": "38e1c901e5b825d36a5a260b24dc5265d4204d41965fe381fef4daa974884e0b", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_elliptical_frame": "332e90410ca2a5b0a4151c325f1878dd800fe3e400fbb14af3a80fd8c3895ee2", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_hms_labels": "c0ef16030e9df22b539db367637d851a1abcfca559f126092a9ddb4210bf3d73", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_latex_labels": "8d30459ca3b04d3135fb0dbe0cefd6d992abdb7187a347d424d57d13090706e4", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_params": "80bbcaf1a84e45a6dda31f4dd94055f5ddb4def32481f057d1fb8a2d269907b7", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs": "190a14699654f7eae01556013f4fee1bbcddd3b34e62e9e3d92a92f7ce9a7695", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs_format_unit": "e0eea94f0ca0e37e96d903c3f441ecf0eb20b6870bfbd8a3426ad3e8305d1cee", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs_get_transform": "73e65438281efd42cda9e459467b18f7695aa50589782d0ce0cfbd298b69cb14", @@ -47,16 +47,16 @@ "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_sliced_low_level_wcs[slices1-custom:pos.helioprojective.lat]": "0a14e5153eb91e2a3a96501f02a21581effc7d6b7ae31e29a402a3abf30d5b12", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices0-hpln]": "60c967eef03cc2f038d7915f1b988dfa60852fa7b950b83c6d3f49054862d5d1", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices1-hplt]": "0a14e5153eb91e2a3a96501f02a21581effc7d6b7ae31e29a402a3abf30d5b12", - "astropy.visualization.wcsaxes.tests.test_images.test_allsky_labels_wrap": "8908818b526d4a506505da890eff47121b23ee41656dc15dff3e19f7d03094af", - "astropy.visualization.wcsaxes.tests.test_images.test_tickable_gridlines": "9d91bcf571af6dcc1425b6dfd76fb92e3e0180fb9fd0c852f9eba3fb06e079fc", - "astropy.visualization.wcsaxes.tests.test_images.test_overlay_nondegree_unit": "aa9b85520da54dc61d4db2988883ff184fb59cd27c52a04899cb33599fcab4b7", - "astropy.visualization.wcsaxes.tests.test_images.test_nosimplify": "565b8bf068323147ae0fcc72263f258c552285aadcf2fc67786818fccc0b812d", - "astropy.visualization.wcsaxes.tests.test_images.test_custom_formatter": "e04eb07365c6bbc4774a20150c13dadcf72b9c5a664805aff2fffd106fdecd87", + "astropy.visualization.wcsaxes.tests.test_images.test_allsky_labels_wrap": "c518cddf11b21a622367b676afaebfb42093932ca46a9287f8c59e4c373234f2", + "astropy.visualization.wcsaxes.tests.test_images.test_tickable_gridlines": "65127c0f9e786105f1219c576f361f88d5987f4d0d6fb5f555a16a5417c570f3", + "astropy.visualization.wcsaxes.tests.test_images.test_overlay_nondegree_unit": "24b94479a00308811f35e9572c9796adf7f24b993e5468e086f39b3dbd20f079", + "astropy.visualization.wcsaxes.tests.test_images.test_nosimplify": "7691427db965ad2884232cb1017cb2ad703f2bcb172064b1cb5a6db6f3562756", + "astropy.visualization.wcsaxes.tests.test_images.test_custom_formatter": "e5e336f23352c6e75cca8a13ef932b15e6f9f4b097734588797495754e21cceb", "astropy.visualization.wcsaxes.tests.test_images.test_equatorial_arcsec": "2e2ad683a3acdda19dd4353654fa6d399f4b5cd528cd41591a7bcba8776a67ce", "astropy.visualization.wcsaxes.tests.test_images.test_wcs_preserve_units": "464a0503099d3f7381479ddbba0e417011adb6e24301692056b124d608b1067b", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay": "0a87473ff8e5b5610f4adac1518c608afcd2178b25584fb42f77bcc594c4f47a", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay_auto_coord_meta": "2f737bb70fb1a5452cb0efa79010376614dc559e9aff607f638f044ac6b04448", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_direct_init": "1f24c5243bfdf0f30e88afc4f2f5d66db85954cea50a2910af94975ff8765b45", - "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_5d_with_names": "c90ae6f3b0f9f407ca7a866fa648dc3ef4bea78369315292bc82f16104ed5736", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay": "94d53bee535a009789e31f71443158c765f4401031b75bbbe3935edbad5a39d6", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay_auto_coord_meta": "d4a0f6a8bb05133a9fcc325fd2bc3d52e1a93185ed3591dfc900828215dd1f90", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_direct_init": "ac5f1e7e04fad78d519d0558009a79fa0ef369c328babbf921467b5b10d4ee43", + "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_5d_with_names": "57adce0a9e991840cfd70e8cacfc63e547c4186994c9d2f63b0711a9bbaa0d34", "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_2d_celestial_arcsec": "4b743d645a85d7516decbcf4a831c127af5a1800072597c2a1299d17fb186adb" } diff --git a/astropy/tests/figures/py311-test-image-mpldev-cov.json b/astropy/tests/figures/py311-test-image-mpldev-cov.json index 5c5bfba140b9..dc22576c7536 100644 --- a/astropy/tests/figures/py311-test-image-mpldev-cov.json +++ b/astropy/tests/figures/py311-test-image-mpldev-cov.json @@ -1,7 +1,7 @@ { - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "5ba2b23bc4107e33eb9ffcfb656dc5b487969443f847497c0a9ab6ad6c4624bf", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_custom_frame": "7df863a91ce010794cdee1f71d8dc22370f4713a93e944f32884b3346f2dbcd6", "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_rectangular": "45669dd577a0750602555da53c590da0b70a64d899e38a6b3be1c8c0c393dc52", - "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_nonrectangular": "38145a9457711720f12888ba62aeb28ac06f0c05425856af415949e49ecc0b97", + "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_nonrectangular": "378e4585a4ec759b4df60b5589859d8988205191463c8475d34fdf4f3f602b44", "astropy.visualization.wcsaxes.tests.test_frame.TestFrame.test_update_clip_path_change_wcs": "6a3b3e21d12fb83e24c3307c24fe300e520db4b158d407f6df7fc79dfa2210cf", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tight_layout": "5bd7b10da254a8cee16b4d2304faf3a9d876f9f512f2778f1326aec2d9c10371", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_image_plot": "a10da5ff6a071bb278b51fe811887e978773cf9f69717eeae9ded395e37bccfd", @@ -9,23 +9,23 @@ "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[True]": "3492f9ddb93014073086269f862aa7c6183da0c28a3a98afd330aa9e0f27cf09", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[False]": "8019d129a842be8d6c91543790f1e7b1933b2b91edfa7885a0d71b0ca8ef00d7", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axisbelow[line]": "75516135332695601d2fe25381c360b03ebf5526c9376bdeab0e618069b1516a", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contour_overlay": "45eee6ea3948c70ffd6e8758423dea0c90a83bd525420f34687f7981b451e911", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contourf_overlay": "44d57ebdd3216650be39b019729b532a88ceb95f83f1f8dc664377f4f64a631e", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contour_overlay": "c3d92fbc94e9396fd814e8ed4e38996558ee90d9adaaaf5d73c831d60d81574b", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_contourf_overlay": "19a5b2448716cd74fcd36a3c171d596507aa63631e0187b55b6421cbcbf2f680", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_overlay_features_image": "bf160601fe1e0bf8996f71c2907422cffaa468eb2755f4fd065dd2b5694af848", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_curvilinear_grid_patches_image": "3af87106f1b0fcf6070d33b748557d897be80531ab39192b227c82a409bef835", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image": "bd4013e426adde9461a46b2c88b26020fb198bb14d7b4835e4cdeed8ca185678", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image_lonlat": "eff17af9665669826c8924cb0686f9ef313bdad8a477e12622d8d7d07422efb3", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_coord": "6d5cf6dae41a88239870282530c60f771f6369b600856c81b4991610821c3b3b", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scatter_coord": "6d5cf6dae41a88239870282530c60f771f6369b600856c81b4991610821c3b3b", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_text_coord": "5ddca4e5ee5a86b6cb29588e5826ab271268f9502be440af5852bf0aa2724d2a", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_line": "2bae09e3199778dcbea8f058e200a245367f7b70072b285cd7fb82635fcd7482", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_curvilinear_grid_patches_image": "b8375ec9b69cbe58d8cbf557244d26ee8c029cecbc3f2f9b674b68825277d1f6", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image": "6b26921c7898609cfa86ff59e2184c7e0dd5133960061042c554853fc61cee5f", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_cube_slice_image_lonlat": "398a19ac15f5f822ca6e4078b98a39f231adb808cbdff70567083e06db766751", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_coord": "8f8189f785348caaf8c109adc581ece3d071602057d49d99a7ad896bb578e333", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scatter_coord": "8f8189f785348caaf8c109adc581ece3d071602057d49d99a7ad896bb578e333", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_text_coord": "c07febd6a45e52b461435c5dff7f6db9d11662e6d7ffb0950c40846a6f747137", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_plot_line": "c003af1cd835f1fd7173aacfdc591645661da47d00e6c6992459785313a4f6d5", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_changed_axis_units": "c8b0ac1c48f9beba0261caea8aae466574c296ea088f4439e54497f04f7940f5", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_minor_ticks": "c314eb4ef0f20621c2fc62ba09d0d71afe67baafbdb289d1b79308ea6164291d", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_labels": "e1cefee39eedc7320e17e64e2c4e711f574c833c9f0155aa431944aa2f9ca899", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_labels": "a8f77b5ebea7735027ddc085bb5d6d74476093358cfea4e9bfdfc290ed8eff18", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_no_ticks": "7ac52e09ffde02327bf01d233fd7947a74a1a36e9ec273f94ffd04a5d3f699d5", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_rcparams": "132410d077bf5d6ff682f285961b4a3b8b6750c344ffd4660f1fb176aa424680", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles": "11ef218080920301ada1ef9cea558599ca110ba49e3dc4e9dc547c013b87fcc7", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles_non_square_axes": "19b6034dc4f1e350efe12eb2e5a6add1bf47ffe37d79bd05f1d15cff684af365", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_angles_non_square_axes": "8717c72274f25d42846ea8296a5b346ce6c467fc7ea194a9d1f5e208b0ea97f2", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_set_coord_type": "c66c6870631760871da3d722cf4739f2e542f6d10937f5c1e87054ea67121ae4", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_ticks_regression": "e1cbb0d489ecc3ca72258e87bbf0006bbc1bd2979df464cb61c68f5f38615987", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_axislabels_regression": "72598fe74fb8fc1d0ab83df6ef4886accd08c8b8f47938408fa9327c8a37e2eb", @@ -35,9 +35,9 @@ "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_args": "01dffa3f79ddbfa5c119a49fc1282fee63f2792ccebc189ad44ab644b59f1559", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_beam_shape_from_header": "c4b34fab02c5be0d9d69b2615a1b05c3b73ddf9e40031c6873abe26ab3090472", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_scalebar": "4b5c3fc70a8bbd7aaeed0d15d78bc491c8253ca2cc7d239c343306e0a71f44c5", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_elliptical_frame": "3cf0c18fa653fafaaba2287b4a8c3e125eb29c2ba6a120918d0e088cbc8467d2", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_hms_labels": "507fb9485ce61050323d021347ce43acd1929f44249cfc98bd684ce685b4315b", - "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_latex_labels": "66a902d8e9ed0f6e95a5433d4842ede2163ff11387804bf93ed810632e1cd373", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_elliptical_frame": "1524cbcdd819e04638a891660069467f9cc31e6f1430b525f3fee93992a636d0", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_hms_labels": "97e91caed53309d31e24cffad39c6a256739fd35123436f142092c37a99cfd26", + "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_latex_labels": "b08ef30472956763adafaf4d50a250d04f0cd2a47f96fcbb55528bfd1e2c80ef", "astropy.visualization.wcsaxes.tests.test_images.TestBasic.test_tick_params": "b197947dedc38821e02f31edb4e259a2d87439470947aa5c53bdfc89faa9d9b3", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs": "bf85a4cc85935869d4c26a39d155d90f68a9d266adcf2bac0d10b5da384d7045", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_wcs_format_unit": "f632e2eddb9d683a085558339c23995049a7463644d6d6f997d923f5c6aaaa42", @@ -47,16 +47,16 @@ "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_1d_sliced_low_level_wcs[slices1-custom:pos.helioprojective.lat]": "03708033709fe9b63375f96fbf5ea44b4e17cae968d385f5899d4b2e42be1f6c", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices0-hpln]": "24af7845ef230f2be4949377f8d2a8e7b3bccd2622d8f488442ed09fc07b2b5d", "astropy.visualization.wcsaxes.tests.test_images.test_1d_plot_put_varying_axis_on_bottom_lon[slices1-hplt]": "03708033709fe9b63375f96fbf5ea44b4e17cae968d385f5899d4b2e42be1f6c", - "astropy.visualization.wcsaxes.tests.test_images.test_allsky_labels_wrap": "4697fefe84a8b0e5b58cc35bc250921ec3fffc7cbdee85836427af90fe4ac475", - "astropy.visualization.wcsaxes.tests.test_images.test_tickable_gridlines": "5ce7b377d7cf15d3be97a922929536413a36fc2c1a34423ecee8f04d361d35fd", - "astropy.visualization.wcsaxes.tests.test_images.test_overlay_nondegree_unit": "4f5819ad0dc6cc6c5a4776265f114f81b192bfd11ce1ddbb3d8b43da6b56e642", - "astropy.visualization.wcsaxes.tests.test_images.test_nosimplify": "336423447a5066d39a60ac4b0839dbe8eb2fab6a742830a71464f941c6307c66", - "astropy.visualization.wcsaxes.tests.test_images.test_custom_formatter": "1142e72415e9affaa97c7e763d1274632fe1d9cf8a9eae2a579a179fd14d6b9d", + "astropy.visualization.wcsaxes.tests.test_images.test_allsky_labels_wrap": "cc83d998e367896cd7f9c112ad0bfc61dbd83297df19b76d5822f70deaeffe52", + "astropy.visualization.wcsaxes.tests.test_images.test_tickable_gridlines": "317db2a0b2c5b69abbd015b23a929a3cfc954481167dc2ad95a59ccd7daa656b", + "astropy.visualization.wcsaxes.tests.test_images.test_overlay_nondegree_unit": "f4908df4bea70a091d8145296d4cd06bbdd45726b010315bdd8780cac9d07ad6", + "astropy.visualization.wcsaxes.tests.test_images.test_nosimplify": "3b7ef11648af0e54623ec891a498f03908031119aa4b0061d6d3f342bb8d117a", + "astropy.visualization.wcsaxes.tests.test_images.test_custom_formatter": "01b81670f49bec5d721a4d35ad1705ff1b08397ce7c04f4c840cbdd200615529", "astropy.visualization.wcsaxes.tests.test_images.test_equatorial_arcsec": "f048285e01f99235e711068e06ea5729b1ff5258604a944a5fa26042e613dae6", "astropy.visualization.wcsaxes.tests.test_images.test_wcs_preserve_units": "4111f2731c268bbe8ff57551b011afd6dd918efe07236507625f6e99e69b7263", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay": "f7635158075969632bc3cabd6cfa9798a3d4227f173aed32a546c540c65d9b4d", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay_auto_coord_meta": "9a3cc14c87a8349487888c69bab345748894be4295598eaed2475804f97b5f8a", - "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_direct_init": "021364576662b07f1eedf58251d657244fd1b67bb16ec4c99a0506b1dde0d66e", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay": "fc11a76eb5f492e78908056cfc952b230866d5eb926ef7c51caea3fd873a77c2", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_coords_overlay_auto_coord_meta": "34946e9b2a0e99223ec06f20c6df26db1716ea01c0495cf7ba519db78f5d0d41", + "astropy.visualization.wcsaxes.tests.test_transform_coord_meta.TestTransformCoordMeta.test_direct_init": "bd7af8c8ba136338e52619e594e7a926ed215b0d192429b2aa1a5654aef1449a", "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_5d_with_names": "b243aec60020235d04c816743c0fcc643495f58d4d4d5086588c59628d9cea76", "astropy.visualization.wcsaxes.tests.test_wcsapi.test_wcsapi_2d_celestial_arcsec": "ecb11a595e1352b3b8e6967494fa5eac63992fc878bf15a934329bb28aeb61d6" } diff --git a/astropy/visualization/wcsaxes/coordinate_helpers.py b/astropy/visualization/wcsaxes/coordinate_helpers.py index 3f490a8fc835..629245065ae5 100644 --- a/astropy/visualization/wcsaxes/coordinate_helpers.py +++ b/astropy/visualization/wcsaxes/coordinate_helpers.py @@ -1019,23 +1019,35 @@ def _update_ticks(self): continue axes0 = transData.transform(pixel0) - # Advance 2 pixels in figure coordinates - pixel1 = axes0.copy() - pixel1[:, 0] += 2.0 - pixel1 = invertedTransLimits.transform(pixel1) - with np.errstate(invalid="ignore"): - world1 = self.transform.transform(pixel1)[:, self.coord_index] + # Define a helper function to minimize code repetition + def shifted_pixel_to_world(index, shift): + pixel = axes0.copy() + pixel[:, index] += shift + pixel = invertedTransLimits.transform(pixel) + with np.errstate(invalid="ignore"): + return self.transform.transform(pixel)[:, self.coord_index] + + # Advance 2 pixels to the right in figure coordinates + world1 = shifted_pixel_to_world(0, 2) + dx = world1 - world0 - # Advance 2 pixels in figure coordinates - pixel2 = axes0.copy() - pixel2[:, 1] += 2.0 if self.frame.origin == "lower" else -2.0 - pixel2 = invertedTransLimits.transform(pixel2) - with np.errstate(invalid="ignore"): - world2 = self.transform.transform(pixel2)[:, self.coord_index] + # Where advancing to the right results in NaN, advance to the left instead + invalid1 = np.isnan(world1) + if invalid1.any(): + world1 = shifted_pixel_to_world(0, -2) + dx[invalid1] = (world0 - world1)[invalid1] - dx = world1 - world0 + # Advance 2 pixels up in figure coordinates + amount = 2.0 if self.frame.origin == "lower" else -2.0 + world2 = shifted_pixel_to_world(1, amount) dy = world2 - world0 + # Where advancing up results in NaN, advance down instead + invalid2 = np.isnan(world2) + if invalid2.any(): + world2 = shifted_pixel_to_world(1, -amount) + dy[invalid2] = (world0 - world2)[invalid2] + # Rotate by 90 degrees dx, dy = -dy, dx @@ -1191,6 +1203,7 @@ def _compute_ticks( data=(x_data_i, y_data_i), world=world, angle=spine.normal_angle[imin], + tick_angle=angle_i, axis_displacement=imin + frac, ) ) diff --git a/astropy/visualization/wcsaxes/tests/test_misc.py b/astropy/visualization/wcsaxes/tests/test_misc.py index e2395e4fe876..6e93ffa1e268 100644 --- a/astropy/visualization/wcsaxes/tests/test_misc.py +++ b/astropy/visualization/wcsaxes/tests/test_misc.py @@ -578,7 +578,7 @@ def test_set_labels_with_coords(ignore_matplotlibrc, frame_class): @pytest.mark.parametrize("atol", [0.2, 1.0e-8]) def test_bbox_size(atol): # Test for the size of a WCSAxes bbox (only have Matplotlib >= 3.0 now) - extents = [11.38888888888889, 3.5, 576.0, 432.0] + extents = [11.38888888888889, 3.388888888888889, 576.0, 432.0] fig = Figure() canvas = FigureCanvasAgg(fig) diff --git a/astropy/visualization/wcsaxes/ticklabels.py b/astropy/visualization/wcsaxes/ticklabels.py index fdc55a47b63d..e4cd92f88666 100644 --- a/astropy/visualization/wcsaxes/ticklabels.py +++ b/astropy/visualization/wcsaxes/ticklabels.py @@ -10,8 +10,6 @@ from astropy.utils.decorators import deprecated_renamed_argument from astropy.utils.exceptions import AstropyDeprecationWarning -from .frame import RectangularFrame - def sort_using(X, Y): return [x for (y, x) in sorted(zip(Y, X))] @@ -66,6 +64,7 @@ def clear(self): self.world = defaultdict(list) self.data = defaultdict(list) self.angle = defaultdict(list) + self.tick_angle = defaultdict(list) self.text = defaultdict(list) self.disp = defaultdict(list) @@ -78,6 +77,7 @@ def add( text=None, axis_displacement=None, data=None, + tick_angle=None, ): """ Add a label. @@ -98,6 +98,8 @@ def add( Displacement from axis. data : [float, float] Data coordinates of the label. + tick_angle : float + Angle of the corresponding tick. Defaults to be equal to ``angle``. """ required_args = ["axis", "world", "angle", "text", "axis_displacement", "data"] if pixel is not None: @@ -123,6 +125,7 @@ def add( self.world[axis].append(world) self.data[axis].append(data) self.angle[axis].append(angle) + self.tick_angle[axis].append(tick_angle if tick_angle is not None else angle) self.text[axis].append(text) self.disp[axis].append(axis_displacement) @@ -137,6 +140,7 @@ def sort(self): self.world[axis] = sort_using(self.world[axis], self.disp[axis]) self.data[axis] = sort_using(self.data[axis], self.disp[axis]) self.angle[axis] = sort_using(self.angle[axis], self.disp[axis]) + self.tick_angle[axis] = sort_using(self.tick_angle[axis], self.disp[axis]) self.text[axis] = sort_using(self.text[axis], self.disp[axis]) self.disp[axis] = sort_using(self.disp[axis], self.disp[axis]) self._stale = True @@ -229,89 +233,60 @@ def _set_xy_alignments(self, renderer): x, y = self._frame.parent_axes.transData.transform(self.data[axis][i]) pad = renderer.points_to_pixels(self.get_pad() + self._tick_out_size) - if isinstance(self._frame, RectangularFrame): - # This is just to preserve the current results, but can be - # removed next time the reference images are re-generated. - if np.abs(self.angle[axis][i]) < 45.0: - ha = "right" - va = "bottom" - dx = -pad - dy = -text_size * 0.5 - elif np.abs(self.angle[axis][i] - 90.0) < 45: - ha = "center" - va = "bottom" - dx = 0 - dy = -text_size - pad - elif np.abs(self.angle[axis][i] - 180.0) < 45: - ha = "left" - va = "bottom" - dx = pad - dy = -text_size * 0.5 + # Set initial position and find bounding box + self.set_text(self.text[axis][i]) + self.set_position((x, y)) + bb = super().get_window_extent(renderer) + + width = bb.width + height = bb.height + + # The pad direction (typically perpendicular to the spine) + pad_angle = np.radians(self.angle[axis][i]) + px = np.cos(pad_angle) + py = np.sin(pad_angle) + + # If the tick angle is NaN, use the pad angle for the tick angle + tick_angle = ( + np.radians(self.tick_angle[axis][i]) + if not np.isnan(self.tick_angle[axis][i]) + else pad_angle + ) + tx = np.cos(tick_angle) + ty = np.sin(tick_angle) + + # Set anchor point for label where the pad direction intersects bounding box + with np.errstate(divide="ignore"): + if np.abs(py / px) < np.abs(height / width): + ax = width / 2 * np.sign(px) + ay = width / 2 * py / np.abs(px) else: - ha = "center" - va = "bottom" - dx = 0 - dy = pad - - x = x + dx - y = y + dy - - else: - # This is the more general code for arbitrarily oriented - # axes - - # Set initial position and find bounding box - self.set_text(self.text[axis][i]) - self.set_position((x, y)) - bb = super().get_window_extent(renderer) - - # Find width and height, as well as angle at which we - # transition which side of the label we use to anchor the - # label. - width = bb.width - height = bb.height - - # Project axis angle onto bounding box - ax = np.cos(np.radians(self.angle[axis][i])) - ay = np.sin(np.radians(self.angle[axis][i])) - - # Set anchor point for label - if np.abs(self.angle[axis][i]) < 45.0: - dx = width - dy = ay * height - elif np.abs(self.angle[axis][i] - 90.0) < 45: - dx = ax * width - dy = height - elif np.abs(self.angle[axis][i] - 180.0) < 45: - dx = -width - dy = ay * height - else: - dx = ax * width - dy = -height - - dx *= 0.5 - dy *= 0.5 - - # Find normalized vector along axis normal, so as to be - # able to nudge the label away by a constant padding factor - - dist = np.hypot(dx, dy) - - ddx = dx / dist - ddy = dy / dist - - dx += ddx * pad - dy += ddy * pad - - x = x - dx - y = y - dy - - ha = "center" - va = "center" - - self.xy[axis][i] = (x, y) - self.ha[axis][i] = ha - self.va[axis][i] = va + ax = height / 2 * px / np.abs(py) + ay = height / 2 * np.sign(py) + + # Extract the component of the tick direction perpendicular to the pad direction + scale = tx * px + ty * py + vx = tx - px * scale + vy = ty - py * scale + + # We scale the above vector for adding to the pad direction to get a combined + # displacement. When the tick direction is close to the pad direction, the scaling + # is such that the displacement direction is exactly the tick direction. When the + # tick direction is close to perpendicular to the pad direction, we cap the scaling, + # which means that the effective tick direction is never more than 60 degrees + # perpendicular to the pad direction. This prevents pushing the tick label too far + # away from the tick. + scale = np.max([scale, 0.5]) # 0.5 == cos(60 deg) + vx /= scale + vy /= scale + + # Pad the anchor point in the combined displacement direction + dx = ax + (vx + px) * pad + dy = ay + (vy + py) * pad + + self.xy[axis][i] = (x - dx, y - dy) + self.ha[axis][i] = "center" + self.va[axis][i] = "center" self._stale = False diff --git a/docs/changes/visualization/19057.bugfix.rst b/docs/changes/visualization/19057.bugfix.rst new file mode 100644 index 000000000000..c3e705c870c5 --- /dev/null +++ b/docs/changes/visualization/19057.bugfix.rst @@ -0,0 +1 @@ +Fixed issues with the placement of WCSAxes tick labels for non-perpendicular grids or ticks. From 5d390cf2d02f8b38d8f78e902266ede81ed3ce8d Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 6 May 2026 14:16:20 -0400 Subject: [PATCH 168/199] Backport PR #19686: MNT: Ignore generated fast_converters.c --- astropy/io/votable/src/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/astropy/io/votable/src/.gitignore b/astropy/io/votable/src/.gitignore index 970d78fd3f93..3fc66339e375 100644 --- a/astropy/io/votable/src/.gitignore +++ b/astropy/io/votable/src/.gitignore @@ -1 +1,2 @@ !*.c +fast_converters.c From 9ae44a2ccdf421e065759d839217e95ba5ba6c97 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:52:47 -0400 Subject: [PATCH 169/199] Partial backport of PR 19837 (Bump the actions group in /.github/workflows with 3 updates) --- .github/workflows/ci_cron_monthly.yml | 2 +- .github/workflows/ci_cron_weekly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_cron_monthly.yml b/.github/workflows/ci_cron_monthly.yml index d039cd49aab0..a0d2bfa351d7 100644 --- a/.github/workflows/ci_cron_monthly.yml +++ b/.github/workflows/ci_cron_monthly.yml @@ -63,7 +63,7 @@ jobs: with: persist-credentials: false fetch-depth: 0 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 + - uses: uraimo/run-on-arch-action@f9b26e3a1a408d5fd530d20c17b9f3f4428ff8d9 # v3.1.0 name: Run tests id: build with: diff --git a/.github/workflows/ci_cron_weekly.yml b/.github/workflows/ci_cron_weekly.yml index cac007dba543..abae73dbf70b 100644 --- a/.github/workflows/ci_cron_weekly.yml +++ b/.github/workflows/ci_cron_weekly.yml @@ -139,7 +139,7 @@ jobs: with: persist-credentials: false fetch-depth: 0 - - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 + - uses: uraimo/run-on-arch-action@f9b26e3a1a408d5fd530d20c17b9f3f4428ff8d9 # v3.1.0 name: Run tests id: build with: From c1008189b1e8cd99e35ec183f0f7152261d76ac2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 1 Jun 2026 18:52:13 +0000 Subject: [PATCH 170/199] Update minimum required version of astropy-iers-data --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index edd93978dd37..b9094ebed2c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ keywords = [ "ascii", ] dependencies = [ - "astropy-iers-data>=0.2026.5.18.1.11.28", + "astropy-iers-data>=0.2026.6.1.17.39.59", "numpy>=1.24, <2.7", "packaging>=22.0.0", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires From 2fb31fd13f230f62df324b704d8e872f77265030 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 8 Jun 2026 20:44:37 +0100 Subject: [PATCH 171/199] Backport PR #19873: Update credits to add new contributors --- docs/credits.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/credits.rst b/docs/credits.rst index e2440429a6e5..e9d2eceb2fa6 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -232,6 +232,7 @@ Core Package Contributors * Humna Awan * iamsoto * Igor Lemos +* Ikbar Faiz * ikkamens * Inada Naoki * J\. Berg @@ -287,6 +288,7 @@ Core Package Contributors * Joseph Schlitz * Jost Migenda * JP Maia +* Juan Escudero Pedrosa * Juan Luis Cano Rodríguez * Juanjo Bazán * Julien Woillez @@ -434,6 +436,7 @@ Core Package Contributors * Nora Luetzgendorf * odidev * Ole Streicher +* Om S Habib * omahs * Orion Poplawski * orionlee From e08f734fd9d48b2eebc98f41b86c266f0b542439 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 9 Jun 2026 10:20:45 +0100 Subject: [PATCH 172/199] Backport PR #19864: DOC: Update license year --- LICENSE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.rst b/LICENSE.rst index f09379c82ef5..5945355a7190 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -1,4 +1,4 @@ -Copyright (c) 2011-2024, Astropy Developers +Copyright (c) 2011-2026, Astropy Developers All rights reserved. From 0299dd71e9c42b2969709b96631d759e145071a2 Mon Sep 17 00:00:00 2001 From: Ikbar Faiz Date: Tue, 9 Jun 2026 21:37:36 +0700 Subject: [PATCH 173/199] Backport PR #19871: DOC: Fix stretch parameter typo in docstrings --- astropy/visualization/mpl_normalize.py | 4 ++-- astropy/visualization/scripts/fits2bitmap.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/astropy/visualization/mpl_normalize.py b/astropy/visualization/mpl_normalize.py index 6d8e0d4d21bb..9adce48f5ba2 100644 --- a/astropy/visualization/mpl_normalize.py +++ b/astropy/visualization/mpl_normalize.py @@ -231,7 +231,7 @@ class SimpleNorm: Parameters ---------- - stretch : {'linear', 'sqrt', 'power', log', 'asinh', 'sinh'}, optional + stretch : {'linear', 'sqrt', 'power', 'log', 'asinh', 'sinh'}, optional The stretch function to apply to the image. The default is 'linear'. @@ -489,7 +489,7 @@ def simple_norm( data : ndarray The image array. - stretch : {'linear', 'sqrt', 'power', log', 'asinh', 'sinh'}, optional + stretch : {'linear', 'sqrt', 'power', 'log', 'asinh', 'sinh'}, optional The stretch function to apply to the image. The default is 'linear'. diff --git a/astropy/visualization/scripts/fits2bitmap.py b/astropy/visualization/scripts/fits2bitmap.py index 388a7d051449..0e8805e74b5b 100644 --- a/astropy/visualization/scripts/fits2bitmap.py +++ b/astropy/visualization/scripts/fits2bitmap.py @@ -42,7 +42,7 @@ def fits2bitmap( The filename of the output bitmap image. The type of bitmap is determined by the filename extension (e.g. '.jpg', '.png'). The default is a PNG file with the same name as the FITS file. - stretch : {'linear', 'sqrt', 'power', log', 'asinh'} + stretch : {'linear', 'sqrt', 'power', 'log', 'asinh'} The stretching function to apply to the image. The default is 'linear'. power : float, optional From 680bd3d79e78745b3be98e6b2545b47c45f338cf Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:16:49 -0400 Subject: [PATCH 174/199] Backport PR #19141: Fix `AttributeError` when pickling unbound `DataInfo` instances --- astropy/utils/data_info.py | 10 ++++++++-- astropy/utils/tests/test_data_info.py | 11 ++++++++++- docs/changes/utils/19141.bugfix.rst | 1 + 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 docs/changes/utils/19141.bugfix.rst diff --git a/astropy/utils/data_info.py b/astropy/utils/data_info.py index bf36975b93f9..0a43ac911c74 100644 --- a/astropy/utils/data_info.py +++ b/astropy/utils/data_info.py @@ -376,10 +376,16 @@ def __set__(self, instance, value): raise TypeError("info must be set with a DataInfo instance") def __getstate__(self): - return self._attrs + # If this is an unbound descriptor, _attrs is uninitialized, so + # return None to indicate no state for it. + return getattr(self, "_attrs", None) def __setstate__(self, state): - self._attrs = state + # Only assign _attrs if state is not None. This prevents _attrs + # from being set to None when unpickling an object that did not + # originally have _attrs initialized. + if state is not None: + self._attrs = state def _represent_as_dict(self, attrs=None): """Get the values for the parent ``attrs`` and return as a dict. This diff --git a/astropy/utils/tests/test_data_info.py b/astropy/utils/tests/test_data_info.py index 521133ed95d1..ea2c929af931 100644 --- a/astropy/utils/tests/test_data_info.py +++ b/astropy/utils/tests/test_data_info.py @@ -1,5 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import pickle + import numpy as np import pytest @@ -8,7 +10,7 @@ from astropy.table import QTable from astropy.table.index import SlicedIndex from astropy.time import Time -from astropy.utils.data_info import dtype_info_name +from astropy.utils.data_info import DataInfo, dtype_info_name STRING_TYPE_NAMES = {(True, "S"): "bytes", (True, "U"): "str"} @@ -106,3 +108,10 @@ def test_setting_info_name_to_with_invalid_type(): qt["a"] = [1, 2] * u.m with pytest.raises(TypeError, match="Expected a str value, got 1 with type int"): qt["a"].info.name = 1 + + +def test_pickle_unbound_data_info(): + """Test that an unbound DataInfo object can be pickled.""" + di = DataInfo() + di_rt = pickle.loads(pickle.dumps(di)) + assert isinstance(di_rt, DataInfo) diff --git a/docs/changes/utils/19141.bugfix.rst b/docs/changes/utils/19141.bugfix.rst new file mode 100644 index 000000000000..82592ba102db --- /dev/null +++ b/docs/changes/utils/19141.bugfix.rst @@ -0,0 +1 @@ +``AttributeError`` will no longer occur when attempting to pickle an unbound ``DataInfo`` instance. From 32cf3db9feae18811d9b8bd951a8982eaee5b860 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 12 Jun 2026 11:31:27 +0100 Subject: [PATCH 175/199] Backport PR #19904: Add MultiNorm to the nitpick exceptions --- docs/nitpick-exceptions | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/nitpick-exceptions b/docs/nitpick-exceptions index 2658d709051d..ab929b399e89 100644 --- a/docs/nitpick-exceptions +++ b/docs/nitpick-exceptions @@ -45,6 +45,7 @@ py:obj Bbox py:obj Transform py:obj Figure py:obj AbstractPathEffect +py:obj MultiNorm py:obj N py:obj masked From 05dbdb3e51cbef319cf9887a0bdc65e9fe53eb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Fri, 12 Jun 2026 14:36:39 +0200 Subject: [PATCH 176/199] Backport PR #19106: Fix unit extraction in structured_to_unstructured for StructuredQuantity --- .../units/quantity_helper/function_helpers.py | 9 +++++++-- .../units/tests/test_quantity_non_ufuncs.py | 19 +++++++++++++++++++ astropy/units/tests/test_structured.py | 2 +- docs/changes/units/19106.bugfix.rst | 1 + 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 docs/changes/units/19106.bugfix.rst diff --git a/astropy/units/quantity_helper/function_helpers.py b/astropy/units/quantity_helper/function_helpers.py index cb4296c542ef..86ac14416836 100644 --- a/astropy/units/quantity_helper/function_helpers.py +++ b/astropy/units/quantity_helper/function_helpers.py @@ -1601,7 +1601,11 @@ def structured_to_unstructured(arr, *args, **kwargs): """ from astropy.units import StructuredUnit - target_unit = arr.unit.values()[0] + if isinstance(arr.unit, StructuredUnit) and len(arr.unit) > 0: + target_unit = next(iter(arr.unit.values())) + else: + # Fallback if the unit isn't structured or is empty + target_unit = arr.unit def replace_unit(x): if isinstance(x, StructuredUnit): @@ -1609,7 +1613,8 @@ def replace_unit(x): else: return target_unit - to_unit = arr.unit._recursively_apply(replace_unit) + to_unit = replace_unit(arr.unit) + return (arr.to_value(to_unit),) + args, kwargs, target_unit, None diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py index d25fe49d5ef4..737fbc20c439 100644 --- a/astropy/units/tests/test_quantity_non_ufuncs.py +++ b/astropy/units/tests/test_quantity_non_ufuncs.py @@ -2852,6 +2852,25 @@ def test_merge_arrays_str(self, flatten): ): rfn.merge_arrays((self.q_pv, np.array(["a", "b", "c"])), flatten=flatten) + def test_structured_to_unstructured_success(self): + """ + Test that structured_to_unstructured correctly extracts + the target unit for structured quantities where all fields + share the same compatible unit initialized as a standard unit. + """ + # Using a standard unit (not StructuredUnit) triggers the bug + # where `unit` (a standard unit) has no `values()` method. + dtype = np.dtype([("a", "f8"), ("b", "f8")]) + data = np.array([(100.0, 2.0), (300.0, 4.0)], dtype=dtype) + unit = u.m + q = u.Quantity(data, unit=unit) + + result = rfn.structured_to_unstructured(q) + + assert isinstance(result, u.Quantity) + assert result.unit == u.m + assert_array_equal(result.value, np.array([[100.0, 2.0], [300.0, 4.0]])) + all_wrapped_functions = get_wrapped_functions( np, np.fft, np.linalg, np.lib.recfunctions diff --git a/astropy/units/tests/test_structured.py b/astropy/units/tests/test_structured.py index 3967e39cd796..8ed24cbf2c21 100644 --- a/astropy/units/tests/test_structured.py +++ b/astropy/units/tests/test_structured.py @@ -659,7 +659,7 @@ def test_structured_to_unstructured(self): # ``test_quantity_non_ufuncs.TestRecFunctions.test_structured_to_unstructured`` def test_unstructured_to_structured(self): - # can't structure something that's already structured + # can't structure something with incompatible shape dtype = np.dtype([("f1", float), ("f2", float)]) with pytest.raises(ValueError, match="The length of the last dimension"): rfn.unstructured_to_structured(self.q_pv, dtype=self.q_pv.dtype) diff --git a/docs/changes/units/19106.bugfix.rst b/docs/changes/units/19106.bugfix.rst new file mode 100644 index 000000000000..2400be54f864 --- /dev/null +++ b/docs/changes/units/19106.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug where ``numpy.lib.recfunctions.structured_to_unstructured`` failed when applied to a ``StructuredQuantity`` by improving unit extraction logic. From dd82265e71031f2f48ae5f65e64471b0a5beb27e Mon Sep 17 00:00:00 2001 From: veyron Date: Fri, 12 Jun 2026 20:40:15 +0530 Subject: [PATCH 177/199] Backport PR #19903: Fix SortedArray index range query dropping duplicate rows --- astropy/table/sorted_array.py | 57 ++++++++++++++++++----------- astropy/table/tests/test_index.py | 19 ++++++++++ docs/changes/table/19903.bugfix.rst | 3 ++ 3 files changed, 57 insertions(+), 22 deletions(-) create mode 100644 docs/changes/table/19903.bugfix.rst diff --git a/astropy/table/sorted_array.py b/astropy/table/sorted_array.py index eb660e3ad76d..f8aecad12313 100644 --- a/astropy/table/sorted_array.py +++ b/astropy/table/sorted_array.py @@ -93,7 +93,7 @@ def _get_key_slice(self, i, begin, end): def find_pos(self, key, data, exact=False): """ - Return the index of the largest key in data greater than or + Return the index of the first key in data greater than or equal to the given key, data pair. Parameters @@ -190,27 +190,40 @@ def range( argument corresponds to an inclusive lower bound, and the second argument to an inclusive upper bound. """ - # Find initial positions for lower and upper bounds. Just like a slice object, - # None values for `lower` or `upper` correspond to no bound in that direction. - lower_pos = 0 if lower is None else self.find_pos(lower, 0) - upper_pos = len(self.row_index) if upper is None else self.find_pos(upper, 0) - - if lower_pos == len(self.row_index): - return [] - - lower_bound = tuple(col[lower_pos] for col in self.cols) - if not bounds[0] and lower_bound == lower: - lower_pos += 1 # data[lower_pos] > lower - - # data[lower_pos] >= lower - # data[upper_pos] >= upper - if upper_pos < len(self.row_index): - upper_bound = tuple(col[upper_pos] for col in self.cols) - if not bounds[1] and upper_bound == upper: - upper_pos -= 1 # data[upper_pos] < upper - elif upper_bound > upper: - upper_pos -= 1 # data[upper_pos] <= upper - return self.row_index[lower_pos : upper_pos + 1] + n = len(self.row_index) + + def bisect_left(key): + # Position of the first entry whose key is >= ``key``. + return self.find_pos(key, 0) + + def bisect_right(key): + # Position of the first entry whose key is > ``key`` (i.e. just + # past the last entry equal to ``key``). + if self.unique: + pos = self.find_pos(key, 0) + if pos < n and tuple(col[pos] for col in self.cols) == key: + pos += 1 + return pos + # For a non-unique index the stored keys include the row number, so + # searching with a row number larger than any real row lands just + # past the last entry equal to ``key`` (and thus past all duplicates). + return self.find_pos(key, n) + + # Compute a half-open range [lower_pos, upper_pos). Just like a slice + # object, a None value for `lower` or `upper` means no bound in that + # direction. Using bisect_left/bisect_right ensures all entries that + # share a value with an inclusive bound are included, even duplicates. + if lower is None: + lower_pos = 0 + else: + lower_pos = bisect_left(lower) if bounds[0] else bisect_right(lower) + + if upper is None: + upper_pos = n + else: + upper_pos = bisect_right(upper) if bounds[1] else bisect_left(upper) + + return self.row_index[lower_pos:upper_pos] def remove(self, key: tuple, data: int) -> bool: """ diff --git a/astropy/table/tests/test_index.py b/astropy/table/tests/test_index.py index 73be1ee68c5d..dfc0f5f46df3 100644 --- a/astropy/table/tests/test_index.py +++ b/astropy/table/tests/test_index.py @@ -883,3 +883,22 @@ def test_index_not_corrupted_on_failed_row_assignment(engine): # No ghost entry for the attempted new value with pytest.raises(KeyError): t.loc[99] + + +@pytest.mark.parametrize("table_type", [Table, QTable]) +def test_loc_range_with_duplicate_index_values(engine, table_type): + """Regression test: a range query must return every row whose value equals + an inclusive bound, even when that value is duplicated in the index. + + The default ``SortedArray`` engine previously kept only the first matching + row, so e.g. ``t.loc[1:2]`` silently dropped all but one of the ``2`` rows. + """ + t = table_type() + t["a"] = [3, 2, 2, 3, 1, 2] + t["b"] = [30, 20, 21, 31, 10, 22] + t.add_index("a", engine=engine) + + assert sorted(t.loc[1:2]["b"].tolist()) == [10, 20, 21, 22] + assert sorted(t.loc[2:2]["b"].tolist()) == [20, 21, 22] + assert sorted(t.loc[2:3]["b"].tolist()) == [20, 21, 22, 30, 31] + assert sorted(t.loc[:]["b"].tolist()) == [10, 20, 21, 22, 30, 31] diff --git a/docs/changes/table/19903.bugfix.rst b/docs/changes/table/19903.bugfix.rst new file mode 100644 index 000000000000..476db8efef92 --- /dev/null +++ b/docs/changes/table/19903.bugfix.rst @@ -0,0 +1,3 @@ +Fixed a bug where a ``Table.loc[]`` range query using the default +``SortedArray`` index engine silently dropped rows when the upper bound +matched a value that appears more than once in the indexed column. From c2916a593576e158d47d679993d18abc64dd037e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 15 Jun 2026 22:18:36 +0200 Subject: [PATCH 178/199] Backport PR #19921: Update credits to add new contributors --- docs/credits.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/credits.rst b/docs/credits.rst index e9d2eceb2fa6..a9a6f00ca274 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -593,6 +593,7 @@ Core Package Contributors * Varun Kasyap Pentamaraju * Varun Nikam * Vatsala Swaroop +* veyron * Víctor Terrón * Víctor Zabalza * Victoria Dye From 4bcd8395431aff7a32e128e717a67fa01a67d079 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 16 Jun 2026 10:20:39 +0100 Subject: [PATCH 179/199] Finalizing changelog for v7.2.1 --- CHANGES.rst | 176 ++++++++++++++++++++ docs/changes/19347.other.rst | 1 - docs/changes/config/19559.bugfix.rst | 2 - docs/changes/coordinates/18687.bugfix.rst | 2 - docs/changes/coordinates/19001.bugfix.rst | 6 - docs/changes/coordinates/19308.bugfix.rst | 2 - docs/changes/coordinates/19330.bugfix.rst | 2 - docs/changes/coordinates/19403.bugfix.rst | 1 - docs/changes/io.fits/19023.bugfix.rst | 1 - docs/changes/io.fits/19335.bugfix.rst | 5 - docs/changes/io.fits/19362.bugfix.rst | 1 - docs/changes/io.fits/19363.bugfix.rst | 1 - docs/changes/io.fits/19367.bugfix.rst | 1 - docs/changes/io.fits/19404.bugfix.rst | 4 - docs/changes/io.fits/19416.bugfix.rst | 1 - docs/changes/io.fits/19438.bugfix.rst | 3 - docs/changes/io.fits/19592.bugfix.rst | 3 - docs/changes/io.fits/19670.bugfix.rst | 8 - docs/changes/io.fits/19738.bugfix.rst | 4 - docs/changes/modeling/19189.bugfix.rst | 1 - docs/changes/nddata/19829.bugfix.rst | 1 - docs/changes/stats/19372.bugfix.rst | 1 - docs/changes/table/19199.bugfix.rst | 2 - docs/changes/table/19358.bugfix.rst | 3 - docs/changes/table/19450.bugfix.rst | 2 - docs/changes/table/19466.bugfix.rst | 2 - docs/changes/time/19368.bugfix.rst | 1 - docs/changes/units/19055.bugfix.rst | 3 - docs/changes/utils/19141.bugfix.rst | 1 - docs/changes/utils/19142.bugfix.rst | 1 - docs/changes/utils/19351.bugfix.rst | 1 - docs/changes/utils/19534.bugfix.rst | 3 - docs/changes/utils/19594.bugfix.rst | 4 - docs/changes/utils/19672.bugfix.rst | 1 - docs/changes/utils/19700.bugfix.rst | 1 - docs/changes/visualization/19057.bugfix.rst | 1 - docs/changes/wcs/19506.bugfix.rst | 1 - docs/changes/wcs/19591.bugfix.rst | 9 - docs/changes/wcs/19720.bugfix.rst | 1 - docs/changes/wcs/19819.bugfix.rst | 3 - 40 files changed, 176 insertions(+), 91 deletions(-) delete mode 100644 docs/changes/19347.other.rst delete mode 100644 docs/changes/config/19559.bugfix.rst delete mode 100644 docs/changes/coordinates/18687.bugfix.rst delete mode 100644 docs/changes/coordinates/19001.bugfix.rst delete mode 100644 docs/changes/coordinates/19308.bugfix.rst delete mode 100644 docs/changes/coordinates/19330.bugfix.rst delete mode 100644 docs/changes/coordinates/19403.bugfix.rst delete mode 100644 docs/changes/io.fits/19023.bugfix.rst delete mode 100644 docs/changes/io.fits/19335.bugfix.rst delete mode 100644 docs/changes/io.fits/19362.bugfix.rst delete mode 100644 docs/changes/io.fits/19363.bugfix.rst delete mode 100644 docs/changes/io.fits/19367.bugfix.rst delete mode 100644 docs/changes/io.fits/19404.bugfix.rst delete mode 100644 docs/changes/io.fits/19416.bugfix.rst delete mode 100644 docs/changes/io.fits/19438.bugfix.rst delete mode 100644 docs/changes/io.fits/19592.bugfix.rst delete mode 100644 docs/changes/io.fits/19670.bugfix.rst delete mode 100644 docs/changes/io.fits/19738.bugfix.rst delete mode 100644 docs/changes/modeling/19189.bugfix.rst delete mode 100644 docs/changes/nddata/19829.bugfix.rst delete mode 100644 docs/changes/stats/19372.bugfix.rst delete mode 100644 docs/changes/table/19199.bugfix.rst delete mode 100644 docs/changes/table/19358.bugfix.rst delete mode 100644 docs/changes/table/19450.bugfix.rst delete mode 100644 docs/changes/table/19466.bugfix.rst delete mode 100644 docs/changes/time/19368.bugfix.rst delete mode 100644 docs/changes/units/19055.bugfix.rst delete mode 100644 docs/changes/utils/19141.bugfix.rst delete mode 100644 docs/changes/utils/19142.bugfix.rst delete mode 100644 docs/changes/utils/19351.bugfix.rst delete mode 100644 docs/changes/utils/19534.bugfix.rst delete mode 100644 docs/changes/utils/19594.bugfix.rst delete mode 100644 docs/changes/utils/19672.bugfix.rst delete mode 100644 docs/changes/utils/19700.bugfix.rst delete mode 100644 docs/changes/visualization/19057.bugfix.rst delete mode 100644 docs/changes/wcs/19506.bugfix.rst delete mode 100644 docs/changes/wcs/19591.bugfix.rst delete mode 100644 docs/changes/wcs/19720.bugfix.rst delete mode 100644 docs/changes/wcs/19819.bugfix.rst diff --git a/CHANGES.rst b/CHANGES.rst index 6057fe864a47..2a991d17071b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,179 @@ +Version 7.2.1 (2026-06-16) +========================== + + +Bug Fixes +--------- + +astropy.config +^^^^^^^^^^^^^^ + +- Disabling thread concurrency within ``set_temp_cache`` and ``set_temp_config`` + context managers, ensuring thread safety. [#19559] + +astropy.coordinates +^^^^^^^^^^^^^^^^^^^ + +- The ``refresh_cache`` parameter of ``EarthLocation.of_site()`` and + ``EarthLocation.get_site_names()`` methods now works. [#18687] + +- Relativistic Doppler shifts are now applied to ``SpectralCoord`` in the correct + direction even if the ``astropy.units.spectral()`` equivalency is enabled. + Previously enabling the equivalency could cause wrong results with the + ``SpectralCoord.to_rest()``, + ``SpectralCoord.with_observer_stationary_relative_to()`` or + ``SpectralCoord.with_radial_velocity_shift()`` methods. [#19001] + +- Fixed a broadcasting bug in ``astropy.uncertainty.distributions.uniform`` where + multi-dimensional inputs (``ndim >= 2``) would raise a ``ValueError``. [#19308] + +- Send a User-Agent header to the OpenStreetMap API in ``EarthLocation.of_address`` + to fix access denied errors. [#19330] + +- Fixed a missing f-string in a TypeError from ``BaseAffineTransform._apply_transform`` that caused ``{data.__class__}`` to appear literally in the error message. [#19403] + +astropy.io.fits +^^^^^^^^^^^^^^^ + +- Fix ``getdata()``'s lower and upper keywords. [#19023] + +- ``np.char.chararray`` being deprecated in Numpy 2.5, in a future version + ``io.fits`` will return a normal array instead of a ``chararray`` for string + columns. As a consequence the special chararray methods will be deprecated in + version 8.0 (e.g., ``.rstrip()`` or ``.decode()``). In version 7.2.x deprecation + warnings from Numpy are simply filtered. [#19335] + +- Fix a bug which caused CompImageHDU to have both SIMPLE and XTENSION keywords when initialized from a PrimaryHDU header. [#19362] + +- Fix support for reading FITS files using image tile compression with UNCOMPRESSED_DATA columns. [#19363] + +- Fixed a bug that caused reading FITS files to not work properly on WASM when relying on the default memmap settings. [#19367] + +- Fixed silent data corruption in ``FITS_rec.__setitem__`` when using negative + slice indices. Assigning to slices like ``data[-2:] = new_rows`` previously + wrote to the wrong rows because negative indices were clamped to 0 instead of + being resolved relative to the array length. [#19404] + +- Fixed a bug that caused compressed FITS files to become corrupted when opened in update mode and modifying the header. [#19416] + +- Fix a bug that caused header verification for CompImageHDU to not work correctly and + in some cases produce corrupt files when the decompressed header was a primary HDU + header not an extension header. [#19438] + +- Prevent creating RICE_1 or PLIO_1 compressed files with (u)int64 data. If + possible without overflow data is converted to (u)int32, otherwise an error is + raised. [#19592] + +- Fix a bug that caused uint64 data to be silently shifted by 2**63 when + round-tripping through compressed FITS files (the FITS BZERO=2**63 offset was + missing at compression time but still applied on read). Extend the existing + RICE_1 and PLIO_1 64-bit-to-32-bit conversion fallback to also cover uint64 + input and HCOMPRESS_1, and fix big-endian 64-bit input being silently truncated + by the conversion check instead of raising. Reject PLIO_1 with unsigned + multi-byte input outright, since the BZERO convention used to store unsigned + FITS data produces negative values that PLIO cannot encode. [#19670] + +- Fix a bug that caused int8 data to be shifted by 128 when round-tripping + through compressed FITS files (the FITS BZERO=-128 offset was missing at + compression time but still applied on read), so writing ``[0, 1]`` and + reading back returned ``[-128, -127]``. [#19738] + +astropy.modeling +^^^^^^^^^^^^^^^^ + +- Bugfix for ``Parameter.value`` accessor when the parameter value has not been set yet. [#19189] + +astropy.nddata +^^^^^^^^^^^^^^ + +- Fix unexpected behavior in ``Cutout2D.plot_on_original()``. The displayed region spanned pixels not included in the cutout array. [#19829] + +astropy.stats +^^^^^^^^^^^^^ + +- Fixed pickling of ``SigmaClip`` objects when Bottleneck is installed. ``_dtype_dispatch`` returned a local closure which cannot be pickled; replaced with a picklable ``_DtypeDispatch`` class. [#19372] + +astropy.table +^^^^^^^^^^^^^ + +- Tables can now be joined and stacked also if they contain columns with + user-defined data types such as ``QuadPrecDtype``. [#19199] + +- Fixed ``AssertionError`` when passing ``numpy.bool_`` as the + ``index`` argument to ``Table.to_pandas()`` or ``Table.to_dataframe()``. + The correct ``ValueError`` is now raised instead. [#19358] + +- Fixed table index getting corrupted when a row assignment raises an exception + mid-update. The index is now properly restored to its original state on failure. [#19450] + +- Fixes a problem where deepcopying a MaskedColumn did not correctly create the ``info`` + attribute. This resulted in an inability to print the column after the deepcopy. [#19466] + +astropy.time +^^^^^^^^^^^^ + +- Fixed missing ``goto fail`` in ``create_parser`` in ``parse_times.c`` after setting a ``ValueError`` for invalid parameter array size, preventing execution from continuing with an exception already set. [#19368] + +astropy.units +^^^^^^^^^^^^^ + +- Fixed a bug in the ``np.average`` function when weights with units and a + different shape to the input array were passed and the optionally-returned sum + of the weights was requested. The sum of the weights now has correct units. [#19055] + +astropy.utils +^^^^^^^^^^^^^ + +- ``AttributeError`` will no longer occur when attempting to pickle an unbound ``DataInfo`` instance. [#19141] + +- Pickling ``Masked`` subclasses with an initialized ``info`` attribute no longer fails. [#19142] + +- ``ShapedLikeNDArray.take()`` now raises ``NotImplementedError`` when ``out`` is passed, instead of returning the exception object. [#19351] + +- Ensure that ``utils.masked.get_data_and_mask`` works properly with containers, + returning ``None`` for the mask if ``masked`` is not set (instead of returning + an all-False array). [#19534] + +- The dummy file object used by ``astropy.utils.misc.silence`` to replace + ``sys.stdout``/``sys.stderr`` now implements ``flush()`` and ``isatty()``, + so importing libraries that probe the stream (e.g. IPython 9.13 at import + time) no longer raises ``AttributeError`` under ``silence``. [#19594] + +- Fixed a bug where values returned by ``cache_contents`` could include files and symlinks, instead of just directories. [#19672] + +- Add missing ``stacklevel`` arguments in warnings emitted from ``astropy.utils.data`` APIs [#19700] + +astropy.visualization +^^^^^^^^^^^^^^^^^^^^^ + +- Fixed issues with the placement of WCSAxes tick labels for non-perpendicular grids or ticks. [#19057] + +astropy.wcs +^^^^^^^^^^^ + +- Fixed a bug where degree units were hardcoded into the FITS WCS APE 14 ``world_to_pixel`` method. [#19506] + +- Lazily-populated caches on a ``WCS`` (such as the internal + ``world_axis_object_components``/``world_axis_object_classes`` cache) are no + longer included in the pickled state. They are regenerated on demand after + unpickling, which keeps pickling robust even when a cache entry holds a + non-picklable value. + + Fixed a bug where the ``preserve_units`` option passed to the ``WCS`` + constructor was silently reset to ``False`` when a ``WCS`` object was pickled + and unpickled. [#19591] + +- Fix reference-count handling after ``PyList_SetItem`` steals references. [#19720] + +- Made the WCS coordinate transform paths (``pixel_to_world``, ``world_to_pixel``, + ``all_pix2world``, ``wcs_pix2world``, ``wcs_world2pix``, ``mix``) safe to call + concurrently on a shared ``WCS`` instance from multiple threads. [#19819] + +Other Changes and Additions +--------------------------- + +- Publishing files to PyPI is now done using the Trusted Publisher mechanism (https://docs.pypi.org/trusted-publishers/). [#19347] + Version 7.2.0 (2025-11-25) ========================== diff --git a/docs/changes/19347.other.rst b/docs/changes/19347.other.rst deleted file mode 100644 index dae27136e928..000000000000 --- a/docs/changes/19347.other.rst +++ /dev/null @@ -1 +0,0 @@ -Publishing files to PyPI is now done using the Trusted Publisher mechanism (https://docs.pypi.org/trusted-publishers/). diff --git a/docs/changes/config/19559.bugfix.rst b/docs/changes/config/19559.bugfix.rst deleted file mode 100644 index 7da143c2926d..000000000000 --- a/docs/changes/config/19559.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Disabling thread concurrency within ``set_temp_cache`` and ``set_temp_config`` -context managers, ensuring thread safety. diff --git a/docs/changes/coordinates/18687.bugfix.rst b/docs/changes/coordinates/18687.bugfix.rst deleted file mode 100644 index 97ed51c5743d..000000000000 --- a/docs/changes/coordinates/18687.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -The ``refresh_cache`` parameter of ``EarthLocation.of_site()`` and -``EarthLocation.get_site_names()`` methods now works. diff --git a/docs/changes/coordinates/19001.bugfix.rst b/docs/changes/coordinates/19001.bugfix.rst deleted file mode 100644 index c0975104fc27..000000000000 --- a/docs/changes/coordinates/19001.bugfix.rst +++ /dev/null @@ -1,6 +0,0 @@ -Relativistic Doppler shifts are now applied to ``SpectralCoord`` in the correct -direction even if the ``astropy.units.spectral()`` equivalency is enabled. -Previously enabling the equivalency could cause wrong results with the -``SpectralCoord.to_rest()``, -``SpectralCoord.with_observer_stationary_relative_to()`` or -``SpectralCoord.with_radial_velocity_shift()`` methods. diff --git a/docs/changes/coordinates/19308.bugfix.rst b/docs/changes/coordinates/19308.bugfix.rst deleted file mode 100644 index ebb9e210d15f..000000000000 --- a/docs/changes/coordinates/19308.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed a broadcasting bug in ``astropy.uncertainty.distributions.uniform`` where -multi-dimensional inputs (``ndim >= 2``) would raise a ``ValueError``. diff --git a/docs/changes/coordinates/19330.bugfix.rst b/docs/changes/coordinates/19330.bugfix.rst deleted file mode 100644 index 60df745beb2b..000000000000 --- a/docs/changes/coordinates/19330.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Send a User-Agent header to the OpenStreetMap API in ``EarthLocation.of_address`` -to fix access denied errors. diff --git a/docs/changes/coordinates/19403.bugfix.rst b/docs/changes/coordinates/19403.bugfix.rst deleted file mode 100644 index 31180d7ccf6f..000000000000 --- a/docs/changes/coordinates/19403.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a missing f-string in a TypeError from ``BaseAffineTransform._apply_transform`` that caused ``{data.__class__}`` to appear literally in the error message. diff --git a/docs/changes/io.fits/19023.bugfix.rst b/docs/changes/io.fits/19023.bugfix.rst deleted file mode 100644 index bfca12894ad4..000000000000 --- a/docs/changes/io.fits/19023.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``getdata()``'s lower and upper keywords. diff --git a/docs/changes/io.fits/19335.bugfix.rst b/docs/changes/io.fits/19335.bugfix.rst deleted file mode 100644 index 079edfa86a16..000000000000 --- a/docs/changes/io.fits/19335.bugfix.rst +++ /dev/null @@ -1,5 +0,0 @@ -``np.char.chararray`` being deprecated in Numpy 2.5, in a future version -``io.fits`` will return a normal array instead of a ``chararray`` for string -columns. As a consequence the special chararray methods will be deprecated in -version 8.0 (e.g., ``.rstrip()`` or ``.decode()``). In version 7.2.x deprecation -warnings from Numpy are simply filtered. diff --git a/docs/changes/io.fits/19362.bugfix.rst b/docs/changes/io.fits/19362.bugfix.rst deleted file mode 100644 index ce82b255df9e..000000000000 --- a/docs/changes/io.fits/19362.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which caused CompImageHDU to have both SIMPLE and XTENSION keywords when initialized from a PrimaryHDU header. diff --git a/docs/changes/io.fits/19363.bugfix.rst b/docs/changes/io.fits/19363.bugfix.rst deleted file mode 100644 index a0c61a1ebf9e..000000000000 --- a/docs/changes/io.fits/19363.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix support for reading FITS files using image tile compression with UNCOMPRESSED_DATA columns. diff --git a/docs/changes/io.fits/19367.bugfix.rst b/docs/changes/io.fits/19367.bugfix.rst deleted file mode 100644 index ed5faafc252b..000000000000 --- a/docs/changes/io.fits/19367.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug that caused reading FITS files to not work properly on WASM when relying on the default memmap settings. diff --git a/docs/changes/io.fits/19404.bugfix.rst b/docs/changes/io.fits/19404.bugfix.rst deleted file mode 100644 index a28512f6d491..000000000000 --- a/docs/changes/io.fits/19404.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fixed silent data corruption in ``FITS_rec.__setitem__`` when using negative -slice indices. Assigning to slices like ``data[-2:] = new_rows`` previously -wrote to the wrong rows because negative indices were clamped to 0 instead of -being resolved relative to the array length. diff --git a/docs/changes/io.fits/19416.bugfix.rst b/docs/changes/io.fits/19416.bugfix.rst deleted file mode 100644 index 2278a62e0a79..000000000000 --- a/docs/changes/io.fits/19416.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug that caused compressed FITS files to become corrupted when opened in update mode and modifying the header. diff --git a/docs/changes/io.fits/19438.bugfix.rst b/docs/changes/io.fits/19438.bugfix.rst deleted file mode 100644 index 1a35992df565..000000000000 --- a/docs/changes/io.fits/19438.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix a bug that caused header verification for CompImageHDU to not work correctly and -in some cases produce corrupt files when the decompressed header was a primary HDU -header not an extension header. diff --git a/docs/changes/io.fits/19592.bugfix.rst b/docs/changes/io.fits/19592.bugfix.rst deleted file mode 100644 index 3a9de9f60951..000000000000 --- a/docs/changes/io.fits/19592.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Prevent creating RICE_1 or PLIO_1 compressed files with (u)int64 data. If -possible without overflow data is converted to (u)int32, otherwise an error is -raised. diff --git a/docs/changes/io.fits/19670.bugfix.rst b/docs/changes/io.fits/19670.bugfix.rst deleted file mode 100644 index 5c037ccd5bc8..000000000000 --- a/docs/changes/io.fits/19670.bugfix.rst +++ /dev/null @@ -1,8 +0,0 @@ -Fix a bug that caused uint64 data to be silently shifted by 2**63 when -round-tripping through compressed FITS files (the FITS BZERO=2**63 offset was -missing at compression time but still applied on read). Extend the existing -RICE_1 and PLIO_1 64-bit-to-32-bit conversion fallback to also cover uint64 -input and HCOMPRESS_1, and fix big-endian 64-bit input being silently truncated -by the conversion check instead of raising. Reject PLIO_1 with unsigned -multi-byte input outright, since the BZERO convention used to store unsigned -FITS data produces negative values that PLIO cannot encode. diff --git a/docs/changes/io.fits/19738.bugfix.rst b/docs/changes/io.fits/19738.bugfix.rst deleted file mode 100644 index 5839e96cb25b..000000000000 --- a/docs/changes/io.fits/19738.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fix a bug that caused int8 data to be shifted by 128 when round-tripping -through compressed FITS files (the FITS BZERO=-128 offset was missing at -compression time but still applied on read), so writing ``[0, 1]`` and -reading back returned ``[-128, -127]``. diff --git a/docs/changes/modeling/19189.bugfix.rst b/docs/changes/modeling/19189.bugfix.rst deleted file mode 100644 index 2983132014bf..000000000000 --- a/docs/changes/modeling/19189.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Bugfix for ``Parameter.value`` accessor when the parameter value has not been set yet. diff --git a/docs/changes/nddata/19829.bugfix.rst b/docs/changes/nddata/19829.bugfix.rst deleted file mode 100644 index 4abf07fbe9f8..000000000000 --- a/docs/changes/nddata/19829.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix unexpected behavior in ``Cutout2D.plot_on_original()``. The displayed region spanned pixels not included in the cutout array. diff --git a/docs/changes/stats/19372.bugfix.rst b/docs/changes/stats/19372.bugfix.rst deleted file mode 100644 index 05d5b3d4b6aa..000000000000 --- a/docs/changes/stats/19372.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed pickling of ``SigmaClip`` objects when Bottleneck is installed. ``_dtype_dispatch`` returned a local closure which cannot be pickled; replaced with a picklable ``_DtypeDispatch`` class. diff --git a/docs/changes/table/19199.bugfix.rst b/docs/changes/table/19199.bugfix.rst deleted file mode 100644 index 140582a5f856..000000000000 --- a/docs/changes/table/19199.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Tables can now be joined and stacked also if they contain columns with -user-defined data types such as ``QuadPrecDtype``. diff --git a/docs/changes/table/19358.bugfix.rst b/docs/changes/table/19358.bugfix.rst deleted file mode 100644 index f837ee97b524..000000000000 --- a/docs/changes/table/19358.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed ``AssertionError`` when passing ``numpy.bool_`` as the -``index`` argument to ``Table.to_pandas()`` or ``Table.to_dataframe()``. -The correct ``ValueError`` is now raised instead. diff --git a/docs/changes/table/19450.bugfix.rst b/docs/changes/table/19450.bugfix.rst deleted file mode 100644 index 5f3e137dea8f..000000000000 --- a/docs/changes/table/19450.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed table index getting corrupted when a row assignment raises an exception -mid-update. The index is now properly restored to its original state on failure. diff --git a/docs/changes/table/19466.bugfix.rst b/docs/changes/table/19466.bugfix.rst deleted file mode 100644 index 10c79a8de077..000000000000 --- a/docs/changes/table/19466.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixes a problem where deepcopying a MaskedColumn did not correctly create the ``info`` -attribute. This resulted in an inability to print the column after the deepcopy. diff --git a/docs/changes/time/19368.bugfix.rst b/docs/changes/time/19368.bugfix.rst deleted file mode 100644 index 79a5070fe64c..000000000000 --- a/docs/changes/time/19368.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed missing ``goto fail`` in ``create_parser`` in ``parse_times.c`` after setting a ``ValueError`` for invalid parameter array size, preventing execution from continuing with an exception already set. diff --git a/docs/changes/units/19055.bugfix.rst b/docs/changes/units/19055.bugfix.rst deleted file mode 100644 index c658508f0e62..000000000000 --- a/docs/changes/units/19055.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed a bug in the ``np.average`` function when weights with units and a -different shape to the input array were passed and the optionally-returned sum -of the weights was requested. The sum of the weights now has correct units. diff --git a/docs/changes/utils/19141.bugfix.rst b/docs/changes/utils/19141.bugfix.rst deleted file mode 100644 index 82592ba102db..000000000000 --- a/docs/changes/utils/19141.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``AttributeError`` will no longer occur when attempting to pickle an unbound ``DataInfo`` instance. diff --git a/docs/changes/utils/19142.bugfix.rst b/docs/changes/utils/19142.bugfix.rst deleted file mode 100644 index ddbd2191c3cc..000000000000 --- a/docs/changes/utils/19142.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Pickling ``Masked`` subclasses with an initialized ``info`` attribute no longer fails. diff --git a/docs/changes/utils/19351.bugfix.rst b/docs/changes/utils/19351.bugfix.rst deleted file mode 100644 index 1211519191af..000000000000 --- a/docs/changes/utils/19351.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``ShapedLikeNDArray.take()`` now raises ``NotImplementedError`` when ``out`` is passed, instead of returning the exception object. diff --git a/docs/changes/utils/19534.bugfix.rst b/docs/changes/utils/19534.bugfix.rst deleted file mode 100644 index 6b4bf8d5a3b0..000000000000 --- a/docs/changes/utils/19534.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Ensure that ``utils.masked.get_data_and_mask`` works properly with containers, -returning ``None`` for the mask if ``masked`` is not set (instead of returning -an all-False array). diff --git a/docs/changes/utils/19594.bugfix.rst b/docs/changes/utils/19594.bugfix.rst deleted file mode 100644 index f3c85ec51eb5..000000000000 --- a/docs/changes/utils/19594.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -The dummy file object used by ``astropy.utils.misc.silence`` to replace -``sys.stdout``/``sys.stderr`` now implements ``flush()`` and ``isatty()``, -so importing libraries that probe the stream (e.g. IPython 9.13 at import -time) no longer raises ``AttributeError`` under ``silence``. diff --git a/docs/changes/utils/19672.bugfix.rst b/docs/changes/utils/19672.bugfix.rst deleted file mode 100644 index 5cec185fa9b9..000000000000 --- a/docs/changes/utils/19672.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug where values returned by ``cache_contents`` could include files and symlinks, instead of just directories. diff --git a/docs/changes/utils/19700.bugfix.rst b/docs/changes/utils/19700.bugfix.rst deleted file mode 100644 index b44f48635547..000000000000 --- a/docs/changes/utils/19700.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Add missing ``stacklevel`` arguments in warnings emitted from ``astropy.utils.data`` APIs diff --git a/docs/changes/visualization/19057.bugfix.rst b/docs/changes/visualization/19057.bugfix.rst deleted file mode 100644 index c3e705c870c5..000000000000 --- a/docs/changes/visualization/19057.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed issues with the placement of WCSAxes tick labels for non-perpendicular grids or ticks. diff --git a/docs/changes/wcs/19506.bugfix.rst b/docs/changes/wcs/19506.bugfix.rst deleted file mode 100644 index 9c33379c049b..000000000000 --- a/docs/changes/wcs/19506.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug where degree units were hardcoded into the FITS WCS APE 14 ``world_to_pixel`` method. diff --git a/docs/changes/wcs/19591.bugfix.rst b/docs/changes/wcs/19591.bugfix.rst deleted file mode 100644 index 9ebd28094d1f..000000000000 --- a/docs/changes/wcs/19591.bugfix.rst +++ /dev/null @@ -1,9 +0,0 @@ -Lazily-populated caches on a ``WCS`` (such as the internal -``world_axis_object_components``/``world_axis_object_classes`` cache) are no -longer included in the pickled state. They are regenerated on demand after -unpickling, which keeps pickling robust even when a cache entry holds a -non-picklable value. - -Fixed a bug where the ``preserve_units`` option passed to the ``WCS`` -constructor was silently reset to ``False`` when a ``WCS`` object was pickled -and unpickled. diff --git a/docs/changes/wcs/19720.bugfix.rst b/docs/changes/wcs/19720.bugfix.rst deleted file mode 100644 index 904186bcbf61..000000000000 --- a/docs/changes/wcs/19720.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix reference-count handling after ``PyList_SetItem`` steals references. diff --git a/docs/changes/wcs/19819.bugfix.rst b/docs/changes/wcs/19819.bugfix.rst deleted file mode 100644 index 0e57b87b18da..000000000000 --- a/docs/changes/wcs/19819.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Made the WCS coordinate transform paths (``pixel_to_world``, ``world_to_pixel``, -``all_pix2world``, ``wcs_pix2world``, ``wcs_world2pix``, ``mix``) safe to call -concurrently on a shared ``WCS`` instance from multiple threads. From 99adad9dd850804e011c1d5997d382c4ec4e1999 Mon Sep 17 00:00:00 2001 From: veyron Date: Fri, 12 Jun 2026 23:23:18 +0530 Subject: [PATCH 180/199] Backport PR #19112 to branch v7.2.x (Fix BST index engine returning range query results in unsorted order) * Fix BST index engine returning range query results in unsorted order BST._range collected matching nodes in node-right-left order, so range queries were only sorted by accident when the tree was a degenerate right-chain (as built from pre-sorted index data). After add_row the tree branches and t.loc[lower:upper] returned rows in scrambled order, unlike the SortedArray and SCEngine engines. Switch _range and _same_prefix to an in-order traversal so matching nodes are collected in ascending key order. Add a regression test parametrized over all index engines. Fixes #19911 * Add changelog fragment for PR #19912 (cherry picked from commit ece161274a4ec6afefe4acd90ffd1ea07de5bdd2) --- astropy/table/bst.py | 12 ++++++++---- astropy/table/tests/test_index.py | 25 ++++++++++++------------- docs/changes/table/19912.bugfix.rst | 3 +++ 3 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 docs/changes/table/19912.bugfix.rst diff --git a/astropy/table/bst.py b/astropy/table/bst.py index 40f97ec68a8f..c7eeab6061b1 100644 --- a/astropy/table/bst.py +++ b/astropy/table/bst.py @@ -451,22 +451,26 @@ def same_prefix(self, val): return [x for node in nodes for x in node.data] def _range(self, lower, upper, op1, op2, node, lst): + # In-order traversal (left, node, right) so that matching nodes + # are collected in ascending key order. + if lower < node.key and node.left is not None: + self._range(lower, upper, op1, op2, node.left, lst) if op1(lower, node.key) and op2(upper, node.key): lst.append(node) if upper > node.key and node.right is not None: self._range(lower, upper, op1, op2, node.right, lst) - if lower < node.key and node.left is not None: - self._range(lower, upper, op1, op2, node.left, lst) return lst def _same_prefix(self, val, node, lst): + # In-order traversal (left, node, right) so that matching nodes + # are collected in ascending key order. prefix = node.key[: len(val)] + if prefix >= val and node.left is not None: + self._same_prefix(val, node.left, lst) if prefix == val: lst.append(node) if prefix <= val and node.right is not None: self._same_prefix(val, node.right, lst) - if prefix >= val and node.left is not None: - self._same_prefix(val, node.left, lst) return lst def __repr__(self): diff --git a/astropy/table/tests/test_index.py b/astropy/table/tests/test_index.py index dfc0f5f46df3..d4783a1ab201 100644 --- a/astropy/table/tests/test_index.py +++ b/astropy/table/tests/test_index.py @@ -885,20 +885,19 @@ def test_index_not_corrupted_on_failed_row_assignment(engine): t.loc[99] -@pytest.mark.parametrize("table_type", [Table, QTable]) -def test_loc_range_with_duplicate_index_values(engine, table_type): - """Regression test: a range query must return every row whose value equals - an inclusive bound, even when that value is duplicated in the index. +def test_loc_range_sorted_after_add_row(engine): + """Regression test: a range query must return rows in ascending key order, + also for rows added after the index was created. - The default ``SortedArray`` engine previously kept only the first matching - row, so e.g. ``t.loc[1:2]`` silently dropped all but one of the ``2`` rows. + The ``BST`` engine previously collected nodes in node-right-left order, + so after ``add_row`` the tree was no longer a degenerate chain and + ``t.loc[lower:upper]`` returned rows in scrambled order. """ - t = table_type() - t["a"] = [3, 2, 2, 3, 1, 2] - t["b"] = [30, 20, 21, 31, 10, 22] + t = Table([[1, 5, 9], [10, 50, 90]], names=("a", "b")) t.add_index("a", engine=engine) + for a, b in [(7, 70), (3, 30), (8, 80), (2, 20), (6, 60), (4, 40)]: + t.add_row((a, b)) - assert sorted(t.loc[1:2]["b"].tolist()) == [10, 20, 21, 22] - assert sorted(t.loc[2:2]["b"].tolist()) == [20, 21, 22] - assert sorted(t.loc[2:3]["b"].tolist()) == [20, 21, 22, 30, 31] - assert sorted(t.loc[:]["b"].tolist()) == [10, 20, 21, 22, 30, 31] + assert t.loc[2:8]["a"].tolist() == [2, 3, 4, 5, 6, 7, 8] + assert t.loc[2:8]["b"].tolist() == [20, 30, 40, 50, 60, 70, 80] + assert t.loc[:]["a"].tolist() == [1, 2, 3, 4, 5, 6, 7, 8, 9] diff --git a/docs/changes/table/19912.bugfix.rst b/docs/changes/table/19912.bugfix.rst new file mode 100644 index 000000000000..a337b77b3778 --- /dev/null +++ b/docs/changes/table/19912.bugfix.rst @@ -0,0 +1,3 @@ +Fixed a bug where a ``Table.loc[]`` range query using the ``BST`` index +engine returned rows in scrambled order if rows had been added after the +index was created. From 6d1068398a4b133e2a735f35e991496e1cf7fea8 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:12:12 -0400 Subject: [PATCH 181/199] Backport PR #19935: Add a pre-commit hook to normalize line endings in rst files --- .pre-commit-config.yaml | 4 + docs/coordinates/example_gallery_index.rst | 32 +- ...mple_gallery_plot_galactocentric_frame.rst | 348 ++++++------- ...ple_gallery_plot_mars_coordinate_frame.rst | 230 ++++----- .../example_gallery_plot_obs_planning.rst | 308 ++++++------ ...mple_gallery_plot_sgr_coordinate_frame.rst | 474 +++++++++--------- .../coordinates/example_gallery_rv_to_gsr.rst | 206 ++++---- 7 files changed, 803 insertions(+), 799 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95bdfd1e42c4..5362c8e31205 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,10 @@ repos: - id: end-of-file-fixer # Makes sure files end in a newline and only a newline. exclude: ".*(data.*|extern.*|licenses.*|_static.*|_parsetab.py)$" + - id: mixed-line-ending + args: ["--fix=lf"] + # Forces all text files to use LF line endings. + exclude: ".*(data.*|extern.*|licenses.*|_static.*|_parsetab.py)$" # - id: fix-encoding-pragma # covered by pyupgrade - id: trailing-whitespace # Trims trailing whitespace. diff --git a/docs/coordinates/example_gallery_index.rst b/docs/coordinates/example_gallery_index.rst index 92d11984d85a..115e29749892 100644 --- a/docs/coordinates/example_gallery_index.rst +++ b/docs/coordinates/example_gallery_index.rst @@ -1,16 +1,16 @@ -.. _astropy-coordinates-example-gallery: - -Example Gallery -*************** - -This gallery of examples shows a variety of relatively small snippets or -examples of tasks that can be done with the ``astropy.coordinates`` sub-package. - -.. toctree:: - :maxdepth: 1 - - example_gallery_plot_galactocentric_frame - example_gallery_plot_mars_coordinate_frame - example_gallery_plot_obs_planning - example_gallery_plot_sgr_coordinate_frame - example_gallery_rv_to_gsr +.. _astropy-coordinates-example-gallery: + +Example Gallery +*************** + +This gallery of examples shows a variety of relatively small snippets or +examples of tasks that can be done with the ``astropy.coordinates`` sub-package. + +.. toctree:: + :maxdepth: 1 + + example_gallery_plot_galactocentric_frame + example_gallery_plot_mars_coordinate_frame + example_gallery_plot_obs_planning + example_gallery_plot_sgr_coordinate_frame + example_gallery_rv_to_gsr diff --git a/docs/coordinates/example_gallery_plot_galactocentric_frame.rst b/docs/coordinates/example_gallery_plot_galactocentric_frame.rst index 9090c311002a..00e2a4555a95 100644 --- a/docs/coordinates/example_gallery_plot_galactocentric_frame.rst +++ b/docs/coordinates/example_gallery_plot_galactocentric_frame.rst @@ -1,174 +1,174 @@ -.. _sphx_glr_generated_examples_coordinates_plot_galactocentric-frame.py: - -Transforming positions and velocities to and from a Galactocentric frame -======================================================================== - -.. - EXAMPLE START - Transforming positions and velocities to and from a Galactocentric frames - -This example shows a few examples of how to use and customize the -`~astropy.coordinates.Galactocentric` frame to transform Heliocentric sky -positions, distance, proper motions, and radial velocities to a Galactocentric, -Cartesian frame, and the same in reverse. - -The main configurable parameters of the `~astropy.coordinates.Galactocentric` -frame control the position and velocity of the solar system barycenter within -the Galaxy. These are specified by setting the ICRS coordinates of the -Galactic center, the distance to the Galactic center (the sun-galactic center -line is always assumed to be the x-axis of the Galactocentric frame), and the -Cartesian 3-velocity of the sun in the Galactocentric frame. We will first -demonstrate how to customize these values, then show how to set the solar motion -instead by inputting the proper motion of Sgr A*. - -Note that, for brevity, we may refer to the solar system barycenter as just "the -sun" in the examples below. - -Let's first define a barycentric coordinate and velocity in the ICRS frame. -We will use the data for the star HD 39881 from the -`Simbad `_ database: - - ->>> import astropy.coordinates as coord ->>> from astropy import units as u ->>> c1 = coord.SkyCoord( -... ra=89.014303 * u.degree, -... dec=13.924912 * u.degree, -... distance=(37.59 * u.mas).to(u.pc, u.parallax()), -... pm_ra_cosdec=372.72 * (u.mas / u.yr), -... pm_dec=-483.69 * (u.mas / u.yr), -... radial_velocity=0.37 * (u.km / u.s), -... frame="icrs", -... ) - -This is a high proper-motion star; suppose we'd like to transform its position -and velocity to a Galactocentric frame to see if it has a large 3D velocity -as well. To use the Astropy default solar position and motion parameters, we -can do the following: - ->>> gc1 = c1.transform_to(coord.Galactocentric) - -From here, we can access the components of the resulting -`~astropy.coordinates.Galactocentric` instance to see the 3D Cartesian -velocity components: - ->>> print(gc1.v_x, gc1.v_y, gc1.v_z) # doctest: +FLOAT_CMP -30.254684717897074 km / s 171.29916086104885 km / s 18.19390627095307 km / s - -The default parameters for the `~astropy.coordinates.Galactocentric` frame -are detailed in the linked documentation, but we can modify the most commonly -changed values using the keywords ``galcen_distance``, ``galcen_v_sun``, and -``z_sun`` which set the sun-Galactic center distance, the 3D velocity vector -of the sun, and the height of the sun above the Galactic midplane, -respectively. The velocity of the sun can be specified as an -`~astropy.units.Quantity` object with velocity units and is interpreted as a -Cartesian velocity, as in the example below. Note that, as with the positions, -the Galactocentric frame is a right-handed system (i.e., the Sun is at negative -x values) so ``v_x`` is opposite of the Galactocentric radial velocity: - ->>> v_sun = [11.1, 244, 7.25] * (u.km / u.s) # [vx, vy, vz] ->>> gc_frame = coord.Galactocentric( -... galcen_distance=8 * u.kpc, galcen_v_sun=v_sun, z_sun=0 * u.pc -... ) - -We can then transform to this frame instead, with our custom parameters: - ->>> gc2 = c1.transform_to(gc_frame) ->>> print(gc2.v_x, gc2.v_y, gc2.v_z) # doctest: +FLOAT_CMP -28.427958360720748 km / s 169.69916086104888 km / s 17.70831652451455 km / s - -It is sometimes useful to specify the solar motion using the -`proper motion of Sgr A* `_ -instead of Cartesian velocity components. With an assumed distance, we can convert -proper motion components to Cartesian velocity components using `astropy.units`: - ->>> galcen_distance = 8 * u.kpc ->>> pm_gal_sgrA = [-6.379, -0.202] * (u.mas / u.yr) # from Reid & Brunthaler 2004 ->>> vy, vz = -(galcen_distance * pm_gal_sgrA).to(u.km / u.s, u.dimensionless_angles()) - -We still have to assume a line-of-sight velocity for the Galactic center, -which we will again take to be 11 km/s: - ->>> vx = 11.1 * (u.km / u.s) ->>> v_sun2 = u.Quantity([vx, vy, vz]) # List of Quantity -> a single Quantity ->>> gc_frame2 = coord.Galactocentric( -... galcen_distance=galcen_distance, galcen_v_sun=v_sun2, z_sun=0 * u.pc -... ) ->>> gc3 = c1.transform_to(gc_frame2) ->>> print(gc3.v_x, gc3.v_y, gc3.v_z) # doctest: +FLOAT_CMP -28.427958360720748 km / s 167.61484955608267 km / s 18.118916793584443 km / s - -The transformations also work in the opposite direction. This can be useful -for transforming simulated or theoretical data to observable quantities. As -an example, we will generate 4 theoretical circular orbits at different -Galactocentric radii with the same circular velocity, and transform them to -Heliocentric coordinates: - -.. plot:: - :include-source: - - >>> import matplotlib.pyplot as plt - >>> import numpy as np - >>> import astropy.coordinates as coord - >>> from astropy import units as u - >>> ring_distances = np.arange(10, 26, 5) * u.kpc - >>> circ_velocity = 220 * (u.km / u.s) - >>> phi_grid = np.linspace(90, 270, 512) * u.degree # grid of azimuths - >>> ring_rep = coord.CylindricalRepresentation( - ... rho=ring_distances[:, np.newaxis], - ... phi=phi_grid[np.newaxis], - ... z=np.zeros_like(ring_distances)[:, np.newaxis], - ... ) - >>> angular_velocity = (-circ_velocity / ring_distances).to( - ... u.mas / u.yr, u.dimensionless_angles() - ... ) - >>> ring_dif = coord.CylindricalDifferential( - ... d_rho=np.zeros(phi_grid.shape)[np.newaxis] * (u.km / u.s), - ... d_phi=angular_velocity[:, np.newaxis], - ... d_z=np.zeros(phi_grid.shape)[np.newaxis] * (u.km / u.s), - ... ) - >>> ring_rep = ring_rep.with_differentials(ring_dif) - >>> gc_rings = coord.SkyCoord(ring_rep, frame=coord.Galactocentric) - - First, let's visualize the geometry in Galactocentric coordinates. Here are - the positions and velocities of the rings; note that in the velocity plot, - the velocities of the 4 rings are identical and thus overlaid under the same - curve: - - >>> fig, axes = plt.subplots(1, 2, figsize=(12, 6)) - >>> axes[0].plot(gc_rings.x.T, gc_rings.y.T, marker="None", linewidth=3) # doctest: +IGNORE_OUTPUT - >>> axes[0].text(-8.0, 0, r"$\odot$", fontsize=20) # doctest: +IGNORE_OUTPUT - >>> axes[0].set_xlim(-30, 30) # doctest: +IGNORE_OUTPUT - >>> axes[0].set_ylim(-30, 30) # doctest: +IGNORE_OUTPUT - >>> axes[0].set_xlabel("$x$ [kpc]") # doctest: +IGNORE_OUTPUT - >>> axes[0].set_ylabel("$y$ [kpc]") # doctest: +IGNORE_OUTPUT - >>> axes[0].set_title("Positions") # doctest: +IGNORE_OUTPUT - >>> axes[1].plot(gc_rings.v_x.T, gc_rings.v_y.T, marker="None", linewidth=3) # doctest: +IGNORE_OUTPUT - >>> axes[1].set_xlim(-250, 250) # doctest: +IGNORE_OUTPUT - >>> axes[1].set_ylim(-250, 250) # doctest: +IGNORE_OUTPUT - >>> axes[1].set_xlabel(f"$v_x$ [{(u.km / u.s).to_string('latex_inline')}]") # doctest: +IGNORE_OUTPUT - >>> axes[1].set_ylabel(f"$v_y$ [{(u.km / u.s).to_string('latex_inline')}]") # doctest: +IGNORE_OUTPUT - >>> axes[1].set_title("Velocities") # doctest: +IGNORE_OUTPUT - >>> fig.tight_layout() - - Now we can transform to Galactic coordinates and visualize the rings in - observable coordinates: - - >>> gal_rings = gc_rings.transform_to(coord.Galactic) - >>> fig, ax = plt.subplots(1, 1, figsize=(8, 6)) - >>> for i in range(len(ring_distances)): - ... ax.plot( - ... gal_rings[i].l.degree, - ... gal_rings[i].pm_l_cosb.value, - ... label=str(ring_distances[i]), - ... marker="None", - ... linewidth=3, - ... ) # doctest: +IGNORE_OUTPUT - >>> ax.set_xlim(360, 0) # doctest: +IGNORE_OUTPUT - >>> ax.set_xlabel("$l$ [deg]") # doctest: +IGNORE_OUTPUT - >>> ax.set_ylabel(rf'$\mu_l \, \cos b$ [{(u.mas/u.yr).to_string("latex_inline")}]') # doctest: +IGNORE_OUTPUT - >>> ax.legend() # doctest: +IGNORE_OUTPUT - >>> plt.draw() - -.. - EXAMPLE END +.. _sphx_glr_generated_examples_coordinates_plot_galactocentric-frame.py: + +Transforming positions and velocities to and from a Galactocentric frame +======================================================================== + +.. + EXAMPLE START + Transforming positions and velocities to and from a Galactocentric frames + +This example shows a few examples of how to use and customize the +`~astropy.coordinates.Galactocentric` frame to transform Heliocentric sky +positions, distance, proper motions, and radial velocities to a Galactocentric, +Cartesian frame, and the same in reverse. + +The main configurable parameters of the `~astropy.coordinates.Galactocentric` +frame control the position and velocity of the solar system barycenter within +the Galaxy. These are specified by setting the ICRS coordinates of the +Galactic center, the distance to the Galactic center (the sun-galactic center +line is always assumed to be the x-axis of the Galactocentric frame), and the +Cartesian 3-velocity of the sun in the Galactocentric frame. We will first +demonstrate how to customize these values, then show how to set the solar motion +instead by inputting the proper motion of Sgr A*. + +Note that, for brevity, we may refer to the solar system barycenter as just "the +sun" in the examples below. + +Let's first define a barycentric coordinate and velocity in the ICRS frame. +We will use the data for the star HD 39881 from the +`Simbad `_ database: + + +>>> import astropy.coordinates as coord +>>> from astropy import units as u +>>> c1 = coord.SkyCoord( +... ra=89.014303 * u.degree, +... dec=13.924912 * u.degree, +... distance=(37.59 * u.mas).to(u.pc, u.parallax()), +... pm_ra_cosdec=372.72 * (u.mas / u.yr), +... pm_dec=-483.69 * (u.mas / u.yr), +... radial_velocity=0.37 * (u.km / u.s), +... frame="icrs", +... ) + +This is a high proper-motion star; suppose we'd like to transform its position +and velocity to a Galactocentric frame to see if it has a large 3D velocity +as well. To use the Astropy default solar position and motion parameters, we +can do the following: + +>>> gc1 = c1.transform_to(coord.Galactocentric) + +From here, we can access the components of the resulting +`~astropy.coordinates.Galactocentric` instance to see the 3D Cartesian +velocity components: + +>>> print(gc1.v_x, gc1.v_y, gc1.v_z) # doctest: +FLOAT_CMP +30.254684717897074 km / s 171.29916086104885 km / s 18.19390627095307 km / s + +The default parameters for the `~astropy.coordinates.Galactocentric` frame +are detailed in the linked documentation, but we can modify the most commonly +changed values using the keywords ``galcen_distance``, ``galcen_v_sun``, and +``z_sun`` which set the sun-Galactic center distance, the 3D velocity vector +of the sun, and the height of the sun above the Galactic midplane, +respectively. The velocity of the sun can be specified as an +`~astropy.units.Quantity` object with velocity units and is interpreted as a +Cartesian velocity, as in the example below. Note that, as with the positions, +the Galactocentric frame is a right-handed system (i.e., the Sun is at negative +x values) so ``v_x`` is opposite of the Galactocentric radial velocity: + +>>> v_sun = [11.1, 244, 7.25] * (u.km / u.s) # [vx, vy, vz] +>>> gc_frame = coord.Galactocentric( +... galcen_distance=8 * u.kpc, galcen_v_sun=v_sun, z_sun=0 * u.pc +... ) + +We can then transform to this frame instead, with our custom parameters: + +>>> gc2 = c1.transform_to(gc_frame) +>>> print(gc2.v_x, gc2.v_y, gc2.v_z) # doctest: +FLOAT_CMP +28.427958360720748 km / s 169.69916086104888 km / s 17.70831652451455 km / s + +It is sometimes useful to specify the solar motion using the +`proper motion of Sgr A* `_ +instead of Cartesian velocity components. With an assumed distance, we can convert +proper motion components to Cartesian velocity components using `astropy.units`: + +>>> galcen_distance = 8 * u.kpc +>>> pm_gal_sgrA = [-6.379, -0.202] * (u.mas / u.yr) # from Reid & Brunthaler 2004 +>>> vy, vz = -(galcen_distance * pm_gal_sgrA).to(u.km / u.s, u.dimensionless_angles()) + +We still have to assume a line-of-sight velocity for the Galactic center, +which we will again take to be 11 km/s: + +>>> vx = 11.1 * (u.km / u.s) +>>> v_sun2 = u.Quantity([vx, vy, vz]) # List of Quantity -> a single Quantity +>>> gc_frame2 = coord.Galactocentric( +... galcen_distance=galcen_distance, galcen_v_sun=v_sun2, z_sun=0 * u.pc +... ) +>>> gc3 = c1.transform_to(gc_frame2) +>>> print(gc3.v_x, gc3.v_y, gc3.v_z) # doctest: +FLOAT_CMP +28.427958360720748 km / s 167.61484955608267 km / s 18.118916793584443 km / s + +The transformations also work in the opposite direction. This can be useful +for transforming simulated or theoretical data to observable quantities. As +an example, we will generate 4 theoretical circular orbits at different +Galactocentric radii with the same circular velocity, and transform them to +Heliocentric coordinates: + +.. plot:: + :include-source: + + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> import astropy.coordinates as coord + >>> from astropy import units as u + >>> ring_distances = np.arange(10, 26, 5) * u.kpc + >>> circ_velocity = 220 * (u.km / u.s) + >>> phi_grid = np.linspace(90, 270, 512) * u.degree # grid of azimuths + >>> ring_rep = coord.CylindricalRepresentation( + ... rho=ring_distances[:, np.newaxis], + ... phi=phi_grid[np.newaxis], + ... z=np.zeros_like(ring_distances)[:, np.newaxis], + ... ) + >>> angular_velocity = (-circ_velocity / ring_distances).to( + ... u.mas / u.yr, u.dimensionless_angles() + ... ) + >>> ring_dif = coord.CylindricalDifferential( + ... d_rho=np.zeros(phi_grid.shape)[np.newaxis] * (u.km / u.s), + ... d_phi=angular_velocity[:, np.newaxis], + ... d_z=np.zeros(phi_grid.shape)[np.newaxis] * (u.km / u.s), + ... ) + >>> ring_rep = ring_rep.with_differentials(ring_dif) + >>> gc_rings = coord.SkyCoord(ring_rep, frame=coord.Galactocentric) + + First, let's visualize the geometry in Galactocentric coordinates. Here are + the positions and velocities of the rings; note that in the velocity plot, + the velocities of the 4 rings are identical and thus overlaid under the same + curve: + + >>> fig, axes = plt.subplots(1, 2, figsize=(12, 6)) + >>> axes[0].plot(gc_rings.x.T, gc_rings.y.T, marker="None", linewidth=3) # doctest: +IGNORE_OUTPUT + >>> axes[0].text(-8.0, 0, r"$\odot$", fontsize=20) # doctest: +IGNORE_OUTPUT + >>> axes[0].set_xlim(-30, 30) # doctest: +IGNORE_OUTPUT + >>> axes[0].set_ylim(-30, 30) # doctest: +IGNORE_OUTPUT + >>> axes[0].set_xlabel("$x$ [kpc]") # doctest: +IGNORE_OUTPUT + >>> axes[0].set_ylabel("$y$ [kpc]") # doctest: +IGNORE_OUTPUT + >>> axes[0].set_title("Positions") # doctest: +IGNORE_OUTPUT + >>> axes[1].plot(gc_rings.v_x.T, gc_rings.v_y.T, marker="None", linewidth=3) # doctest: +IGNORE_OUTPUT + >>> axes[1].set_xlim(-250, 250) # doctest: +IGNORE_OUTPUT + >>> axes[1].set_ylim(-250, 250) # doctest: +IGNORE_OUTPUT + >>> axes[1].set_xlabel(f"$v_x$ [{(u.km / u.s).to_string('latex_inline')}]") # doctest: +IGNORE_OUTPUT + >>> axes[1].set_ylabel(f"$v_y$ [{(u.km / u.s).to_string('latex_inline')}]") # doctest: +IGNORE_OUTPUT + >>> axes[1].set_title("Velocities") # doctest: +IGNORE_OUTPUT + >>> fig.tight_layout() + + Now we can transform to Galactic coordinates and visualize the rings in + observable coordinates: + + >>> gal_rings = gc_rings.transform_to(coord.Galactic) + >>> fig, ax = plt.subplots(1, 1, figsize=(8, 6)) + >>> for i in range(len(ring_distances)): + ... ax.plot( + ... gal_rings[i].l.degree, + ... gal_rings[i].pm_l_cosb.value, + ... label=str(ring_distances[i]), + ... marker="None", + ... linewidth=3, + ... ) # doctest: +IGNORE_OUTPUT + >>> ax.set_xlim(360, 0) # doctest: +IGNORE_OUTPUT + >>> ax.set_xlabel("$l$ [deg]") # doctest: +IGNORE_OUTPUT + >>> ax.set_ylabel(rf'$\mu_l \, \cos b$ [{(u.mas/u.yr).to_string("latex_inline")}]') # doctest: +IGNORE_OUTPUT + >>> ax.legend() # doctest: +IGNORE_OUTPUT + >>> plt.draw() + +.. + EXAMPLE END diff --git a/docs/coordinates/example_gallery_plot_mars_coordinate_frame.rst b/docs/coordinates/example_gallery_plot_mars_coordinate_frame.rst index 987bb6aa3379..7eee3fab201b 100644 --- a/docs/coordinates/example_gallery_plot_mars_coordinate_frame.rst +++ b/docs/coordinates/example_gallery_plot_mars_coordinate_frame.rst @@ -1,115 +1,115 @@ -.. _sphx_glr_generated_examples_coordinates_plot_mars-coordinate-frame.py: - -Create a new coordinate frame class for Mars -============================================ - -.. - EXAMPLE START - Create a new coordinate frame class for Mars - -This example describes how to subclass and define a custom coordinate frame for a -planetary body which can be described by a geodetic or bodycentric representation, -as discussed in :ref:`astropy:astropy-coordinates-design` and -:ref:`astropy-coordinates-create-geodetic`. - -Note that we use the frame here only to store coordinates. To use it to determine, e.g., -where to point a telescope on Earth to observe Olympus Mons, one would need to add the -frame to the transfer graph, which is beyond the scope of this example. - -To do this, first we need to define a subclass of a -`~astropy.coordinates.BaseGeodeticRepresentation` and -`~astropy.coordinates.BaseBodycentricRepresentation`, then a subclass of -`~astropy.coordinates.BaseCoordinateFrame` using the previous defined -representations. - -.. plot:: - :include-source: - - >>> import matplotlib.pyplot as plt - >>> import numpy as np - >>> from astropy import units as u - >>> from astropy.coordinates.baseframe import BaseCoordinateFrame - >>> from astropy.coordinates.representation import CartesianRepresentation - >>> from astropy.coordinates.representation.geodetic import ( - ... BaseBodycentricRepresentation, - ... BaseGeodeticRepresentation, - ... ) - >>> from astropy.visualization import quantity_support - - The first step is to create a new class, and make it a subclass of - `~astropy.coordinates.BaseGeodeticRepresentation`. - Geodetic latitudes are used and longitudes span from 0 to 360 degrees east positive - It represent a best fit of the Mars spheroid to the martian geoid (areoid): - - >>> class MarsBestFitAeroid(BaseGeodeticRepresentation): - ... """A Spheroidal representation of Mars that minimized deviations with respect to the - ... areoid following - ... Ardalan A. A, R. Karimi, and E. W. Grafarend (2010) - ... https://doi.org/10.1007/s11038-009-9342-7 - ... """ - ... _equatorial_radius = 3395.4280 * u.km - ... _flattening = 0.5227617843759314 * u.percent - - Now let's define a new geodetic representation obtained from MarsBestFitAeroid but - described by planetocentric latitudes: - - >>> class MarsBestFitOcentricAeroid(BaseBodycentricRepresentation): - ... """A Spheroidal planetocentric representation of Mars that minimized deviations with - ... respect to the areoid following - ... Ardalan A. A, R. Karimi, and E. W. Grafarend (2010) - ... https://doi.org/10.1007/s11038-009-9342-7 - ... """ - ... _equatorial_radius = 3395.4280 * u.km - ... _flattening = 0.5227617843759314 * u.percent - - As a comparison we define a new spherical frame representation, we could - have based it on `~astropy.coordinates.BaseBodycentricRepresentation` too: - - >>> class MarsSphere(BaseGeodeticRepresentation): - ... """A Spherical representation of Mars.""" - ... _equatorial_radius = 3395.4280 * u.km - ... _flattening = 0.0 * u.percent - - The new planetary body-fixed reference system will be described using the - previous defined representations: - - >>> class MarsCoordinateFrame(BaseCoordinateFrame): - ... """A reference system for Mars.""" - ... name = "Mars" - - Now we plot the differences between each component of the cartesian - representation with respect to the spherical model, assuming the point on the - surface of the body (``height = 0``): - - >>> mars_sphere = MarsCoordinateFrame( - ... lon=np.linspace(0, 360, 128) * u.deg, - ... lat=np.linspace(-90, 90, 128) * u.deg, - ... representation_type=MarsSphere, - ... ) - >>> mars = MarsCoordinateFrame( - ... lon=np.linspace(0, 360, 128) * u.deg, - ... lat=np.linspace(-90, 90, 128) * u.deg, - ... representation_type=MarsBestFitAeroid, - ... ) - >>> mars_ocentric = MarsCoordinateFrame( - ... lon=np.linspace(0, 360, 128) * u.deg, - ... lat=np.linspace(-90, 90, 128) * u.deg, - ... representation_type=MarsBestFitOcentricAeroid, - ... ) - >>> xyz_sphere = mars_sphere.represent_as(CartesianRepresentation) - >>> xyz = mars.represent_as(CartesianRepresentation) - >>> xyz_ocentric = mars_ocentric.represent_as(CartesianRepresentation) - >>> with quantity_support(): - ... fig, ax = plt.subplots(2, subplot_kw={"projection": "3d"}) - ... ax[0].scatter(*((xyz - xyz_sphere).xyz << u.km)) # doctest: +IGNORE_OUTPUT - ... ax[0].tick_params(labelsize=8) # doctest: +IGNORE_OUTPUT - ... ax[0].set(xlabel="x [km]", ylabel="y [km]", zlabel="z [km]") # doctest: +IGNORE_OUTPUT - ... ax[0].set_title("Mars-odetic spheroid difference from sphere") # doctest: +IGNORE_OUTPUT - ... ax[1].scatter(*((xyz_ocentric - xyz_sphere).xyz << u.km)) # doctest: +IGNORE_OUTPUT - ... ax[1].tick_params(labelsize=8) # doctest: +IGNORE_OUTPUT - ... ax[1].set(xlabel="x [km]", ylabel="y [km]", zlabel="z [km]") # doctest: +IGNORE_OUTPUT - ... ax[1].set_title("Mars-ocentric spheroid difference from sphere") # doctest: +IGNORE_OUTPUT - ... plt.draw() - -.. - EXAMPLE END +.. _sphx_glr_generated_examples_coordinates_plot_mars-coordinate-frame.py: + +Create a new coordinate frame class for Mars +============================================ + +.. + EXAMPLE START + Create a new coordinate frame class for Mars + +This example describes how to subclass and define a custom coordinate frame for a +planetary body which can be described by a geodetic or bodycentric representation, +as discussed in :ref:`astropy:astropy-coordinates-design` and +:ref:`astropy-coordinates-create-geodetic`. + +Note that we use the frame here only to store coordinates. To use it to determine, e.g., +where to point a telescope on Earth to observe Olympus Mons, one would need to add the +frame to the transfer graph, which is beyond the scope of this example. + +To do this, first we need to define a subclass of a +`~astropy.coordinates.BaseGeodeticRepresentation` and +`~astropy.coordinates.BaseBodycentricRepresentation`, then a subclass of +`~astropy.coordinates.BaseCoordinateFrame` using the previous defined +representations. + +.. plot:: + :include-source: + + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> from astropy import units as u + >>> from astropy.coordinates.baseframe import BaseCoordinateFrame + >>> from astropy.coordinates.representation import CartesianRepresentation + >>> from astropy.coordinates.representation.geodetic import ( + ... BaseBodycentricRepresentation, + ... BaseGeodeticRepresentation, + ... ) + >>> from astropy.visualization import quantity_support + + The first step is to create a new class, and make it a subclass of + `~astropy.coordinates.BaseGeodeticRepresentation`. + Geodetic latitudes are used and longitudes span from 0 to 360 degrees east positive + It represent a best fit of the Mars spheroid to the martian geoid (areoid): + + >>> class MarsBestFitAeroid(BaseGeodeticRepresentation): + ... """A Spheroidal representation of Mars that minimized deviations with respect to the + ... areoid following + ... Ardalan A. A, R. Karimi, and E. W. Grafarend (2010) + ... https://doi.org/10.1007/s11038-009-9342-7 + ... """ + ... _equatorial_radius = 3395.4280 * u.km + ... _flattening = 0.5227617843759314 * u.percent + + Now let's define a new geodetic representation obtained from MarsBestFitAeroid but + described by planetocentric latitudes: + + >>> class MarsBestFitOcentricAeroid(BaseBodycentricRepresentation): + ... """A Spheroidal planetocentric representation of Mars that minimized deviations with + ... respect to the areoid following + ... Ardalan A. A, R. Karimi, and E. W. Grafarend (2010) + ... https://doi.org/10.1007/s11038-009-9342-7 + ... """ + ... _equatorial_radius = 3395.4280 * u.km + ... _flattening = 0.5227617843759314 * u.percent + + As a comparison we define a new spherical frame representation, we could + have based it on `~astropy.coordinates.BaseBodycentricRepresentation` too: + + >>> class MarsSphere(BaseGeodeticRepresentation): + ... """A Spherical representation of Mars.""" + ... _equatorial_radius = 3395.4280 * u.km + ... _flattening = 0.0 * u.percent + + The new planetary body-fixed reference system will be described using the + previous defined representations: + + >>> class MarsCoordinateFrame(BaseCoordinateFrame): + ... """A reference system for Mars.""" + ... name = "Mars" + + Now we plot the differences between each component of the cartesian + representation with respect to the spherical model, assuming the point on the + surface of the body (``height = 0``): + + >>> mars_sphere = MarsCoordinateFrame( + ... lon=np.linspace(0, 360, 128) * u.deg, + ... lat=np.linspace(-90, 90, 128) * u.deg, + ... representation_type=MarsSphere, + ... ) + >>> mars = MarsCoordinateFrame( + ... lon=np.linspace(0, 360, 128) * u.deg, + ... lat=np.linspace(-90, 90, 128) * u.deg, + ... representation_type=MarsBestFitAeroid, + ... ) + >>> mars_ocentric = MarsCoordinateFrame( + ... lon=np.linspace(0, 360, 128) * u.deg, + ... lat=np.linspace(-90, 90, 128) * u.deg, + ... representation_type=MarsBestFitOcentricAeroid, + ... ) + >>> xyz_sphere = mars_sphere.represent_as(CartesianRepresentation) + >>> xyz = mars.represent_as(CartesianRepresentation) + >>> xyz_ocentric = mars_ocentric.represent_as(CartesianRepresentation) + >>> with quantity_support(): + ... fig, ax = plt.subplots(2, subplot_kw={"projection": "3d"}) + ... ax[0].scatter(*((xyz - xyz_sphere).xyz << u.km)) # doctest: +IGNORE_OUTPUT + ... ax[0].tick_params(labelsize=8) # doctest: +IGNORE_OUTPUT + ... ax[0].set(xlabel="x [km]", ylabel="y [km]", zlabel="z [km]") # doctest: +IGNORE_OUTPUT + ... ax[0].set_title("Mars-odetic spheroid difference from sphere") # doctest: +IGNORE_OUTPUT + ... ax[1].scatter(*((xyz_ocentric - xyz_sphere).xyz << u.km)) # doctest: +IGNORE_OUTPUT + ... ax[1].tick_params(labelsize=8) # doctest: +IGNORE_OUTPUT + ... ax[1].set(xlabel="x [km]", ylabel="y [km]", zlabel="z [km]") # doctest: +IGNORE_OUTPUT + ... ax[1].set_title("Mars-ocentric spheroid difference from sphere") # doctest: +IGNORE_OUTPUT + ... plt.draw() + +.. + EXAMPLE END diff --git a/docs/coordinates/example_gallery_plot_obs_planning.rst b/docs/coordinates/example_gallery_plot_obs_planning.rst index fd361fb81cc1..b5c7a6e9550b 100644 --- a/docs/coordinates/example_gallery_plot_obs_planning.rst +++ b/docs/coordinates/example_gallery_plot_obs_planning.rst @@ -1,154 +1,154 @@ -.. _sphx_glr_generated_examples_coordinates_plot_obs-planning.py: - -Determining and plotting the altitude/azimuth of a celestial object -=================================================================== - -.. - EXAMPLE START - Determining and plotting the altitude/azimuth of a celestial object - -This example demonstrates coordinate transformations and the creation of -visibility curves to assist with observing run planning. - -In this example, we make a `~astropy.coordinates.SkyCoord` instance for M33. -The altitude-azimuth coordinates are then found using -`astropy.coordinates.EarthLocation` and `astropy.time.Time` objects. - -This example is meant to demonstrate the capabilities of the -`astropy.coordinates` package. For more convenient and/or complex observation -planning, consider the `astroplan `_ -package. - -Let's suppose you are planning to visit picturesque Bear Mountain State Park -in New York, USA. You are bringing your telescope with you (of course), and -someone told you M33 is a great target to observe there. You happen to know -you are free at 11:00 PM local time, and you want to know if it will be up. -Astropy can answer that. - -.. plot:: - :include-source: - - >>> import matplotlib.pyplot as plt - >>> import numpy as np - >>> from astropy import units as u - >>> from astropy.coordinates import AltAz, EarthLocation, SkyCoord, get_body, get_sun - >>> from astropy.time import Time - >>> from astropy.visualization import quantity_support - - :meth:`astropy.coordinates.SkyCoord.from_name` uses Simbad to resolve object - names and retrieve coordinates. - - Get the coordinates of M33: - - >>> # m33 = SkyCoord.from_name("M33") - >>> m33 = SkyCoord(23.46206906, 30.66017511, unit="deg") - - Use `astropy.coordinates.EarthLocation` to provide the location of Bear - Mountain and set the time to 11pm Eastern Daylight Time (EDT) on 2012 July 12: - - >>> bear_mountain = EarthLocation(lat=41.3 * u.deg, lon=-74 * u.deg, height=390 * u.m) - >>> utcoffset = -4 * u.hour # EDT - >>> time = Time("2012-7-12 23:00:00") - utcoffset - - :meth:`astropy.coordinates.EarthLocation.get_site_names` can be used to get - locations of major observatories. - - Use `astropy.coordinates` to find the Alt, Az coordinates of M33 at as - observed from Bear Mountain at 11pm on 2012 July 12: - - >>> m33altaz = m33.transform_to(AltAz(obstime=time, location=bear_mountain)) - >>> print(f"M33's Altitude = {m33altaz.alt:.2}") - M33's Altitude = 0.13 deg - - This is helpful since it turns out M33 is barely above the horizon at this - time. It is more informative to find M33's airmass over the course of - the night. - - Find the Alt, Az coordinates of M33 at 100 times evenly spaced between 10 PM - and 7 AM EDT: - - >>> midnight = Time("2012-7-13 00:00:00") - utcoffset - >>> delta_midnight = np.linspace(-2, 10, 100) * u.hour - >>> frame_July13night = AltAz(obstime=midnight + delta_midnight, location=bear_mountain) - >>> m33altazs_July13night = m33.transform_to(frame_July13night) - - Convert Alt, Az to airmass with `~astropy.coordinates.AltAz.secz` attribute: - - >>> m33airmasss_July13night = m33altazs_July13night.secz - - Plot the airmass as a function of time: - - >>> with quantity_support(): - ... fig, ax = plt.subplots(1, 1, figsize=(12, 6)) - ... ax.plot(delta_midnight, m33airmasss_July13night) # doctest: +IGNORE_OUTPUT - ... ax.set_xlim(-2, 10) # doctest: +IGNORE_OUTPUT - ... ax.set_ylim(1, 4) # doctest: +IGNORE_OUTPUT - ... ax.set_xlabel("Hours from EDT Midnight") # doctest: +IGNORE_OUTPUT - ... ax.set_ylabel("Airmass [Sec(z)]") # doctest: +IGNORE_OUTPUT - ... plt.draw() - - Use :func:`~astropy.coordinates.get_sun` to find the location of the Sun at 1000 - evenly spaced times between noon on July 12 and noon on July 13: - - >>> delta_midnight = np.linspace(-12, 12, 1000) * u.hour - >>> times_July12_to_13 = midnight + delta_midnight - >>> frame_July12_to_13 = AltAz(obstime=times_July12_to_13, location=bear_mountain) - >>> sunaltazs_July12_to_13 = get_sun(times_July12_to_13).transform_to(frame_July12_to_13) - - Do the same with :func:`~astropy.coordinates.get_body` to find when the moon is - up. Be aware that this will need to download a 10 MB file from the internet - to get a precise location of the moon. - - >>> moon_July12_to_13 = get_body("moon", times_July12_to_13) - >>> moonaltazs_July12_to_13 = moon_July12_to_13.transform_to(frame_July12_to_13) - - Find the Alt, Az coordinates of M33 at those same times: - - >>> m33altazs_July12_to_13 = m33.transform_to(frame_July12_to_13) - - Make a figure illustrating nighttime and the altitudes of M33 and - the Sun over that time: - - >>> with quantity_support(): - ... fig, ax = plt.subplots(1, 1, figsize=(12, 6)) - ... ax.plot(delta_midnight, sunaltazs_July12_to_13.alt, color="r", label="Sun") # doctest: +IGNORE_OUTPUT - ... ax.plot( - ... delta_midnight, moonaltazs_July12_to_13.alt, color=[0.75] * 3, ls="--", label="Moon" - ... ) # doctest: +IGNORE_OUTPUT - ... mappable = ax.scatter( - ... delta_midnight, - ... m33altazs_July12_to_13.alt, - ... c=m33altazs_July12_to_13.az.value, - ... label="M33", - ... lw=0, - ... s=8, - ... cmap="viridis", - ... ) - ... ax.fill_between( - ... delta_midnight, - ... 0 * u.deg, - ... 90 * u.deg, - ... sunaltazs_July12_to_13.alt < (-0 * u.deg), - ... color="0.5", - ... zorder=0, - ... ) # doctest: +IGNORE_OUTPUT - ... ax.fill_between( - ... delta_midnight, - ... 0 * u.deg, - ... 90 * u.deg, - ... sunaltazs_July12_to_13.alt < (-18 * u.deg), - ... color="k", - ... zorder=0, - ... ) # doctest: +IGNORE_OUTPUT - ... fig.colorbar(mappable).set_label("Azimuth [deg]") # doctest: +IGNORE_OUTPUT - ... ax.legend(loc="upper left") # doctest: +IGNORE_OUTPUT - ... ax.set_xlim(-12 * u.hour, 12 * u.hour) # doctest: +IGNORE_OUTPUT - ... ax.set_xticks((np.arange(13) * 2 - 12) * u.hour) # doctest: +IGNORE_OUTPUT - ... ax.set_ylim(0 * u.deg, 90 * u.deg) # doctest: +IGNORE_OUTPUT - ... ax.set_xlabel("Hours from EDT Midnight") # doctest: +IGNORE_OUTPUT - ... ax.set_ylabel("Altitude [deg]") # doctest: +IGNORE_OUTPUT - ... ax.grid(visible=True) # doctest: +IGNORE_OUTPUT - ... plt.draw() - -.. - EXAMPLE END +.. _sphx_glr_generated_examples_coordinates_plot_obs-planning.py: + +Determining and plotting the altitude/azimuth of a celestial object +=================================================================== + +.. + EXAMPLE START + Determining and plotting the altitude/azimuth of a celestial object + +This example demonstrates coordinate transformations and the creation of +visibility curves to assist with observing run planning. + +In this example, we make a `~astropy.coordinates.SkyCoord` instance for M33. +The altitude-azimuth coordinates are then found using +`astropy.coordinates.EarthLocation` and `astropy.time.Time` objects. + +This example is meant to demonstrate the capabilities of the +`astropy.coordinates` package. For more convenient and/or complex observation +planning, consider the `astroplan `_ +package. + +Let's suppose you are planning to visit picturesque Bear Mountain State Park +in New York, USA. You are bringing your telescope with you (of course), and +someone told you M33 is a great target to observe there. You happen to know +you are free at 11:00 PM local time, and you want to know if it will be up. +Astropy can answer that. + +.. plot:: + :include-source: + + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> from astropy import units as u + >>> from astropy.coordinates import AltAz, EarthLocation, SkyCoord, get_body, get_sun + >>> from astropy.time import Time + >>> from astropy.visualization import quantity_support + + :meth:`astropy.coordinates.SkyCoord.from_name` uses Simbad to resolve object + names and retrieve coordinates. + + Get the coordinates of M33: + + >>> # m33 = SkyCoord.from_name("M33") + >>> m33 = SkyCoord(23.46206906, 30.66017511, unit="deg") + + Use `astropy.coordinates.EarthLocation` to provide the location of Bear + Mountain and set the time to 11pm Eastern Daylight Time (EDT) on 2012 July 12: + + >>> bear_mountain = EarthLocation(lat=41.3 * u.deg, lon=-74 * u.deg, height=390 * u.m) + >>> utcoffset = -4 * u.hour # EDT + >>> time = Time("2012-7-12 23:00:00") - utcoffset + + :meth:`astropy.coordinates.EarthLocation.get_site_names` can be used to get + locations of major observatories. + + Use `astropy.coordinates` to find the Alt, Az coordinates of M33 at as + observed from Bear Mountain at 11pm on 2012 July 12: + + >>> m33altaz = m33.transform_to(AltAz(obstime=time, location=bear_mountain)) + >>> print(f"M33's Altitude = {m33altaz.alt:.2}") + M33's Altitude = 0.13 deg + + This is helpful since it turns out M33 is barely above the horizon at this + time. It is more informative to find M33's airmass over the course of + the night. + + Find the Alt, Az coordinates of M33 at 100 times evenly spaced between 10 PM + and 7 AM EDT: + + >>> midnight = Time("2012-7-13 00:00:00") - utcoffset + >>> delta_midnight = np.linspace(-2, 10, 100) * u.hour + >>> frame_July13night = AltAz(obstime=midnight + delta_midnight, location=bear_mountain) + >>> m33altazs_July13night = m33.transform_to(frame_July13night) + + Convert Alt, Az to airmass with `~astropy.coordinates.AltAz.secz` attribute: + + >>> m33airmasss_July13night = m33altazs_July13night.secz + + Plot the airmass as a function of time: + + >>> with quantity_support(): + ... fig, ax = plt.subplots(1, 1, figsize=(12, 6)) + ... ax.plot(delta_midnight, m33airmasss_July13night) # doctest: +IGNORE_OUTPUT + ... ax.set_xlim(-2, 10) # doctest: +IGNORE_OUTPUT + ... ax.set_ylim(1, 4) # doctest: +IGNORE_OUTPUT + ... ax.set_xlabel("Hours from EDT Midnight") # doctest: +IGNORE_OUTPUT + ... ax.set_ylabel("Airmass [Sec(z)]") # doctest: +IGNORE_OUTPUT + ... plt.draw() + + Use :func:`~astropy.coordinates.get_sun` to find the location of the Sun at 1000 + evenly spaced times between noon on July 12 and noon on July 13: + + >>> delta_midnight = np.linspace(-12, 12, 1000) * u.hour + >>> times_July12_to_13 = midnight + delta_midnight + >>> frame_July12_to_13 = AltAz(obstime=times_July12_to_13, location=bear_mountain) + >>> sunaltazs_July12_to_13 = get_sun(times_July12_to_13).transform_to(frame_July12_to_13) + + Do the same with :func:`~astropy.coordinates.get_body` to find when the moon is + up. Be aware that this will need to download a 10 MB file from the internet + to get a precise location of the moon. + + >>> moon_July12_to_13 = get_body("moon", times_July12_to_13) + >>> moonaltazs_July12_to_13 = moon_July12_to_13.transform_to(frame_July12_to_13) + + Find the Alt, Az coordinates of M33 at those same times: + + >>> m33altazs_July12_to_13 = m33.transform_to(frame_July12_to_13) + + Make a figure illustrating nighttime and the altitudes of M33 and + the Sun over that time: + + >>> with quantity_support(): + ... fig, ax = plt.subplots(1, 1, figsize=(12, 6)) + ... ax.plot(delta_midnight, sunaltazs_July12_to_13.alt, color="r", label="Sun") # doctest: +IGNORE_OUTPUT + ... ax.plot( + ... delta_midnight, moonaltazs_July12_to_13.alt, color=[0.75] * 3, ls="--", label="Moon" + ... ) # doctest: +IGNORE_OUTPUT + ... mappable = ax.scatter( + ... delta_midnight, + ... m33altazs_July12_to_13.alt, + ... c=m33altazs_July12_to_13.az.value, + ... label="M33", + ... lw=0, + ... s=8, + ... cmap="viridis", + ... ) + ... ax.fill_between( + ... delta_midnight, + ... 0 * u.deg, + ... 90 * u.deg, + ... sunaltazs_July12_to_13.alt < (-0 * u.deg), + ... color="0.5", + ... zorder=0, + ... ) # doctest: +IGNORE_OUTPUT + ... ax.fill_between( + ... delta_midnight, + ... 0 * u.deg, + ... 90 * u.deg, + ... sunaltazs_July12_to_13.alt < (-18 * u.deg), + ... color="k", + ... zorder=0, + ... ) # doctest: +IGNORE_OUTPUT + ... fig.colorbar(mappable).set_label("Azimuth [deg]") # doctest: +IGNORE_OUTPUT + ... ax.legend(loc="upper left") # doctest: +IGNORE_OUTPUT + ... ax.set_xlim(-12 * u.hour, 12 * u.hour) # doctest: +IGNORE_OUTPUT + ... ax.set_xticks((np.arange(13) * 2 - 12) * u.hour) # doctest: +IGNORE_OUTPUT + ... ax.set_ylim(0 * u.deg, 90 * u.deg) # doctest: +IGNORE_OUTPUT + ... ax.set_xlabel("Hours from EDT Midnight") # doctest: +IGNORE_OUTPUT + ... ax.set_ylabel("Altitude [deg]") # doctest: +IGNORE_OUTPUT + ... ax.grid(visible=True) # doctest: +IGNORE_OUTPUT + ... plt.draw() + +.. + EXAMPLE END diff --git a/docs/coordinates/example_gallery_plot_sgr_coordinate_frame.rst b/docs/coordinates/example_gallery_plot_sgr_coordinate_frame.rst index 5f752001ba84..935ad8819db6 100644 --- a/docs/coordinates/example_gallery_plot_sgr_coordinate_frame.rst +++ b/docs/coordinates/example_gallery_plot_sgr_coordinate_frame.rst @@ -1,237 +1,237 @@ -.. _sphx_glr_generated_examples_coordinates_plot_sgr-coordinate-frame.py: - -Create a new coordinate class (for the Sagittarius stream) -========================================================== - -.. - EXAMPLE START - Create a new coordinate class (for the Sagittarius stream) - -This document describes in detail how to subclass and define a custom spherical -coordinate frame, as discussed in :ref:`astropy:astropy-coordinates-design` and -the docstring for `~astropy.coordinates.BaseCoordinateFrame`. In this example, -we will define a coordinate system defined by the plane of orbit of the -Sagittarius Dwarf Galaxy (hereafter Sgr; as defined in Majewski et al. 2003). -The Sgr coordinate system is often referred to in terms of two angular -coordinates, :math:`\Lambda,B`. - -To do this, we need to define a subclass of -`~astropy.coordinates.BaseCoordinateFrame` that knows the names and units of the -coordinate system angles in each of the supported representations. In this case -we support `~astropy.coordinates.SphericalRepresentation` with "Lambda" and -"Beta". Then we have to define the transformation from this coordinate system to -some other built-in system. Here we will use Galactic coordinates, represented -by the `~astropy.coordinates.Galactic` class. - -.. seealso:: - - The `gala package `_ - Defines a number of Astropy coordinate frames for - stellar stream coordinate systems. - - Majewski et al. 2003 - "A Two Micron All Sky Survey View of the Sagittarius - Dwarf Galaxy. I. Morphology of the Sagittarius Core and Tidal Arms", - https://arxiv.org/abs/astro-ph/0304198 - - Law & Majewski 2010 - "The Sagittarius Dwarf Galaxy: A Model for Evolution in a - Triaxial Milky Way Halo", https://arxiv.org/abs/1003.1132 - - David Law's Sgr info page - https://www.stsci.edu/~dlaw/Sgr/ - -.. plot:: - :include-source: - - >>> import matplotlib.pyplot as plt - >>> import numpy as np - >>> import astropy.coordinates as coord - >>> from astropy import units as u - >>> from astropy.coordinates import frame_transform_graph, rotation_matrix - - The first step is to create a new class, which we'll call - ``Sagittarius`` and make it a subclass of - `~astropy.coordinates.BaseCoordinateFrame`: - - >>> class Sagittarius(coord.BaseCoordinateFrame): - ... """A Heliocentric spherical coordinate system defined by the orbit - ... of the Sagittarius dwarf galaxy, as described in - ... https://ui.adsabs.harvard.edu/abs/2003ApJ...599.1082M - ... and further explained in - ... https://www.stsci.edu/~dlaw/Sgr/. - ... - ... Parameters - ... ---------- - ... representation : `~astropy.coordinates.BaseRepresentation` or None - ... A representation object or None to have no data (or use the other keywords) - ... Lambda : `~astropy.coordinates.Angle`, optional, must be keyword - ... The longitude-like angle corresponding to Sagittarius' orbit. - ... Beta : `~astropy.coordinates.Angle`, optional, must be keyword - ... The latitude-like angle corresponding to Sagittarius' orbit. - ... distance : `~astropy.units.Quantity`, optional, must be keyword - ... The Distance for this object along the line-of-sight. - ... pm_Lambda_cosBeta : `~astropy.units.Quantity`, optional, must be keyword - ... The proper motion along the stream in ``Lambda`` (including the - ... ``cos(Beta)`` factor) for this object (``pm_Beta`` must also be given). - ... pm_Beta : `~astropy.units.Quantity`, optional, must be keyword - ... The proper motion in Declination for this object (``pm_ra_cosdec`` must - ... also be given). - ... radial_velocity : `~astropy.units.Quantity`, optional, keyword-only - ... The radial velocity of this object. - ... """ - ... default_representation = coord.SphericalRepresentation - ... default_differential = coord.SphericalCosLatDifferential - ... frame_specific_representation_info = { - ... coord.SphericalRepresentation: [ - ... coord.RepresentationMapping("lon", "Lambda"), - ... coord.RepresentationMapping("lat", "Beta"), - ... coord.RepresentationMapping("distance", "distance"), - ... ] - ... } - - Breaking this down line-by-line, we define the class as a subclass of - `~astropy.coordinates.BaseCoordinateFrame`. Then we include a descriptive - docstring. The final lines are class-level attributes that specify the - default representation for the data, default differential for the velocity - information, and mappings from the attribute names used by representation - objects to the names that are to be used by the ``Sagittarius`` frame. In this - case we override the names in the spherical representations but do not do - anything with other representations like cartesian or cylindrical. - - Next we have to define the transformation from this coordinate system to some - other built-in coordinate system; we will use Galactic coordinates. We can do - this by defining functions that return transformation matrices, or by simply - defining a function that accepts a coordinate and returns a new coordinate in - the new system. Because the transformation to the Sagittarius coordinate - stem is just a spherical rotation from Galactic coordinates, we will - define a function that returns this matrix. We will start by constructing the - transformation matrix using pre-determined Euler angles and the - ``rotation_matrix`` helper function: - - >>> SGR_PHI = (180 + 3.75) * u.degree # Euler angles (from Law & Majewski 2010) - >>> SGR_THETA = (90 - 13.46) * u.degree - >>> SGR_PSI = (180 + 14.111534) * u.degree - - Generate the rotation matrix using the x-convention (see Goldstein): - - >>> SGR_MATRIX = ( - ... np.diag([1.0, 1.0, -1.0]) - ... @ rotation_matrix(SGR_PSI, "z") - ... @ rotation_matrix(SGR_THETA, "x") - ... @ rotation_matrix(SGR_PHI, "z") - ... ) - - Since we already constructed the transformation (rotation) matrix above, and - the inverse of a rotation matrix is just its transpose, the required - transformation functions are very simple: - - >>> @frame_transform_graph.transform( - ... coord.StaticMatrixTransform, coord.Galactic, Sagittarius - ... ) - ... def galactic_to_sgr(): - ... """Compute the Galactic spherical to heliocentric Sgr transformation matrix.""" - ... return SGR_MATRIX - - The decorator ``@frame_transform_graph.transform(coord.StaticMatrixTransform, coord.Galactic, Sagittarius)`` - registers this function on the - ``frame_transform_graph`` as a coordinate transformation. Inside the function, - we return the previously defined rotation matrix. - - We then register the inverse transformation by using the transpose of the - rotation matrix (which is faster to compute than the inverse): - - >>> @frame_transform_graph.transform( - ... coord.StaticMatrixTransform, Sagittarius, coord.Galactic - ... ) - ... def sgr_to_galactic(): - ... """Compute the heliocentric Sgr to spherical Galactic transformation matrix.""" - ... return SGR_MATRIX.swapaxes(-2, -1) - - Now that we have registered these transformations between ``Sagittarius`` and - `~astropy.coordinates.Galactic`, we can transform between *any* coordinate - system and ``Sagittarius`` (as long as the other system has a path to - transform to `~astropy.coordinates.Galactic`). For example, to transform from - ICRS coordinates to ``Sagittarius``, we would do: - - >>> icrs = coord.SkyCoord(280.161732 * u.degree, 11.91934 * u.degree, frame="icrs") - >>> sgr = icrs.transform_to(Sagittarius) - >>> print(sgr) - - - Or, to transform from the ``Sagittarius`` frame to ICRS coordinates (in this - case, a line along the ``Sagittarius`` x-y plane): - - >>> sgr = coord.SkyCoord( - ... Lambda=np.linspace(0, 2 * np.pi, 128) * u.radian, - ... Beta=np.zeros(128) * u.radian, - ... frame="sagittarius", - ... ) - >>> icrs = sgr.transform_to(coord.ICRS) - >>> print(icrs) # doctest: +ELLIPSIS - - - As an example, we will now plot the points in both coordinate systems: - - >>> fig, axes = plt.subplots(2, 1, figsize=(8, 10), subplot_kw={"projection": "aitoff"}) - >>> axes[0].set_title("Sagittarius") # doctest: +IGNORE_OUTPUT - >>> axes[0].plot( - ... sgr.Lambda.wrap_at(180 * u.deg).radian, - ... sgr.Beta.radian, - ... linestyle="none", - ... marker=".", - ... ) # doctest: +IGNORE_OUTPUT - >>> axes[0].grid(visible=True) # doctest: +IGNORE_OUTPUT - >>> axes[1].set_title("ICRS") # doctest: +IGNORE_OUTPUT - >>> axes[1].plot( - ... icrs.ra.wrap_at(180 * u.deg).radian, icrs.dec.radian, linestyle="none", marker="." - ... ) # doctest: +IGNORE_OUTPUT - >>> axes[1].grid(visible=True) # doctest: +IGNORE_OUTPUT - - This particular transformation is just a spherical rotation, which is a - special case of an Affine transformation with no vector offset. The - transformation of velocity components is therefore natively supported as - well: - - >>> sgr = coord.SkyCoord( - ... Lambda=np.linspace(0, 2 * np.pi, 128) * u.radian, - ... Beta=np.zeros(128) * u.radian, - ... pm_Lambda_cosBeta=np.random.uniform(-5, 5, 128) * (u.mas / u.yr), - ... pm_Beta=np.zeros(128) * (u.mas / u.yr), - ... frame="sagittarius", - ... ) - >>> icrs = sgr.transform_to(coord.ICRS) - >>> print(icrs) # doctest: +ELLIPSIS - - >>> fig, axes = plt.subplots(3, 1, figsize=(8, 10), sharex=True) - >>> axes[0].set_title("Sagittarius") # doctest: +IGNORE_OUTPUT - >>> axes[0].plot( - ... sgr.Lambda.degree, sgr.pm_Lambda_cosBeta.value, linestyle="none", marker="." - ... ) # doctest: +IGNORE_OUTPUT - >>> axes[0].set_xlabel(r"$\Lambda$ [deg]") # doctest: +IGNORE_OUTPUT - >>> axes[0].set_ylabel( - ... rf"$\mu_\Lambda \, \cos B$ [{sgr.pm_Lambda_cosBeta.unit.to_string('latex_inline')}]" - ... ) # doctest: +IGNORE_OUTPUT - >>> axes[0].grid(visible=True) # doctest: +IGNORE_OUTPUT - >>> axes[1].set_title("ICRS") # doctest: +IGNORE_OUTPUT - >>> axes[1].plot(icrs.ra.degree, icrs.pm_ra_cosdec.value, linestyle="none", marker=".") # doctest: +IGNORE_OUTPUT - >>> axes[1].set_ylabel( - ... rf"$\mu_\alpha \, \cos\delta$ [{icrs.pm_ra_cosdec.unit.to_string('latex_inline')}]" - ... ) # doctest: +IGNORE_OUTPUT - >>> axes[1].grid(visible=True) # doctest: +IGNORE_OUTPUT - >>> axes[2].set_title("ICRS") # doctest: +IGNORE_OUTPUT - >>> axes[2].plot(icrs.ra.degree, icrs.pm_dec.value, linestyle="none", marker=".") # doctest: +IGNORE_OUTPUT - >>> axes[2].set_xlabel("RA [deg]") # doctest: +IGNORE_OUTPUT - >>> axes[2].set_ylabel(rf"$\mu_\delta$ [{icrs.pm_dec.unit.to_string('latex_inline')}]") # doctest: +IGNORE_OUTPUT - >>> axes[2].grid(visible=True) # doctest: +IGNORE_OUTPUT - >>> plt.draw() - -.. - EXAMPLE END +.. _sphx_glr_generated_examples_coordinates_plot_sgr-coordinate-frame.py: + +Create a new coordinate class (for the Sagittarius stream) +========================================================== + +.. + EXAMPLE START + Create a new coordinate class (for the Sagittarius stream) + +This document describes in detail how to subclass and define a custom spherical +coordinate frame, as discussed in :ref:`astropy:astropy-coordinates-design` and +the docstring for `~astropy.coordinates.BaseCoordinateFrame`. In this example, +we will define a coordinate system defined by the plane of orbit of the +Sagittarius Dwarf Galaxy (hereafter Sgr; as defined in Majewski et al. 2003). +The Sgr coordinate system is often referred to in terms of two angular +coordinates, :math:`\Lambda,B`. + +To do this, we need to define a subclass of +`~astropy.coordinates.BaseCoordinateFrame` that knows the names and units of the +coordinate system angles in each of the supported representations. In this case +we support `~astropy.coordinates.SphericalRepresentation` with "Lambda" and +"Beta". Then we have to define the transformation from this coordinate system to +some other built-in system. Here we will use Galactic coordinates, represented +by the `~astropy.coordinates.Galactic` class. + +.. seealso:: + + The `gala package `_ + Defines a number of Astropy coordinate frames for + stellar stream coordinate systems. + + Majewski et al. 2003 + "A Two Micron All Sky Survey View of the Sagittarius + Dwarf Galaxy. I. Morphology of the Sagittarius Core and Tidal Arms", + https://arxiv.org/abs/astro-ph/0304198 + + Law & Majewski 2010 + "The Sagittarius Dwarf Galaxy: A Model for Evolution in a + Triaxial Milky Way Halo", https://arxiv.org/abs/1003.1132 + + David Law's Sgr info page + https://www.stsci.edu/~dlaw/Sgr/ + +.. plot:: + :include-source: + + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> import astropy.coordinates as coord + >>> from astropy import units as u + >>> from astropy.coordinates import frame_transform_graph, rotation_matrix + + The first step is to create a new class, which we'll call + ``Sagittarius`` and make it a subclass of + `~astropy.coordinates.BaseCoordinateFrame`: + + >>> class Sagittarius(coord.BaseCoordinateFrame): + ... """A Heliocentric spherical coordinate system defined by the orbit + ... of the Sagittarius dwarf galaxy, as described in + ... https://ui.adsabs.harvard.edu/abs/2003ApJ...599.1082M + ... and further explained in + ... https://www.stsci.edu/~dlaw/Sgr/. + ... + ... Parameters + ... ---------- + ... representation : `~astropy.coordinates.BaseRepresentation` or None + ... A representation object or None to have no data (or use the other keywords) + ... Lambda : `~astropy.coordinates.Angle`, optional, must be keyword + ... The longitude-like angle corresponding to Sagittarius' orbit. + ... Beta : `~astropy.coordinates.Angle`, optional, must be keyword + ... The latitude-like angle corresponding to Sagittarius' orbit. + ... distance : `~astropy.units.Quantity`, optional, must be keyword + ... The Distance for this object along the line-of-sight. + ... pm_Lambda_cosBeta : `~astropy.units.Quantity`, optional, must be keyword + ... The proper motion along the stream in ``Lambda`` (including the + ... ``cos(Beta)`` factor) for this object (``pm_Beta`` must also be given). + ... pm_Beta : `~astropy.units.Quantity`, optional, must be keyword + ... The proper motion in Declination for this object (``pm_ra_cosdec`` must + ... also be given). + ... radial_velocity : `~astropy.units.Quantity`, optional, keyword-only + ... The radial velocity of this object. + ... """ + ... default_representation = coord.SphericalRepresentation + ... default_differential = coord.SphericalCosLatDifferential + ... frame_specific_representation_info = { + ... coord.SphericalRepresentation: [ + ... coord.RepresentationMapping("lon", "Lambda"), + ... coord.RepresentationMapping("lat", "Beta"), + ... coord.RepresentationMapping("distance", "distance"), + ... ] + ... } + + Breaking this down line-by-line, we define the class as a subclass of + `~astropy.coordinates.BaseCoordinateFrame`. Then we include a descriptive + docstring. The final lines are class-level attributes that specify the + default representation for the data, default differential for the velocity + information, and mappings from the attribute names used by representation + objects to the names that are to be used by the ``Sagittarius`` frame. In this + case we override the names in the spherical representations but do not do + anything with other representations like cartesian or cylindrical. + + Next we have to define the transformation from this coordinate system to some + other built-in coordinate system; we will use Galactic coordinates. We can do + this by defining functions that return transformation matrices, or by simply + defining a function that accepts a coordinate and returns a new coordinate in + the new system. Because the transformation to the Sagittarius coordinate + stem is just a spherical rotation from Galactic coordinates, we will + define a function that returns this matrix. We will start by constructing the + transformation matrix using pre-determined Euler angles and the + ``rotation_matrix`` helper function: + + >>> SGR_PHI = (180 + 3.75) * u.degree # Euler angles (from Law & Majewski 2010) + >>> SGR_THETA = (90 - 13.46) * u.degree + >>> SGR_PSI = (180 + 14.111534) * u.degree + + Generate the rotation matrix using the x-convention (see Goldstein): + + >>> SGR_MATRIX = ( + ... np.diag([1.0, 1.0, -1.0]) + ... @ rotation_matrix(SGR_PSI, "z") + ... @ rotation_matrix(SGR_THETA, "x") + ... @ rotation_matrix(SGR_PHI, "z") + ... ) + + Since we already constructed the transformation (rotation) matrix above, and + the inverse of a rotation matrix is just its transpose, the required + transformation functions are very simple: + + >>> @frame_transform_graph.transform( + ... coord.StaticMatrixTransform, coord.Galactic, Sagittarius + ... ) + ... def galactic_to_sgr(): + ... """Compute the Galactic spherical to heliocentric Sgr transformation matrix.""" + ... return SGR_MATRIX + + The decorator ``@frame_transform_graph.transform(coord.StaticMatrixTransform, coord.Galactic, Sagittarius)`` + registers this function on the + ``frame_transform_graph`` as a coordinate transformation. Inside the function, + we return the previously defined rotation matrix. + + We then register the inverse transformation by using the transpose of the + rotation matrix (which is faster to compute than the inverse): + + >>> @frame_transform_graph.transform( + ... coord.StaticMatrixTransform, Sagittarius, coord.Galactic + ... ) + ... def sgr_to_galactic(): + ... """Compute the heliocentric Sgr to spherical Galactic transformation matrix.""" + ... return SGR_MATRIX.swapaxes(-2, -1) + + Now that we have registered these transformations between ``Sagittarius`` and + `~astropy.coordinates.Galactic`, we can transform between *any* coordinate + system and ``Sagittarius`` (as long as the other system has a path to + transform to `~astropy.coordinates.Galactic`). For example, to transform from + ICRS coordinates to ``Sagittarius``, we would do: + + >>> icrs = coord.SkyCoord(280.161732 * u.degree, 11.91934 * u.degree, frame="icrs") + >>> sgr = icrs.transform_to(Sagittarius) + >>> print(sgr) + + + Or, to transform from the ``Sagittarius`` frame to ICRS coordinates (in this + case, a line along the ``Sagittarius`` x-y plane): + + >>> sgr = coord.SkyCoord( + ... Lambda=np.linspace(0, 2 * np.pi, 128) * u.radian, + ... Beta=np.zeros(128) * u.radian, + ... frame="sagittarius", + ... ) + >>> icrs = sgr.transform_to(coord.ICRS) + >>> print(icrs) # doctest: +ELLIPSIS + + + As an example, we will now plot the points in both coordinate systems: + + >>> fig, axes = plt.subplots(2, 1, figsize=(8, 10), subplot_kw={"projection": "aitoff"}) + >>> axes[0].set_title("Sagittarius") # doctest: +IGNORE_OUTPUT + >>> axes[0].plot( + ... sgr.Lambda.wrap_at(180 * u.deg).radian, + ... sgr.Beta.radian, + ... linestyle="none", + ... marker=".", + ... ) # doctest: +IGNORE_OUTPUT + >>> axes[0].grid(visible=True) # doctest: +IGNORE_OUTPUT + >>> axes[1].set_title("ICRS") # doctest: +IGNORE_OUTPUT + >>> axes[1].plot( + ... icrs.ra.wrap_at(180 * u.deg).radian, icrs.dec.radian, linestyle="none", marker="." + ... ) # doctest: +IGNORE_OUTPUT + >>> axes[1].grid(visible=True) # doctest: +IGNORE_OUTPUT + + This particular transformation is just a spherical rotation, which is a + special case of an Affine transformation with no vector offset. The + transformation of velocity components is therefore natively supported as + well: + + >>> sgr = coord.SkyCoord( + ... Lambda=np.linspace(0, 2 * np.pi, 128) * u.radian, + ... Beta=np.zeros(128) * u.radian, + ... pm_Lambda_cosBeta=np.random.uniform(-5, 5, 128) * (u.mas / u.yr), + ... pm_Beta=np.zeros(128) * (u.mas / u.yr), + ... frame="sagittarius", + ... ) + >>> icrs = sgr.transform_to(coord.ICRS) + >>> print(icrs) # doctest: +ELLIPSIS + + >>> fig, axes = plt.subplots(3, 1, figsize=(8, 10), sharex=True) + >>> axes[0].set_title("Sagittarius") # doctest: +IGNORE_OUTPUT + >>> axes[0].plot( + ... sgr.Lambda.degree, sgr.pm_Lambda_cosBeta.value, linestyle="none", marker="." + ... ) # doctest: +IGNORE_OUTPUT + >>> axes[0].set_xlabel(r"$\Lambda$ [deg]") # doctest: +IGNORE_OUTPUT + >>> axes[0].set_ylabel( + ... rf"$\mu_\Lambda \, \cos B$ [{sgr.pm_Lambda_cosBeta.unit.to_string('latex_inline')}]" + ... ) # doctest: +IGNORE_OUTPUT + >>> axes[0].grid(visible=True) # doctest: +IGNORE_OUTPUT + >>> axes[1].set_title("ICRS") # doctest: +IGNORE_OUTPUT + >>> axes[1].plot(icrs.ra.degree, icrs.pm_ra_cosdec.value, linestyle="none", marker=".") # doctest: +IGNORE_OUTPUT + >>> axes[1].set_ylabel( + ... rf"$\mu_\alpha \, \cos\delta$ [{icrs.pm_ra_cosdec.unit.to_string('latex_inline')}]" + ... ) # doctest: +IGNORE_OUTPUT + >>> axes[1].grid(visible=True) # doctest: +IGNORE_OUTPUT + >>> axes[2].set_title("ICRS") # doctest: +IGNORE_OUTPUT + >>> axes[2].plot(icrs.ra.degree, icrs.pm_dec.value, linestyle="none", marker=".") # doctest: +IGNORE_OUTPUT + >>> axes[2].set_xlabel("RA [deg]") # doctest: +IGNORE_OUTPUT + >>> axes[2].set_ylabel(rf"$\mu_\delta$ [{icrs.pm_dec.unit.to_string('latex_inline')}]") # doctest: +IGNORE_OUTPUT + >>> axes[2].grid(visible=True) # doctest: +IGNORE_OUTPUT + >>> plt.draw() + +.. + EXAMPLE END diff --git a/docs/coordinates/example_gallery_rv_to_gsr.rst b/docs/coordinates/example_gallery_rv_to_gsr.rst index 1669cb874d65..35d38c5063bf 100644 --- a/docs/coordinates/example_gallery_rv_to_gsr.rst +++ b/docs/coordinates/example_gallery_rv_to_gsr.rst @@ -1,103 +1,103 @@ -.. _sphx_glr_generated_examples_coordinates_rv-to-gsr.py: - -Convert a radial velocity to the Galactic Standard of Rest (GSR) -================================================================ - -.. - EXAMPLE START - Convert a radial velocity to the Galactic Standard of Rest (GSR) - -Radial or line-of-sight velocities of sources are often reported in a -Heliocentric or Solar-system barycentric reference frame. A common -transformation incorporates the projection of the Sun's motion along the -line-of-sight to the target, hence transforming it to a Galactic rest frame -instead (sometimes referred to as the Galactic Standard of Rest, GSR). This -transformation depends on the assumptions about the orientation of the Galactic -frame relative to the bary- or Heliocentric frame. It also depends on the -assumed solar velocity vector. Here we will demonstrate how to perform this -transformation using a sky position and barycentric radial-velocity. - -Use the latest convention for the Galactocentric coordinates: - ->>> import astropy.coordinates as coord ->>> coord.galactocentric_frame_defaults.set("latest") # doctest: +IGNORE_OUTPUT - -For this example, let's work with the coordinates and barycentric radial -velocity of the star HD 155967, as obtained from -`Simbad `_: - ->>> from astropy import units as u ->>> icrs = coord.SkyCoord( -... ra=258.58356362 * u.deg, -... dec=14.55255619 * u.deg, -... radial_velocity=-16.1 * u.km / u.s, -... frame="icrs", -... ) - -Next, we need to decide on the velocity of the Sun in the assumed GSR frame. -We will use the same velocity vector as used in the -`~astropy.coordinates.Galactocentric` frame, and convert it to a -`~astropy.coordinates.CartesianRepresentation` object using the -``.to_cartesian()`` method of the -`~astropy.coordinates.CartesianDifferential` object ``galcen_v_sun``: - ->>> v_sun = coord.Galactocentric().galcen_v_sun.to_cartesian() - -We now need to get a unit vector in the assumed Galactic frame from the sky -position in the ICRS frame above. We will use this unit vector to project the -solar velocity onto the line-of-sight: - ->>> gal = icrs.transform_to(coord.Galactic) ->>> cart_data = gal.data.to_cartesian() ->>> unit_vector = cart_data / cart_data.norm() - -Now we project the solar velocity using this unit vector: - ->>> v_proj = v_sun.dot(unit_vector) - -Finally, we add the projection of the solar velocity to the radial velocity -to get a GSR radial velocity: - ->>> rv_gsr = icrs.radial_velocity + v_proj ->>> print(rv_gsr) # doctest: +FLOAT_CMP -123.30460087379765 km / s - -We could wrap this in a function so we can control the solar velocity and -reuse the above code: - ->>> def rv_to_gsr(c, v_sun=None): -... """Transform a barycentric radial velocity to the Galactic Standard of Rest -... (GSR). -... -... Parameters -... ---------- -... c : `~astropy.coordinates.BaseCoordinateFrame` subclass instance -... The radial velocity, associated with a sky coordinates, to be -... transformed. -... v_sun : `~astropy.units.Quantity`, optional -... The 3D velocity of the solar system barycenter in the GSR frame. -... Defaults to the same solar motion as in the -... `~astropy.coordinates.Galactocentric` frame. -... -... Returns -... ------- -... v_gsr : `~astropy.units.Quantity` -... The input radial velocity transformed to a GSR frame. -... """ -... if v_sun is None: -... v_sun = coord.Galactocentric().galcen_v_sun.to_cartesian() -... -... gal = c.transform_to(coord.Galactic) -... cart_data = gal.data.to_cartesian() -... unit_vector = cart_data / cart_data.norm() -... -... v_proj = v_sun.dot(unit_vector) -... -... return c.radial_velocity + v_proj - ->>> rv_gsr = rv_to_gsr(icrs) ->>> print(rv_gsr) # doctest: +FLOAT_CMP -123.30460087379765 km / s - -.. - EXAMPLE END +.. _sphx_glr_generated_examples_coordinates_rv-to-gsr.py: + +Convert a radial velocity to the Galactic Standard of Rest (GSR) +================================================================ + +.. + EXAMPLE START + Convert a radial velocity to the Galactic Standard of Rest (GSR) + +Radial or line-of-sight velocities of sources are often reported in a +Heliocentric or Solar-system barycentric reference frame. A common +transformation incorporates the projection of the Sun's motion along the +line-of-sight to the target, hence transforming it to a Galactic rest frame +instead (sometimes referred to as the Galactic Standard of Rest, GSR). This +transformation depends on the assumptions about the orientation of the Galactic +frame relative to the bary- or Heliocentric frame. It also depends on the +assumed solar velocity vector. Here we will demonstrate how to perform this +transformation using a sky position and barycentric radial-velocity. + +Use the latest convention for the Galactocentric coordinates: + +>>> import astropy.coordinates as coord +>>> coord.galactocentric_frame_defaults.set("latest") # doctest: +IGNORE_OUTPUT + +For this example, let's work with the coordinates and barycentric radial +velocity of the star HD 155967, as obtained from +`Simbad `_: + +>>> from astropy import units as u +>>> icrs = coord.SkyCoord( +... ra=258.58356362 * u.deg, +... dec=14.55255619 * u.deg, +... radial_velocity=-16.1 * u.km / u.s, +... frame="icrs", +... ) + +Next, we need to decide on the velocity of the Sun in the assumed GSR frame. +We will use the same velocity vector as used in the +`~astropy.coordinates.Galactocentric` frame, and convert it to a +`~astropy.coordinates.CartesianRepresentation` object using the +``.to_cartesian()`` method of the +`~astropy.coordinates.CartesianDifferential` object ``galcen_v_sun``: + +>>> v_sun = coord.Galactocentric().galcen_v_sun.to_cartesian() + +We now need to get a unit vector in the assumed Galactic frame from the sky +position in the ICRS frame above. We will use this unit vector to project the +solar velocity onto the line-of-sight: + +>>> gal = icrs.transform_to(coord.Galactic) +>>> cart_data = gal.data.to_cartesian() +>>> unit_vector = cart_data / cart_data.norm() + +Now we project the solar velocity using this unit vector: + +>>> v_proj = v_sun.dot(unit_vector) + +Finally, we add the projection of the solar velocity to the radial velocity +to get a GSR radial velocity: + +>>> rv_gsr = icrs.radial_velocity + v_proj +>>> print(rv_gsr) # doctest: +FLOAT_CMP +123.30460087379765 km / s + +We could wrap this in a function so we can control the solar velocity and +reuse the above code: + +>>> def rv_to_gsr(c, v_sun=None): +... """Transform a barycentric radial velocity to the Galactic Standard of Rest +... (GSR). +... +... Parameters +... ---------- +... c : `~astropy.coordinates.BaseCoordinateFrame` subclass instance +... The radial velocity, associated with a sky coordinates, to be +... transformed. +... v_sun : `~astropy.units.Quantity`, optional +... The 3D velocity of the solar system barycenter in the GSR frame. +... Defaults to the same solar motion as in the +... `~astropy.coordinates.Galactocentric` frame. +... +... Returns +... ------- +... v_gsr : `~astropy.units.Quantity` +... The input radial velocity transformed to a GSR frame. +... """ +... if v_sun is None: +... v_sun = coord.Galactocentric().galcen_v_sun.to_cartesian() +... +... gal = c.transform_to(coord.Galactic) +... cart_data = gal.data.to_cartesian() +... unit_vector = cart_data / cart_data.norm() +... +... v_proj = v_sun.dot(unit_vector) +... +... return c.radial_velocity + v_proj + +>>> rv_gsr = rv_to_gsr(icrs) +>>> print(rv_gsr) # doctest: +FLOAT_CMP +123.30460087379765 km / s + +.. + EXAMPLE END From 026162f32e568301113056d68a6ad43072188ed0 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 18 Jun 2026 08:35:33 -0400 Subject: [PATCH 182/199] Backport PR #19834: Update CITATION.cff with doi --- CITATION.cff | 1 + 1 file changed, 1 insertion(+) diff --git a/CITATION.cff b/CITATION.cff index 5dc6ddcd5b87..bf449e408403 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -685,6 +685,7 @@ preferred-citation: issue: 2 pages: 20 year: 2022 + doi: 10.3847/1538-4357/ac7c74 identifiers: - description: ADS bibcode type: url From ffbbff408dd035db3dcffdfbb2df030f38c5d349 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 19 Jun 2026 23:13:47 +0100 Subject: [PATCH 183/199] Backport PR #19318: follow up to #19315: avoid assert statements in business logic --- astropy/modeling/functional_models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/astropy/modeling/functional_models.py b/astropy/modeling/functional_models.py index 7b1fd5e2ef44..16e4448dc992 100644 --- a/astropy/modeling/functional_models.py +++ b/astropy/modeling/functional_models.py @@ -383,8 +383,10 @@ def __init__( eig_vals, eig_vecs = np.linalg.eig(cov_matrix) if not NUMPY_LT_2_5 or eig_vals.dtype.kind == "c": # in numpy 2.5+, return values are *always* complex - assert np.all(eig_vals.imag == 0) - assert np.all(eig_vecs.imag == 0) + if np.any(eig_vals.imag != 0) or np.any(eig_vecs.imag != 0): + raise TypeError( + "Expected cov_matrix's eigen values to be real. Complex values were found." + ) eig_vals = eig_vals.real eig_vecs = eig_vecs.real From 7c74cbb4b2b1ec0567c17da8cc04d6bb1e9d9f1b Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 23 Jun 2026 10:55:14 +0100 Subject: [PATCH 184/199] Backport PR #19966: Fix warnings in documentation build --- docs/conf.py | 5 +++++ docs/nitpick-exceptions | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 002bb1cdf9fc..a37ed7ecbe19 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -395,6 +395,11 @@ def _custom_edit_url( dtype, target = line.split(None, 1) nitpick_ignore.append((dtype, target.strip())) +# NumPy frequently relocates its private typing namespace between releases +# (e.g. numpy._typing.ArrayLike -> numpy._typing._array_like.ArrayLike), so +# match the whole private namespace rather than tracking individual paths. +nitpick_ignore_regex = [("py:obj", r"numpy\._typing\..*")] + suppress_warnings = [ "config.cache", # our rebuild is okay ] diff --git a/docs/nitpick-exceptions b/docs/nitpick-exceptions index ab929b399e89..57391f9dae7a 100644 --- a/docs/nitpick-exceptions +++ b/docs/nitpick-exceptions @@ -12,6 +12,7 @@ py:class astropy.modeling.polynomial.PolynomialBase py:class astropy.io.fits.hdu.base.ExtensionHDU py:class astropy.io.fits.util.NotifierMixin py:class astropy.io.fits.hdu.compressed._codecs.Codec +py:class numcodecs.abc.Codec # astropy.io.misc.yaml py:class yaml.dumper.SafeDumper @@ -60,7 +61,7 @@ py:obj n py:obj v py:obj ndarray py:obj args -py:obj numpy._typing.ArrayLike +# numpy._typing.* private namespace is handled by nitpick_ignore_regex in conf.py # other classes and functions that cannot be linked to py:class xmlrpc.client.Error From 48492542e836ec31de94b884b42bb658b30e5c9b Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 23 Jun 2026 11:43:43 +0100 Subject: [PATCH 185/199] Backport PR #19970: Fixed deprecated use of shape setting in test_open_scaled_in_update_mode_compressed --- astropy/io/fits/hdu/compressed/tests/test_compressed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astropy/io/fits/hdu/compressed/tests/test_compressed.py b/astropy/io/fits/hdu/compressed/tests/test_compressed.py index 3220e150b7c8..8e845afe8be8 100644 --- a/astropy/io/fits/hdu/compressed/tests/test_compressed.py +++ b/astropy/io/fits/hdu/compressed/tests/test_compressed.py @@ -306,7 +306,7 @@ def test_open_scaled_in_update_mode_compressed(self): # Try reshaping the data, then closing and reopening the file; let's # see if all the changes are preserved properly - hdul[1].data.shape = (42, 10) + hdul[1].data = hdul[1].data.reshape(42, 10) hdul.close() hdul = fits.open(self.temp("scale.fits")) From 98f31b89d5da889a86bceed52bd53f1fb224b6f2 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 23 Jun 2026 23:58:36 +0100 Subject: [PATCH 186/199] Backport PR #19962: Update credits to add new contributors --- docs/credits.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/credits.rst b/docs/credits.rst index a9a6f00ca274..57aa984d6aca 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -495,6 +495,7 @@ Core Package Contributors * Rohit Kapoor * Rohit Patil * Roland Weber +* Romain Thomas * Roman Tolesnikov * Roy Smart * Rui Xue From 77eb7aa92e7ef2aafb9a0d0550e81463984a936a Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:30:59 -0400 Subject: [PATCH 187/199] Backport PR #19979: Bugfix for parameter value --- astropy/modeling/parameters.py | 7 +++++++ astropy/modeling/tests/test_parameters.py | 9 +++++++++ docs/changes/modeling/19979.bugfix.rst | 5 +++++ docs/modeling/parameters.rst | 10 ++++++++++ 4 files changed, 31 insertions(+) create mode 100644 docs/changes/modeling/19979.bugfix.rst diff --git a/astropy/modeling/parameters.py b/astropy/modeling/parameters.py index 4637dcfd05c5..221e1d796910 100644 --- a/astropy/modeling/parameters.py +++ b/astropy/modeling/parameters.py @@ -242,6 +242,7 @@ def __init__( # use this to convert to and from the public unit defined for the # parameter. self._internal_unit = None + self._internal_value = None if not self._model_required: if self._default is not None: self.value = self._default @@ -780,6 +781,12 @@ def _wrapper(value, raw_unit=None, orig_unit=None): orig_unit is the value after the ufunc has been applied it is assumed ufunc(raw_unit) == orig_unit """ + # Make sure value is ufunc compatible + # parameters are expected to be real floats + # If the value is `None` this will result in a NaN + if not isinstance(value, Quantity): + value = np.float64(value) + if orig_unit is not None: return ufunc(value) * orig_unit elif raw_unit is not None: diff --git a/astropy/modeling/tests/test_parameters.py b/astropy/modeling/tests/test_parameters.py index 4231d22dcc6f..4f1caeddbf24 100644 --- a/astropy/modeling/tests/test_parameters.py +++ b/astropy/modeling/tests/test_parameters.py @@ -743,6 +743,10 @@ def test_value(self): assert not isinstance(param.value, np.ndarray) assert param.value == 1 + param = Parameter(name="test", setter=setter1, getter=getter1) + assert not isinstance(param.value, np.ndarray) + assert np.isnan(param.value) + def test_raw_value(self): param = Parameter(name="test", default=[1, 2, 3, 4]) @@ -848,6 +852,11 @@ def test_param_repr_oneline(self): param = Parameter(name="test", default=[1, 2, 3, 4] * u.m) assert param_repr_oneline(param) == "[1., 2., 3., 4.] m" + def test_param_repr_getter_no_value(self): + """Regression test for #19963""" + param = Parameter(name="test", getter=np.deg2rad, setter=np.rad2deg) + assert repr(param) == "Parameter('test', value=nan)" + def test_getter_setter(self): msg = "setter and getter must both be input" with pytest.raises(ValueError, match=msg): diff --git a/docs/changes/modeling/19979.bugfix.rst b/docs/changes/modeling/19979.bugfix.rst new file mode 100644 index 000000000000..fa460575929d --- /dev/null +++ b/docs/changes/modeling/19979.bugfix.rst @@ -0,0 +1,5 @@ +Bugfix for a ``Parameter`` with a custom ``getter`` / ``setter`` pair raising an +``AttributeError`` when accessing the ``value`` of the ``Parameter`` before any +value for the parameter has been set. The ``Parameter`` will now return the result +of the ``getter`` applied to a ``nan`` value in this case which is consistent +with the behavior of a ``Parameter`` without a custom ``getter`` / ``setter`` pair. diff --git a/docs/modeling/parameters.rst b/docs/modeling/parameters.rst index bb4f1c21bded..4fbf8ca95dc8 100644 --- a/docs/modeling/parameters.rst +++ b/docs/modeling/parameters.rst @@ -39,6 +39,16 @@ cases, however, array-valued parameters have no meaning specific to the model, and are simply combined with input arrays during model evaluation according to the standard `Numpy broadcasting rules`_. + +.. note:: + + The value of a `~astropy.modeling.Parameter` which has not been set and has + no default value will be returned as ``nan`` and tracked internally as a ``None`` + until the value for that ``Parameter`` has been set. For the case of a + ``Parameter`` with a custom ``getter`` / ``setter`` pair, the value will be + returned as whatever the output of the ``getter`` is when applied to a ``nan`` + value. + Parameter constraints ===================== From 4271832d9cee1cebbac6bd4b5bbd10dee611e27f Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 30 Jun 2026 10:03:06 +0100 Subject: [PATCH 188/199] Backport PR #19898: Fix the world axis components and classes cache in FITS WCS when equinox was NaN --- astropy/wcs/wcsapi/fitswcs.py | 5 ++++- astropy/wcs/wcsapi/tests/test_fitswcs.py | 10 ++++++++++ docs/changes/wcs/19898.bugfix.rst | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 docs/changes/wcs/19898.bugfix.rst diff --git a/astropy/wcs/wcsapi/fitswcs.py b/astropy/wcs/wcsapi/fitswcs.py index de13ce446a2a..c54157e17d7e 100644 --- a/astropy/wcs/wcsapi/fitswcs.py +++ b/astropy/wcs/wcsapi/fitswcs.py @@ -397,13 +397,16 @@ def _get_components_and_classes(self): # it. We start off by defining a hash based on the attributes of the # WCS that matter here (we can't just use the WCS object as a hash since # it is mutable) + # NaN values must be normalized since NaN != NaN would otherwise + # defeat the cache comparison below. + equinox = self.wcs.equinox wcs_hash = ( self.naxis, list(self.wcs.ctype), list(self.wcs.cunit), self.wcs.radesys, self.wcs.specsys, - self.wcs.equinox, + None if np.isnan(equinox) else equinox, self.wcs.dateobs, self.wcs.lng, self.wcs.lat, diff --git a/astropy/wcs/wcsapi/tests/test_fitswcs.py b/astropy/wcs/wcsapi/tests/test_fitswcs.py index ff9b6b1b9e14..6e6b2acabb8c 100644 --- a/astropy/wcs/wcsapi/tests/test_fitswcs.py +++ b/astropy/wcs/wcsapi/tests/test_fitswcs.py @@ -1663,3 +1663,13 @@ def test_array_index_conversions_scalars_2d(): x, y = wcs.world_to_pixel(scoord) assert isinstance(x, np.ndarray) and x.ndim == 0 assert isinstance(y, np.ndarray) and y.ndim == 0 + + +def test_components_and_classes_cache(): + # Regression test for the components and classes cache never hitting + # when the equinox was undefined, because the NaN it contributed to the + # cache key compares unequal to itself + wcs = WCS(naxis=2) + wcs.wcs.ctype = ["RA---TAN", "DEC--TAN"] + wcs.wcs.set() + assert wcs.world_axis_object_components is wcs.world_axis_object_components diff --git a/docs/changes/wcs/19898.bugfix.rst b/docs/changes/wcs/19898.bugfix.rst new file mode 100644 index 000000000000..7318bd93144d --- /dev/null +++ b/docs/changes/wcs/19898.bugfix.rst @@ -0,0 +1 @@ +Fix caching of ``wcs.world_axis_object_components`` and ``world_axis_object_classes`` in cases where the equinox was NaN From e660ac944d50fef8604735c777c33576580caf2f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 2 Jul 2026 00:45:20 +0000 Subject: [PATCH 189/199] Update minimum required version of astropy-iers-data --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b9094ebed2c7..e34b6c02c120 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ keywords = [ "ascii", ] dependencies = [ - "astropy-iers-data>=0.2026.6.1.17.39.59", + "astropy-iers-data>=0.2026.6.22.1.23.34", "numpy>=1.24, <2.7", "packaging>=22.0.0", "pyerfa>=2.0.1.1", # For >=2.0.1.7, adjust structured_units.rst and doctest-requires From d5652fb0040bc8f603816636c2a2d2943d087d9f Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 2 Jul 2026 11:41:45 +0100 Subject: [PATCH 190/199] Backport PR #19995: Don't download IERS table if it is recent enough as indicated by auto_max_age --- astropy/utils/iers/iers.py | 85 ++++++++++---------------- astropy/utils/iers/tests/test_iers.py | 88 +++++++++++++++++++-------- docs/changes/utils/19995.api.rst | 1 + 3 files changed, 94 insertions(+), 80 deletions(-) create mode 100644 docs/changes/utils/19995.api.rst diff --git a/astropy/utils/iers/iers.py b/astropy/utils/iers/iers.py index efb59b589076..a1e659759cf8 100644 --- a/astropy/utils/iers/iers.py +++ b/astropy/utils/iers/iers.py @@ -783,20 +783,19 @@ class IERS_Auto(IERS_A): @classmethod def open(cls): - """If the configuration setting ``astropy.utils.iers.conf.auto_download`` - is set to True (default), then open a recent version of the IERS-A - table with predictions for UT1-UTC and polar motion out to - approximately one year from now. If the available version of this file - is older than ``astropy.utils.iers.conf.auto_max_age`` days old - (or non-existent) then it will be downloaded over the network and cached. + """ + This reads in the bundled IERS_A table (or the version of the file in + the current working directory, if present - see `IERS_A.read`). - If the configuration setting ``astropy.utils.iers.conf.auto_download`` - is set to False then the bundled IERS-A table will be used rather than - any downloaded version of the IERS-A table. + Subsequently, if values are requested which are more recent than the + range of validity of the bundled table, an updated version of the table + will be downloaded if the configuration setting + ``astropy.utils.iers.conf.auto_download`` is set to True (default) and + if the available version of this file is older than + ``astropy.utils.iers.conf.auto_max_age`` days old. - On the first call in a session, the table will be memoized (in the - ``iers_table`` class attribute), and further calls to ``open`` will - return this stored table. + If the configuration setting ``astropy.utils.iers.conf.auto_download`` + is set to False then the bundled IERS-A table will always be used. Returns ------- @@ -804,47 +803,8 @@ def open(cls): With IERS (Earth rotation) data columns """ - if not conf.auto_download: - # If auto_download is changed to False mid-session, iers_table may have already been - # made from non-bundled files, so it should be remade from bundled files - if not hasattr(cls, "_iers_table_bundled"): - cls._iers_table_bundled = cls.read() - cls.iers_table = cls._iers_table_bundled - return cls.iers_table - - all_urls = (conf.iers_auto_url, conf.iers_auto_url_mirror) - - if cls.iers_table is not None: - # If the URL has changed, we need to redownload the file, so we - # should ignore the internally cached version. - - if cls.iers_table.meta.get("data_url") in all_urls: - return cls.iers_table - - for url in all_urls: - try: - filename = download_file(url, cache=True) - except Exception as err: - warn(f"failed to download {url}: {err}", IERSWarning) - continue - - try: - cls.iers_table = cls.read(file=filename) - except Exception as err: - warn(f"malformed IERS table from {url}: {err}", IERSWarning) - continue - cls.iers_table.meta["data_url"] = url - break - - else: - # Issue a warning here, perhaps user is offline. An exception - # will be raised downstream if actually trying to interpolate - # predictive values. - warn( - "unable to download valid IERS file, using bundled IERS-A", IERSWarning - ) + if cls.iers_table is None: cls.iers_table = cls.read() - return cls.iers_table def _check_interpolate_indices(self, indices_orig, indices_clipped, max_input_mjd): @@ -924,7 +884,26 @@ def _refresh_table_as_needed(self, mjd): ) return - new_table = self.__class__.read(file=filename) + try: + new_table = self.__class__.read(file=filename) + except Exception as err: + # The download succeeded but the content could not be parsed as + # an IERS table (e.g. the server returned an error page). Keep + # using the existing table; an exception will be raised + # downstream when actually trying to interpolate predictive + # values. + warn( + AstropyWarning( + "malformed IERS table downloaded from" + f" {' and '.join(all_urls)}: {err}.\nA coordinate or" + " time-related calculation might be compromised or fail" + " because the dates are not covered by the available IERS" + ' file. See the "IERS data access" section of the astropy' + " documentation for additional information on working" + " offline." + ) + ) + return new_table.meta["data_url"] = str(all_urls[0]) # New table has new values? diff --git a/astropy/utils/iers/tests/test_iers.py b/astropy/utils/iers/tests/test_iers.py index 29f0e802e401..e544e4ccbf69 100644 --- a/astropy/utils/iers/tests/test_iers.py +++ b/astropy/utils/iers/tests/test_iers.py @@ -20,7 +20,7 @@ from astropy.tests.helper import CI, assert_quantity_allclose from astropy.time import Time, TimeDelta from astropy.utils.data import get_pkg_data_filename -from astropy.utils.exceptions import AstropyDeprecationWarning +from astropy.utils.exceptions import AstropyDeprecationWarning, AstropyWarning from astropy.utils.iers import iers FILE_NOT_FOUND_ERROR = getattr(__builtins__, "FileNotFoundError", OSError) @@ -247,6 +247,12 @@ def setup_class(self): self._auto_download = iers.conf.auto_download iers.conf.auto_download = True + # Ensure no IERS_Auto table is cached from an earlier test, so that the + # first test's open() re-reads the (possibly monkeypatched) bundled file + # rather than returning a stale memoized table; teardown_method keeps it + # clean for the remaining tests. + iers.IERS_Auto.close() + # auto_download = False is tested in test_IERS_B_parameters_loading_into_IERS_Auto() def teardown_class(self): @@ -257,10 +263,11 @@ def teardown_method(self, method): """Run this after every test.""" iers.IERS_Auto.close() - def test_interpolate_error_formatting(self): + def test_interpolate_error_formatting(self, monkeypatch): """Regression test: make sure the error message in IERS_Auto._check_interpolate_indices() is formatted correctly. """ + monkeypatch.setattr(iers, "IERS_A_FILE", self.iers_a_file_1) with iers.conf.set_temp("iers_auto_url", self.iers_a_url_1): with iers.conf.set_temp("iers_auto_url_mirror", self.iers_a_url_1): with iers.conf.set_temp("auto_max_age", self.ame): @@ -276,10 +283,11 @@ def test_interpolate_error_formatting(self): warnings.simplefilter("ignore", iers.IERSStaleWarning) iers_table.ut1_utc(self.t.jd1, self.t.jd2) - def test_auto_max_age_none(self): + def test_auto_max_age_none(self, monkeypatch): """Make sure that iers.INTERPOLATE_ERROR's advice about setting auto_max_age = None actually works. """ + monkeypatch.setattr(iers, "IERS_A_FILE", self.iers_a_file_1) with iers.conf.set_temp("iers_auto_url", self.iers_a_url_1): with iers.conf.set_temp("auto_max_age", None): iers_table = iers.IERS_Auto.open() @@ -302,7 +310,8 @@ def test_auto_max_age_minimum(self): iers_table = iers.IERS_Auto.open() _ = iers_table.ut1_utc(self.t.jd1, self.t.jd2) - def test_simple(self): + def test_simple(self, monkeypatch): + monkeypatch.setattr(iers, "IERS_A_FILE", self.iers_a_file_1) with iers.conf.set_temp("iers_auto_url", self.iers_a_url_1): dat = iers.IERS_Auto.open() assert dat["MJD"][0] == 57359.0 * u.d @@ -482,33 +491,58 @@ def test_iers_b_out_of_range_handling(): (now + 100 * u.day).ut1 +@pytest.fixture +def reset_iers_auto_cache(): + """Clear the IERS_A/IERS_Auto/IERS caches around a test. + + IERS_Auto.open() memoizes its table in a class attribute, so a test that + loads a non-default bundled table (e.g. a truncated fixture) would otherwise + leak that table into later tests through earth_orientation_table. close() + only nulls the cached table, so this is safe regardless of the current + IERS_A_FILE value. + """ + for cls in (iers.IERS_A, iers.IERS_Auto, iers.IERS): + cls.close() + yield + for cls in (iers.IERS_A, iers.IERS_Auto, iers.IERS): + cls.close() + + @pytest.mark.remote_data -def test_iers_download_error_handling(tmp_path): - # Make sure an IERS-A table isn't already loaded +def test_iers_download_error_handling(tmp_path, monkeypatch, reset_iers_auto_cache): + # IERS_Auto.open() now reads the bundled table and only attempts a download + # later, when predictive values beyond the table range are requested while + # the table is older than auto_max_age. Point at an old bundled table so + # that requesting a recent date triggers a download attempt. The truncated + # fixture it loads is cleared afterwards by reset_iers_auto_cache. + monkeypatch.setattr( + iers, + "IERS_A_FILE", + get_pkg_data_filename(os.path.join("data", "finals2000A-2016-02-30-test")), + ) with set_temp_cache(tmp_path), iers.conf.set_temp("auto_download", True): - iers.IERS_A.close() - iers.IERS_Auto.close() - iers.IERS.close() now = Time.now() - # bad site name - with iers.conf.set_temp("iers_auto_url", "FAIL FAIL"): - # site that exists but doesn't have IERS data - with iers.conf.set_temp("iers_auto_url_mirror", "https://google.com"): - with pytest.warns(iers.IERSWarning) as record: - with iers.conf.set_temp("iers_degraded_accuracy", "ignore"): - (now + 400 * u.day).ut1 - - assert len(record) == 3 - assert str(record[0].message).startswith( - "failed to download FAIL FAIL: Malformed URL" - ) - assert str(record[1].message).startswith( - "malformed IERS table from https://google.com" - ) - assert str(record[2].message).startswith( - "unable to download valid IERS file, using bundled IERS-A" - ) + # Primary URL is a bad site name and the mirror is a site that exists + # but does not provide a valid IERS table, so the refresh cannot find + # usable data. + with ( + iers.conf.set_temp("iers_auto_url", "FAIL FAIL"), + iers.conf.set_temp("iers_auto_url_mirror", "https://google.com"), + iers.conf.set_temp("iers_degraded_accuracy", "ignore"), + ): + # The failed refresh is reported with a warning (the download fails + # or the downloaded content cannot be parsed), and since the bundled + # table is too old to cover the requested predictive date the + # interpolation then raises. + with pytest.warns( + AstropyWarning, match="failed to download|malformed IERS table" + ): + with pytest.raises( + ValueError, + match="interpolating from IERS_Auto using predictive values", + ): + (now + 400 * u.day).ut1 OLD_DATA_FILES = { diff --git a/docs/changes/utils/19995.api.rst b/docs/changes/utils/19995.api.rst new file mode 100644 index 000000000000..0cd3754c3f00 --- /dev/null +++ b/docs/changes/utils/19995.api.rst @@ -0,0 +1 @@ +Astropy will now no longer download the latest IERS-A table over the network unless it is actually needed, and will also not warn about download issues unless all mirrors fail. The ``IERS_Auto.open`` method now always reads the table bundled in ``astropy-iers-data`` (or a ``finals2000A.all`` file in the current working directory, if present), and the auto-download is deferred until a calculation actually requests ``UT1-UTC`` or polar motion values beyond the predictive range of the bundled table while that table is older than ``astropy.utils.iers.conf.auto_max_age`` days. This avoids network access, and the associated warnings when offline, in the common case where the bundled table is recent enough. From 99b3cb3479ccb7059ac5d33295d388fe4ae78852 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 3 Jul 2026 14:20:15 +0100 Subject: [PATCH 191/199] Finalizing changelog for v7.2.2 --- CHANGES.rst | 46 ++++++++++++++++++++++++++ docs/changes/modeling/19979.bugfix.rst | 5 --- docs/changes/table/19903.bugfix.rst | 3 -- docs/changes/table/19912.bugfix.rst | 3 -- docs/changes/units/19106.bugfix.rst | 1 - docs/changes/utils/19995.api.rst | 1 - docs/changes/wcs/19898.bugfix.rst | 1 - 7 files changed, 46 insertions(+), 14 deletions(-) delete mode 100644 docs/changes/modeling/19979.bugfix.rst delete mode 100644 docs/changes/table/19903.bugfix.rst delete mode 100644 docs/changes/table/19912.bugfix.rst delete mode 100644 docs/changes/units/19106.bugfix.rst delete mode 100644 docs/changes/utils/19995.api.rst delete mode 100644 docs/changes/wcs/19898.bugfix.rst diff --git a/CHANGES.rst b/CHANGES.rst index 2a991d17071b..f5a46ec19ff6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,49 @@ +Version v7.2.2 (2026-07-03) +=========================== + +API Changes +----------- + +astropy.utils +^^^^^^^^^^^^^ + +- Astropy will now no longer download the latest IERS-A table over the network unless it is actually needed, and will also not warn about download issues unless all mirrors fail. The ``IERS_Auto.open`` method now always reads the table bundled in ``astropy-iers-data`` (or a ``finals2000A.all`` file in the current working directory, if present), and the auto-download is deferred until a calculation actually requests ``UT1-UTC`` or polar motion values beyond the predictive range of the bundled table while that table is older than ``astropy.utils.iers.conf.auto_max_age`` days. This avoids network access, and the associated warnings when offline, in the common case where the bundled table is recent enough. [#19995] + + +Bug Fixes +--------- + +astropy.modeling +^^^^^^^^^^^^^^^^ + +- Bugfix for a ``Parameter`` with a custom ``getter`` / ``setter`` pair raising an + ``AttributeError`` when accessing the ``value`` of the ``Parameter`` before any + value for the parameter has been set. The ``Parameter`` will now return the result + of the ``getter`` applied to a ``nan`` value in this case which is consistent + with the behavior of a ``Parameter`` without a custom ``getter`` / ``setter`` pair. [#19979] + +astropy.table +^^^^^^^^^^^^^ + +- Fixed a bug where a ``Table.loc[]`` range query using the default + ``SortedArray`` index engine silently dropped rows when the upper bound + matched a value that appears more than once in the indexed column. [#19903] + +- Fixed a bug where a ``Table.loc[]`` range query using the ``BST`` index + engine returned rows in scrambled order if rows had been added after the + index was created. [#19912] + +astropy.units +^^^^^^^^^^^^^ + +- Fixed a bug where ``numpy.lib.recfunctions.structured_to_unstructured`` failed when applied to a ``StructuredQuantity`` by improving unit extraction logic. [#19106] + +astropy.wcs +^^^^^^^^^^^ + +- Fix caching of ``wcs.world_axis_object_components`` and ``world_axis_object_classes`` in cases where the equinox was NaN [#19898] + + Version 7.2.1 (2026-06-16) ========================== diff --git a/docs/changes/modeling/19979.bugfix.rst b/docs/changes/modeling/19979.bugfix.rst deleted file mode 100644 index fa460575929d..000000000000 --- a/docs/changes/modeling/19979.bugfix.rst +++ /dev/null @@ -1,5 +0,0 @@ -Bugfix for a ``Parameter`` with a custom ``getter`` / ``setter`` pair raising an -``AttributeError`` when accessing the ``value`` of the ``Parameter`` before any -value for the parameter has been set. The ``Parameter`` will now return the result -of the ``getter`` applied to a ``nan`` value in this case which is consistent -with the behavior of a ``Parameter`` without a custom ``getter`` / ``setter`` pair. diff --git a/docs/changes/table/19903.bugfix.rst b/docs/changes/table/19903.bugfix.rst deleted file mode 100644 index 476db8efef92..000000000000 --- a/docs/changes/table/19903.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed a bug where a ``Table.loc[]`` range query using the default -``SortedArray`` index engine silently dropped rows when the upper bound -matched a value that appears more than once in the indexed column. diff --git a/docs/changes/table/19912.bugfix.rst b/docs/changes/table/19912.bugfix.rst deleted file mode 100644 index a337b77b3778..000000000000 --- a/docs/changes/table/19912.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed a bug where a ``Table.loc[]`` range query using the ``BST`` index -engine returned rows in scrambled order if rows had been added after the -index was created. diff --git a/docs/changes/units/19106.bugfix.rst b/docs/changes/units/19106.bugfix.rst deleted file mode 100644 index 2400be54f864..000000000000 --- a/docs/changes/units/19106.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug where ``numpy.lib.recfunctions.structured_to_unstructured`` failed when applied to a ``StructuredQuantity`` by improving unit extraction logic. diff --git a/docs/changes/utils/19995.api.rst b/docs/changes/utils/19995.api.rst deleted file mode 100644 index 0cd3754c3f00..000000000000 --- a/docs/changes/utils/19995.api.rst +++ /dev/null @@ -1 +0,0 @@ -Astropy will now no longer download the latest IERS-A table over the network unless it is actually needed, and will also not warn about download issues unless all mirrors fail. The ``IERS_Auto.open`` method now always reads the table bundled in ``astropy-iers-data`` (or a ``finals2000A.all`` file in the current working directory, if present), and the auto-download is deferred until a calculation actually requests ``UT1-UTC`` or polar motion values beyond the predictive range of the bundled table while that table is older than ``astropy.utils.iers.conf.auto_max_age`` days. This avoids network access, and the associated warnings when offline, in the common case where the bundled table is recent enough. diff --git a/docs/changes/wcs/19898.bugfix.rst b/docs/changes/wcs/19898.bugfix.rst deleted file mode 100644 index 7318bd93144d..000000000000 --- a/docs/changes/wcs/19898.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix caching of ``wcs.world_axis_object_components`` and ``world_axis_object_classes`` in cases where the equinox was NaN From 9495422a618088fb884dba7646e89edeca09d9a1 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:51:45 -0400 Subject: [PATCH 192/199] Backport PR #20021: DOC correct parameter names and remove stale doc entries (4 locations) --- astropy/logger.py | 2 -- astropy/modeling/bounding_box.py | 2 +- astropy/nddata/nduncertainty.py | 4 ---- astropy/table/pprint.py | 2 +- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/astropy/logger.py b/astropy/logger.py index da691dd82df9..60e079c63f05 100644 --- a/astropy/logger.py +++ b/astropy/logger.py @@ -455,8 +455,6 @@ def log_to_list(self, filter_level=None, filter_origin=None): Parameters ---------- - filename : str - The file to log messages to. filter_level : str If set, any log messages less important than ``filter_level`` will not be output to the file. Note that this is in addition to the diff --git a/astropy/modeling/bounding_box.py b/astropy/modeling/bounding_box.py index ff386fdb7502..360d7bc8ab82 100644 --- a/astropy/modeling/bounding_box.py +++ b/astropy/modeling/bounding_box.py @@ -819,7 +819,7 @@ def fix_inputs(self, model, fixed_inputs: dict, _keep_ignored=False) -> Self: The new model for which this will be a bounding_box fixed_inputs : dict Dictionary of inputs which have been fixed by this bounding box. - keep_ignored : bool + _keep_ignored : bool Keep the ignored inputs of the bounding box (internal argument only) """ new = self.copy() diff --git a/astropy/nddata/nduncertainty.py b/astropy/nddata/nduncertainty.py index fc7c37eb1d64..8a897aafa8b4 100644 --- a/astropy/nddata/nduncertainty.py +++ b/astropy/nddata/nduncertainty.py @@ -556,10 +556,6 @@ def _propagate_collapse(self, numpy_op, axis=None): numpy_op : function Numpy operation like `np.sum` or `np.max` to use in the collapse - subtract : bool, optional - If ``True``, propagate for subtraction, otherwise propagate for - addition. - axis : tuple, optional Axis on which to compute collapsing operations. """ diff --git a/astropy/table/pprint.py b/astropy/table/pprint.py index 92946c097f8f..5b0907bd55d1 100644 --- a/astropy/table/pprint.py +++ b/astropy/table/pprint.py @@ -58,7 +58,7 @@ def get_auto_format_func( Parameters ---------- - col_name : object, optional + col : object, optional Hashable object to identify column like id or name. Default is None. possible_string_format_functions : func, optional From faf23d929be8904927644a1a826b899accec4ea7 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:22:52 -0400 Subject: [PATCH 193/199] Backport PR #20025: Remove an invalid URL in documentation --- docs/development/codeguide.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/development/codeguide.rst b/docs/development/codeguide.rst index 8bfff3f1d357..ba7be89eadd8 100644 --- a/docs/development/codeguide.rst +++ b/docs/development/codeguide.rst @@ -434,5 +434,4 @@ the hierarchy. .. _Scipy: https://www.scipy.org/ .. _matplotlib: https://matplotlib.org/ .. _Cython: https://cython.org/ -.. _PyPI: https://pypi.org/project .. _ruff: https://docs.astral.sh/ruff/ From d497f6e7f0ff6f88b5fcbff18adc0869dd0a832c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Wed, 8 Jul 2026 07:31:21 -0700 Subject: [PATCH 194/199] Backport PR #19897: CI: adding workflow_dispatch to all workflows --- .github/workflows/CFF-test.yml | 3 +++ .github/workflows/ci_workflows.yml | 1 + .github/workflows/codeql-analysis.yml | 1 + 3 files changed, 5 insertions(+) diff --git a/.github/workflows/CFF-test.yml b/.github/workflows/CFF-test.yml index 1607571360a6..3e370fa76af4 100644 --- a/.github/workflows/CFF-test.yml +++ b/.github/workflows/CFF-test.yml @@ -1,6 +1,9 @@ name: Checking CITATION.cff on: + workflow_dispatch: + paths: + - "CITATION.cff" push: paths: - "CITATION.cff" diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index 50f72f9fcca3..3af2b4ab9109 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -1,6 +1,7 @@ name: CI on: + workflow_dispatch: push: branches: - main diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f66f1638a8db..17678e6fd4d4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,6 +12,7 @@ name: "CodeQL" on: + workflow_dispatch: schedule: # run every Wednesday at 6am UTC - cron: '0 6 * * 3' From 26d7573f6b6921f4bf9b675bb82ae5caee9026c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:06:34 -0400 Subject: [PATCH 195/199] Backport PR #20061: Update credits to add new contributors --- .mailmap | 2 ++ docs/credits.rst | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.mailmap b/.mailmap index 7ca014583be9..49cea876467c 100644 --- a/.mailmap +++ b/.mailmap @@ -115,6 +115,7 @@ Edward Gomez Edward Slavich Eduardo Olinto <90293761+olintoeduardo@users.noreply.github.com> Eero Vaher +Eesh Saxena Elijah Bernstein-Cooper Emily Deibert Emma Hogan @@ -270,6 +271,7 @@ Matthieu Baumann Matthieu Bec Mavani Bhautik +Mengjia Shang Michael Belfrage <216956+mikez@users.noreply.github.com> Michael Brewer Michael Brewer diff --git a/docs/credits.rst b/docs/credits.rst index 57aa984d6aca..cabd97afc440 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -33,6 +33,7 @@ Core Package Contributors * Alex Hagen * Alex Rudy * Alexander Bakanov +* Alexander Hu * Alexandre Beelen * Alexandre R\. Bomfim Junior * Alfio Puglisi @@ -166,6 +167,7 @@ Core Package Contributors * Edward Betts * Edward Slavich * Eero Vaher +* Eesh Saxena * Eli Bressert * Elijah Bernstein-Cooper * Elise Chavez @@ -384,6 +386,7 @@ Core Package Contributors * Médéric Boquien * Megan Sosey * Melissa Weber Mendonça +* Mengjia Shang * Michael Belfrage * Michael Brewer * Michael Droettboom @@ -537,6 +540,7 @@ Core Package Contributors * Shivansh Mishra * Shreeharsh Shinde * Shresth Verma +* Shreya Vernekar * Shreyas Bapat * Sigurd Næss * Simon Alinder From 4091d07e9f34c164ba20d65258f5efe7946aab16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Tue, 14 Jul 2026 17:13:10 +0200 Subject: [PATCH 196/199] Backport PR #20064: BUG: fix a thread-safety issue in astropy's logger --- astropy/logger.py | 2 +- docs/changes/utils/20064.bugfix.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 docs/changes/utils/20064.bugfix.rst diff --git a/astropy/logger.py b/astropy/logger.py index 60e079c63f05..427618a7a85a 100644 --- a/astropy/logger.py +++ b/astropy/logger.py @@ -229,7 +229,7 @@ def _showwarning(self, *args, **kwargs): # name. The module.__file__ is the original source file name. mod_name = None mod_path = Path(mod_path).with_suffix("") - for mod in sys.modules.values(): + for mod in list(sys.modules.values()): try: # Believe it or not this can fail in some cases: # https://github.com/astropy/astropy/issues/2671 diff --git a/docs/changes/utils/20064.bugfix.rst b/docs/changes/utils/20064.bugfix.rst new file mode 100644 index 000000000000..251aa3a852fd --- /dev/null +++ b/docs/changes/utils/20064.bugfix.rst @@ -0,0 +1,2 @@ +Fixed an issue where, in a multi-threaded context, astropy's logger could race with +imports, leading to an exception being raised. From 7396ff5770fbb5d2a67b3a9f16e4e24a1475be70 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:49:41 -0400 Subject: [PATCH 197/199] Manual backport of PR 19200 onto v7.2.x in hope to fix failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Clément Robert --- .github/workflows/ci_cron_weekly.yml | 13 +++++++++---- tox.ini | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci_cron_weekly.yml b/.github/workflows/ci_cron_weekly.yml index abae73dbf70b..d42dddf5e5cf 100644 --- a/.github/workflows/ci_cron_weekly.yml +++ b/.github/workflows/ci_cron_weekly.yml @@ -70,10 +70,13 @@ jobs: python: '3.13' toxenv: py313-test-devinfra - - name: Python 3.13t (free-threading) with recommended dependencies + - name: Python 3.14t (free-threading) with recommended dependencies os: ubuntu-latest - python: '3.13t' - toxenv: py313t-test-recdeps + python: '3.14t' + toxenv: py314t-test-recdeps + PYTHON_GIL: 0 + # https://github.com/astropy/astropy/issues/19205 + PYTHON_CONTEXT_AWARE_WARNINGS: 0 - name: Documentation link check os: ubuntu-latest @@ -103,7 +106,9 @@ jobs: run: python -m pip install --upgrade tox - name: Run tests run: tox ${{ matrix.toxargs}} -e ${{ matrix.toxenv}} -- ${{ matrix.toxposargs}} - + env: + PYTHON_GIL: ${{ matrix.PYTHON_GIL }} + PYTHON_CONTEXT_AWARE_WARNINGS: ${{ matrix.PYTHON_CONTEXT_AWARE_WARNINGS }} tests_more_architectures: diff --git a/tox.ini b/tox.ini index 09f2ec46d351..e89c1353077a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{311,312,313,313t,dev}-test{,-recdeps,-alldeps,-oldestdeps,-devdeps,-devinfra,-predeps,-numpy124,-numpy125,-mpl380}{,-cov}{,-clocale}{,-fitsio}{,-noscipy} + py{311,312,313,314t,dev}-test{,-recdeps,-alldeps,-oldestdeps,-devdeps,-devinfra,-predeps,-numpy124,-numpy125,-mpl380}{,-cov}{,-clocale}{,-fitsio}{,-noscipy} # Only these two exact tox environments have corresponding figure hash files. py311-test-image-mpl380-cov py311-test-image-mpldev-cov @@ -14,7 +14,7 @@ minversion = 4.22 # for dependency_groups # keep in sync with pyproject.toml [testenv] # Pass through the following environment variables which are needed for the CI -passenv = HOME,WINDIR,LC_ALL,LC_CTYPE,CC,CI,IS_CRON,ARCH_ON_CI,PY_COLORS +passenv = HOME,WINDIR,LC_ALL,LC_CTYPE,CC,CI,IS_CRON,ARCH_ON_CI,PY_COLORS,PYTHON_GIL,PYTHON_CONTEXT_AWARE_WARNINGS setenv = NUMPY_WARN_IF_NO_MEM_POLICY = 1 From 7303882f8752f9c9932a433a76e0c2f0bbaa1214 Mon Sep 17 00:00:00 2001 From: Tom Aldcroft Date: Fri, 17 Jul 2026 08:06:34 -0400 Subject: [PATCH 198/199] Backport PR #20031: Table groups zero length issues --- astropy/table/groups.py | 24 ++++++++++++++++++++---- astropy/table/tests/test_groups.py | 28 +++++++++++++++++++++++++++- docs/changes/table/20031.bugfix.rst | 3 +++ 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 docs/changes/table/20031.bugfix.rst diff --git a/astropy/table/groups.py b/astropy/table/groups.py index 55c670770d05..a2d352fe746d 100644 --- a/astropy/table/groups.py +++ b/astropy/table/groups.py @@ -215,7 +215,11 @@ def __getitem__(self, item): mask[i0:i1] = True out = parent[mask] out.groups._keys = parent.groups.keys[item] - out.groups._indices = np.concatenate([[0], np.cumsum(i1s - i0s)]) + out.groups._indices = ( + np.array([], dtype=int) # No selected groups so no indices + if len(out) == 0 + else np.concatenate([[0], np.cumsum(i1s - i0s)]) + ) return out @@ -223,7 +227,11 @@ def __repr__(self): return f"<{self.__class__.__name__} indices={self.indices}>" def __len__(self): - return len(self.indices) - 1 + _len = len(self.indices) + if _len == 1: + # Should never happen, indices should either have length = 0 or length >= 2. + raise RuntimeError("malformed groups.indices with length=1, this is a bug") + return _len - 1 if _len > 0 else 0 class ColumnGroups(BaseGroups): @@ -240,7 +248,11 @@ def indices(self): return self.parent_table.groups.indices else: if self._indices is None: - return np.array([0, len(self.parent_column)]) + # No explicit groups have been defined so default to a single group if + # the column has any rows, otherwise return an empty array of indices to + # match what group_by() does for an empty column. + _len = len(self.parent_column) + return np.array([0, _len]) if _len > 0 else np.array([], dtype=int) else: return self._indices @@ -344,7 +356,11 @@ def key_colnames(self): @property def indices(self): if self._indices is None: - return np.array([0, len(self.parent_table)]) + # No explicit groups have been defined so default to a single group if + # the table has any rows, otherwise return an empty array of indices to + # match what group_by() does for an empty table. + _len = len(self.parent_table) + return np.array([0, _len]) if _len > 0 else np.array([], dtype=int) else: return self._indices diff --git a/astropy/table/tests/test_groups.py b/astropy/table/tests/test_groups.py index e5ca374a461e..75bc706d6076 100644 --- a/astropy/table/tests/test_groups.py +++ b/astropy/table/tests/test_groups.py @@ -581,10 +581,25 @@ def test_table_aggregate_reduceat_empty(): }, masked=masked, ) - tga = tg.group_by("action").groups.aggregate(np.sum) + # Test default groups for empty table + assert len(tg.groups) == 0 + assert list(tg.groups) == [] + + # Test explicit grouping by one column + grouped = tg.group_by("action") + assert len(grouped.groups) == 0 + assert list(grouped.groups) == [] + tga = grouped.groups.aggregate(np.sum) assert tga.pformat() == ["action duration", "------ --------"] +def test_groups_len_malformed_indices_raises_runtime_error(): + tg = Table({"a": [1, 2], "b": [3, 4]}).group_by("a") + tg.groups._indices = np.array([1], dtype=int) + with pytest.raises(RuntimeError, match="malformed groups.indices"): + len(tg.groups) + + def test_column_aggregate(T1): """ Aggregate a single table column @@ -604,6 +619,17 @@ def test_column_aggregate_f8(): assert tga.pformat() == [" a ", "---", "0.0", "1.0"] +def test_table_group_select_empty(): + """Test selecting no groups returns a table with empty keys and no indices""" + tg = Table({"a": [1, 2], "b": [3, 4]}).group_by("a") + tgs = tg.groups[[]] + assert len(tgs) == 0 + assert len(tgs.groups) == 0 + assert len(tgs.groups.keys) == 0 + assert len(tgs.groups.indices) == 0 + assert tgs.groups.keys.colnames == ["a"] + + def test_table_filter(): """ Table groups filtering diff --git a/docs/changes/table/20031.bugfix.rst b/docs/changes/table/20031.bugfix.rst new file mode 100644 index 000000000000..c80efa7fb013 --- /dev/null +++ b/docs/changes/table/20031.bugfix.rst @@ -0,0 +1,3 @@ +Fixed issue with table groups for zero-length tables in ``astropy.table``. Previously +operations like ``len(tbl.group_by("a").groups)`` or ``len(tbl.groups)`` would fail or +give inconsistent results if the table ``tbl`` had no rows. From 2bd869fb34cb98e11a53e1a6c262e78de2ba9f5f Mon Sep 17 00:00:00 2001 From: Debajeet Mandal Date: Mon, 20 Jul 2026 20:55:11 +0530 Subject: [PATCH 199/199] Backport PR #20008: Fix weighted histogram failure when range is specified --- astropy/stats/histogram.py | 16 ++++++++++------ astropy/stats/tests/test_histogram.py | 8 ++++++++ docs/changes/stats/20008.bugfix.rst | 1 + 3 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 docs/changes/stats/20008.bugfix.rst diff --git a/astropy/stats/histogram.py b/astropy/stats/histogram.py index e5b6bc6de7db..c4fcf59a4788 100644 --- a/astropy/stats/histogram.py +++ b/astropy/stats/histogram.py @@ -61,14 +61,17 @@ def calculate_bin_edges( bins : ndarray Histogram bin edges """ - # if range is specified, we need to truncate the data for - # the bin-finding routines - if range is not None: - a = a[(a >= range[0]) & (a <= range[1])] - # if bins is a string, first compute bin edges with the desired heuristic if isinstance(bins, str): + # Astropy's bin-width estimators (knuth, scott, freedman, blocks) + # compute bin widths from the data distribution, so they should only + # consider values within the requested range. Filter `a` here. + # + # Weights are not supported for these estimators (see the + # NotImplementedError below), so they do not need to be filtered. a = np.asarray(a).ravel() + if range is not None: + a = a[(a >= range[0]) & (a <= range[1])] # TODO: if weights is specified, we need to modify things. # e.g. we could use point measures fitness for Bayesian blocks @@ -99,7 +102,8 @@ def calculate_bin_edges( bins[-1] = range[1] elif np.ndim(bins) == 0: - # Number of bins was given + # Number of bins was given. Delegate range and weight handling + # to NumPy's histogram_bin_edges() bins = np.histogram_bin_edges(a, bins, range=range, weights=weights) return bins diff --git a/astropy/stats/tests/test_histogram.py b/astropy/stats/tests/test_histogram.py index 0c0432f51803..ef8d814fd3b9 100644 --- a/astropy/stats/tests/test_histogram.py +++ b/astropy/stats/tests/test_histogram.py @@ -182,3 +182,11 @@ def test_histogram_badargs(): # bad bins arg gives ValueError with pytest.raises(ValueError): histogram(x, bins="bad_argument") + + +def test_histogram_range_weights(): + data = np.array([-10.0, 2.5, 5.0, 7.5, 50.0]) + weights = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + hist, edges = histogram(data, bins=5, range=(0, 10), weights=weights) + assert_allclose(hist, [0.0, 2.0, 3.0, 4.0, 0.0]) + assert_allclose(edges, [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]) diff --git a/docs/changes/stats/20008.bugfix.rst b/docs/changes/stats/20008.bugfix.rst new file mode 100644 index 000000000000..af019f2fda51 --- /dev/null +++ b/docs/changes/stats/20008.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug where ``astropy.stats.histogram()`` raised a ``ValueError`` when both ``range`` and ``weights`` were provided with a scalar number of bins. For scalar bin counts, ``calculate_bin_edges()`` now delegates range filtering directly to ``numpy.histogram_bin_edges()``, avoiding an array shape mismatch between data and weights while reserving manual range filtering for Astropy's string-based bin width estimators.