Repository: astropy/astropy

Issue:
Deprecate Time.ptp() method?
### What is the problem this feature will solve?

Numpy has ~deprecated~ removed the `ndarray.ptp()` method. This also means the `np.ptp()` function will no longer look for a `.ptp()` method, so will no longer work on `Time`. It seems to make sense to deprecate it on `Time` too (plain removal seems wrong; not sure why numpy didn't deprecate).

### Describe the desired outcome

The `Time` methods that are meant to mimic those of `ndarray` are kept up to date. We also ensure that corresponding numpy functions work on `Time` instances.

### Additional context

See #15378 for other updates related to the removal of `ndarray.ptp()` in numpy 2.0.

Discussion:
github-actions[bot]: Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  - [ ] Do the proposed changes actually accomplish desired goals?
  - [ ] Do the proposed changes follow the [Astropy coding guidelines](https://docs.astropy.org/en/latest/development/codeguide.html)?
  - [ ] Are tests added/updated as required? If so, do they follow the [Astropy testing guidelines](https://docs.astropy.org/en/latest/development/testguide.html)?
  - [ ] Are docs added/updated as required? If so, do they follow the [Astropy documentation guidelines](https://docs.astropy.org/en/latest/development/docguide.html#astropy-documentation-rules-and-guidelines)?
  - [ ] Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for [rebase](https://docs.astropy.org/en/latest/development/workflow/development_workflow.html#rebase-if-necessary) and [squash](https://docs.astropy.org/en/latest/development/workflow/development_workflow.html#squash-if-necessary).
  - [ ] Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the [bot](https://docs.astropy.org/en/latest/development/workflow/development_workflow.html#pre-commit).
  - [ ] Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  - [ ] 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.
github-actions[bot]: 👋 Thank you for your draft pull request! Do you know that you can use `[ci skip]` or `[skip ci]` in your commit messages to skip running continuous integration tests until you are ready?
neutrinoceros: There's one related failure in devdeps where `Time.ptp` is used in a doctest. I'm not sure what's the best way to fix this: should the example rely on deprecated API or should it require numpy 2.0 ? none of these strategies seem ideal to me, but maybe there's a precedent somewhere that I'm just forgetting ?
mhvk: I think new PR(s) are indeed preferable.
mhvk: Sorry, I'm not looking where I'm commenting!

Patch:
diff --git a/astropy/modeling/powerlaws.py b/astropy/modeling/powerlaws.py
index cacda4e7ec..be80ef0169 100644
--- a/astropy/modeling/powerlaws.py
+++ b/astropy/modeling/powerlaws.py
@@ -593,7 +593,7 @@ class Schechter1D(Fittable1DModel):
         ax.set_xlim(-22.6, -17)
         ax.set_ylim(1.e-7, 1.e-2)
         ax.set_xlabel('$M_{UV}$')
-        ax.set_ylabel(r'$\phi$ [mag$^{-1}$ Mpc$^{-3}]$')
+        ax.set_ylabel('$\phi$ [mag$^{-1}$ Mpc$^{-3}]$')
 
     References
     ----------
diff --git a/astropy/time/core.py b/astropy/time/core.py
index 43468a56ae..84a11bc012 100644
--- a/astropy/time/core.py
+++ b/astropy/time/core.py
@@ -29,8 +29,9 @@ from astropy import units as u
 from astropy.extern import _strptime
 from astropy.units import UnitConversionError
 from astropy.utils import ShapedLikeNDArray, lazyproperty
-from astropy.utils.compat import COPY_IF_NEEDED, sanitize_copy_arg
+from astropy.utils.compat import COPY_IF_NEEDED, NUMPY_LT_2_0, sanitize_copy_arg
 from astropy.utils.data_info import MixinInfo, data_info_factory
+from astropy.utils.decorators import deprecated
 from astropy.utils.exceptions import AstropyDeprecationWarning, AstropyWarning
 from astropy.utils.masked import Masked
 
@@ -1633,6 +1634,26 @@ class TimeBase(ShapedLikeNDArray):
             )
         return self[self._advanced_index(self.argmax(axis), axis, keepdims)]
 
+    def _ptp_impl(self, axis=None, out=None, keepdims=False):
+        if out is not None:
+            raise ValueError(
+                "Since `Time` instances are immutable, ``out`` "
+                "cannot be set to anything but ``None``."
+            )
+        return self.max(axis, keepdims=keepdims) - self.min(axis, keepdims=keepdims)
+
+    if NUMPY_LT_2_0:
+        _ptp_decorator = lambda f: f
+    else:
+        _ptp_decorator = deprecated("6.1", alternative="np.ptp")
+
+        def __array_function__(self, function, types, args, kwargs):
+            if function is np.ptp:
+                return self._ptp_impl(*args[1:], **kwargs)
+            else:
+                return super().__array_function__(function, types, args, kwargs)
+
+    @_ptp_decorator
     def ptp(self, axis=None, out=None, keepdims=False):
         """Peak to peak (maximum - minimum) along a given axis.
 
@@ -1644,12 +1665,7 @@ class TimeBase(ShapedLikeNDArray):
         `~numpy.ptp`; since `Time` instances are immutable, it is not possible
         to have an actual ``out`` to store the result in.
         """
-        if out is not None:
-            raise ValueError(
-                "Since `Time` instances are immutable, ``out`` "
-                "cannot be set to anything but ``None``."
-            )
-        return self.max(axis, keepdims=keepdims) - self.min(axis, keepdims=keepdims)
+        return self._ptp_impl(axis, out, keepdims)
 
     def sort(self, axis=-1):
         """Return a copy sorted along the specified axis.
diff --git a/astropy/units/core.py b/astropy/units/core.py
index d8f2b5d906..9233433bd3 100644
--- a/astropy/units/core.py
+++ b/astropy/units/core.py
@@ -1718,9 +1718,7 @@ class UnitBase:
             max_depth=1,
             include_prefix_units=include_prefix_units,
         )
-        results = {
-            x.bases[0] for x in results if len(x.bases) == 1 and x.powers[0] == 1
-        }
+        results = {x.bases[0] for x in results if len(x.bases) == 1}
         return self.EquivalentUnitsList(results)
 
     def is_unity(self):
diff --git a/docs/changes/time/16212.api.rst b/docs/changes/time/16212.api.rst
new file mode 100644
index 0000000000..b97d0f0f4f
--- /dev/null
+++ b/docs/changes/time/16212.api.rst
@@ -0,0 +1,2 @@
+Following the removal of ``np.ndarray.ptp`` in Numpy v2, ``Time.ptp`` is now
+deprecated in favor of ``np.ptp``.
diff --git a/docs/changes/units/16127.bugfix.rst b/docs/changes/units/16127.bugfix.rst
deleted file mode 100644
index 9b3e2caec5..0000000000
--- a/docs/changes/units/16127.bugfix.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Ensure that ``find_equivalent_units`` only returns actual units, not units
-that raised to some power match the requested one.  With this fix,
-``(u.m**-3).find_equivalent_units()`` properly finds nothing, rather than all
-units of length.
diff --git a/docs/modeling/compound-models.rst b/docs/modeling/compound-models.rst
index 44e9f316f4..feebcf4d5f 100644
--- a/docs/modeling/compound-models.rst
+++ b/docs/modeling/compound-models.rst
@@ -423,7 +423,7 @@ example:
         plt.imshow(g(x, y), origin='lower')
         plt.xticks([])
         plt.yticks([])
-        plt.title(rf'Rotated $ {theta}^\circ $')
+        plt.title(f'Rotated $ {theta}^\circ $')
 
 .. note::
 
diff --git a/docs/time/index.rst b/docs/time/index.rst
index e16c658f5b..7b248d3b18 100644
--- a/docs/time/index.rst
+++ b/docs/time/index.rst
@@ -489,8 +489,8 @@ To apply arithmetic methods to |Time| instances::
 
   >>> t.max()
   <Time object: scale='utc' format='mjd' value=50002.5>
-  >>> t.ptp(axis=0)  # doctest: +FLOAT_CMP
-  <TimeDelta object: scale='tai' format='jd' value=[2. 2.]>
+  >>> t.min()
+  <Time object: scale='utc' format='mjd' value=50000.0>
 
 .. EXAMPLE END
 


Test patch:
diff --git a/astropy/time/tests/test_methods.py b/astropy/time/tests/test_methods.py
index f36b1360a8..653620b9c5 100644
--- a/astropy/time/tests/test_methods.py
+++ b/astropy/time/tests/test_methods.py
@@ -3,6 +3,7 @@
 import copy
 import itertools
 import warnings
+from contextlib import nullcontext
 
 import numpy as np
 import pytest
@@ -12,6 +13,8 @@ from astropy.time import Time
 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_0
+from astropy.utils.exceptions import AstropyDeprecationWarning
 
 needs_array_function = pytest.mark.xfail(
     not ARRAY_FUNCTION_ENABLED, reason="Needs __array_function__ support"
@@ -632,10 +635,17 @@ class TestArithmetic:
     def test_ptp(self, use_mask):
         self.create_data(use_mask)
 
-        assert self.t0.ptp() == self.t0.max() - self.t0.min()
-        assert np.all(self.t0.ptp(0) == self.t0.max(0) - self.t0.min(0))
-        assert self.t0.ptp(0).shape == (5, 5)
-        assert self.t0.ptp(0, keepdims=True).shape == (1, 5, 5)
+        assert np.ptp(self.t0) == self.t0.max() - self.t0.min()
+        assert np.all(np.ptp(self.t0, axis=0) == self.t0.max(0) - self.t0.min(0))
+        assert np.ptp(self.t0, axis=0).shape == (5, 5)
+        assert np.ptp(self.t0, 0, keepdims=True).shape == (1, 5, 5)
+
+        if NUMPY_LT_2_0:
+            ctx = nullcontext()
+        else:
+            ctx = pytest.warns(AstropyDeprecationWarning)
+        with ctx:
+            assert self.t0.ptp() == self.t0.max() - self.t0.min()
 
     def test_sort(self, use_mask):
         self.create_data(use_mask)
diff --git a/astropy/units/tests/test_equivalencies.py b/astropy/units/tests/test_equivalencies.py
index 6be5b2857e..07336b803e 100644
--- a/astropy/units/tests/test_equivalencies.py
+++ b/astropy/units/tests/test_equivalencies.py
@@ -14,16 +14,6 @@ from astropy.tests.helper import assert_quantity_allclose
 from astropy.units.equivalencies import Equivalency
 
 
-def test_find_equivalent_units():
-    # Only finds units in the namespace
-    e1 = (u.m**3).find_equivalent_units()
-    assert len(e1) == 1
-    assert e1[0] == u.l
-    # Regression test for gh-16124
-    e2 = (u.m**-3).find_equivalent_units()
-    assert len(e2) == 0
-
-
 def test_dimensionless_angles():
     # test that the angles_dimensionless option allows one to change
     # by any order in radian in the unit (#1161)
diff --git a/docs/development/testguide.rst b/docs/development/testguide.rst
index 64f545940b..2e3409bf8b 100644
--- a/docs/development/testguide.rst
+++ b/docs/development/testguide.rst
@@ -110,18 +110,6 @@ This can be used in conjunction with the ``-P`` option provided by the
 `pytest-filter-subpackage <https://github.com/astropy/pytest-filter-subpackage>`_
 plugin to run just part of the test suite.
 
-Note that even though ``tox`` caches information, interactive debug and test
-sessions with ``tox`` can be quite slow. For this case, it may be better to
-set up a virtual environment with an editable install. Here, ``tox`` can still
-help by setting up a complete test environment, which one can then activate::
-
-  tox -e test-alldeps --develop --notest
-  source .tox/test-alldeps/bin/activate
-
-Here, we use ``--notest`` to prevent ``tox`` from running the tests, since the
-idea is to do that oneself -- using the ``pytest`` commands described below,
-targeting the relevant sub-package or test file.
-
 .. _running-pytest:
 
 pytest
@@ -159,22 +147,18 @@ Or, to run only the ``wcs`` and ``utils`` tests::
 
     pytest -P wcs,utils
 
-You can also specify a single directory, a file (``.py`` python or ``.rst``
-doc file), or a specific test to check, or rerun only tests that failed in
-the previous run::
+You can also specify a single directory or file to test from the commandline,
+e.g.::
 
     pytest astropy/modeling
+
+or::
+
     pytest astropy/wcs/tests/test_wcs.py
-    pytest astropy/units -k float_dtype_promotion
-    pytest astropy/units/tests/test_quantity.py::TestQuantityCreation::test_float_dtype_promotion
-    pytest astropy/wcs/index.rst
-    pytest --last-failed
 
-For more details, see the `pytest invocation guide
-<https://docs.pytest.org/en/stable/how-to/usage.html>`_ and the
-description of `caching
-<https://docs.pytest.org/en/stable/how-to/cache.html>`_.
+and this works for ``.rst`` files too::
 
+    pytest astropy/wcs/index.rst
 
 .. _astropy.test():