diff --git a/CHANGES.rst b/CHANGES.rst index f1842b0821..568d9f29b0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -52,6 +52,8 @@ mast - Raise informative error if ``MastMissions`` query radius is too large. [#3447] +- Separate requests for moving target cutouts in ``Tesscut`` to one per sector. [#3467] + jplspec ^^^^^^^ diff --git a/astroquery/mast/cutouts.py b/astroquery/mast/cutouts.py index 8ce8a342ce..b76751bab9 100644 --- a/astroquery/mast/cutouts.py +++ b/astroquery/mast/cutouts.py @@ -156,6 +156,32 @@ def _validate_product(self, product): if product.upper() != "SPOC": raise InvalidQueryError("Input product must be SPOC.") + def _get_moving_target_sectors(self, objectname, mt_type=None): + """ + Helper method to fetch unique sectors for a moving target + + Parameters + ---------- + objectname : str + The name or ID of the moving target. + mt_type : str, optional + The moving target type (majorbody or smallbody). + + Returns + ------- + sectors : list or None + Sorted list of unique sector numbers, or None if no sectors are available. + """ + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=NoResultsWarning) + sector_table = self.get_sectors(objectname=objectname, moving_target=True, mt_type=mt_type) + + if len(sector_table) == 0: + warnings.warn("Coordinates are not in any TESS sector.", NoResultsWarning) + return None + + return sorted(set(sector_table["sector"])) + @deprecated_renamed_argument('product', None, since='0.4.11', message='Tesscut no longer supports operations on ' 'TESS Image Calibrator (TICA) products. ' 'The `product` argument is deprecated and will be removed in a future version.') @@ -272,6 +298,9 @@ def download_cutouts(self, *, coordinates=None, size=5, sector=None, product='SP Optional. The TESS sector to return the cutout from. If not supplied, cutouts from all available sectors on which the coordinate appears will be returned. + + NOTE: For moving targets, if sector is not specified, the method will automatically + fetch all available sectors and make individual requests per sector. product : str Deprecated. Default is 'SPOC'. The product that the cutouts will be made out of. The only valid value for this parameter is 'SPOC', for the @@ -314,6 +343,34 @@ def download_cutouts(self, *, coordinates=None, size=5, sector=None, product='SP """ self._validate_product(product) self._validate_target_input(coordinates, objectname, moving_target) + + # For moving targets without a sector specified, fetch sectors first and make + # individual requests per sector to reduce memory pressure on the service + if moving_target and sector is None: + localpath_table = Table(names=["Local Path"], dtype=[str]) + unique_sectors = self._get_moving_target_sectors(objectname, mt_type) + + if unique_sectors is None: + return localpath_table + + # Make individual requests per sector and combine results + all_paths = [] + for sect in unique_sectors: + manifest = self.download_cutouts( + size=size, + sector=sect, + path=path, + inflate=inflate, + objectname=objectname, + moving_target=True, + mt_type=mt_type, + verbose=verbose, + ) + all_paths.extend(manifest["Local Path"]) + + localpath_table["Local Path"] = all_paths + return localpath_table + params = _parse_cutout_size(size) if sector: @@ -395,6 +452,9 @@ def get_cutouts(self, *, coordinates=None, size=5, product='SPOC', sector=None, Optional. The TESS sector to return the cutout from. If not supplied, cutouts from all available sectors on which the coordinate appears will be returned. + + NOTE: For moving targets, if sector is not specified, the method will automatically + fetch all available sectors and make individual requests per sector. objectname : str, optional The target around which to search, by name (objectname="M104") or TIC ID (objectname="TIC 141914082"). If moving_target is True, input must be the name or ID @@ -425,6 +485,23 @@ def get_cutouts(self, *, coordinates=None, size=5, product='SPOC', sector=None, self._validate_product(product) self._validate_target_input(coordinates, objectname, moving_target) + # For moving targets without a sector specified, fetch sectors first and make + # individual requests per sector to reduce memory pressure on the service + if moving_target and sector is None: + unique_sectors = self._get_moving_target_sectors(objectname, mt_type) + + if unique_sectors is None: + return [] + + # Make individual requests per sector and combine results + all_cutouts = [] + for sect in unique_sectors: + cutouts = self.get_cutouts( + size=size, sector=sect, objectname=objectname, moving_target=True, mt_type=mt_type + ) + all_cutouts.extend(cutouts) + return all_cutouts + params = _parse_cutout_size(size) if sector: params["sector"] = sector diff --git a/astroquery/mast/tests/test_mast.py b/astroquery/mast/tests/test_mast.py index 7929afb378..c6fdaea822 100644 --- a/astroquery/mast/tests/test_mast.py +++ b/astroquery/mast/tests/test_mast.py @@ -1321,6 +1321,69 @@ def test_tesscut_get_cutouts(patch_post, tmpdir): assert "Input product must be SPOC." in str(invalid_query.value) +def test_tesscut_get_cutouts_mt_no_sector(patch_post): + """Test get_cutouts with moving target but no sector specified. + + When sector is not specified for moving targets, the method should + automatically fetch available sectors and make individual requests per sector. + """ + # Moving target without specifying sector - should automatically fetch sectors + cutout_hdus_list = mast.Tesscut.get_cutouts(objectname="Eleonora", moving_target=True, mt_type="small_body", size=5) + assert isinstance(cutout_hdus_list, list) + # Mock returns 1 sector, so we expect 1 cutout + assert len(cutout_hdus_list) == 1 + assert isinstance(cutout_hdus_list[0], fits.HDUList) + + +def test_tesscut_download_cutouts_mt_no_sector(patch_post, tmpdir): + """Test download_cutouts with moving target but no sector specified. + + When sector is not specified for moving targets, the method should + automatically fetch available sectors and make individual requests per sector. + """ + # Moving target without specifying sector - should automatically fetch sectors + manifest = mast.Tesscut.download_cutouts( + objectname="Eleonora", moving_target=True, mt_type="small_body", size=5, path=str(tmpdir) + ) + assert isinstance(manifest, Table) + # Mock returns 1 sector, so we expect 1 file + assert len(manifest) == 1 + assert manifest["Local Path"][0][-4:] == "fits" + assert os.path.isfile(manifest[0]["Local Path"]) + + +def test_tesscut_get_cutouts_mt_no_sector_empty_results(patch_post, monkeypatch): + """Test get_cutouts with moving target when no sectors are available. + + When get_sectors returns an empty table, the method should warn and return an empty list. + """ + # Mock get_sectors to return an empty Table + empty_sector_table = Table(names=["sectorName", "sector", "camera", "ccd"], dtype=[str, int, int, int]) + monkeypatch.setattr(mast.Tesscut, "get_sectors", lambda *args, **kwargs: empty_sector_table) + + with pytest.warns(NoResultsWarning, match="Coordinates are not in any TESS sector"): + cutout_hdus_list = mast.Tesscut.get_cutouts(objectname="NonExistentObject", moving_target=True, size=5) + assert isinstance(cutout_hdus_list, list) + assert len(cutout_hdus_list) == 0 + + +def test_tesscut_download_cutouts_mt_no_sector_empty_results(patch_post, tmpdir, monkeypatch): + """Test download_cutouts with moving target when no sectors are available. + + When get_sectors returns an empty table, the method should warn and return an empty Table. + """ + # Mock get_sectors to return an empty Table + empty_sector_table = Table(names=["sectorName", "sector", "camera", "ccd"], dtype=[str, int, int, int]) + monkeypatch.setattr(mast.Tesscut, "get_sectors", lambda *args, **kwargs: empty_sector_table) + + with pytest.warns(NoResultsWarning, match="Coordinates are not in any TESS sector"): + manifest = mast.Tesscut.download_cutouts( + objectname="NonExistentObject", moving_target=True, size=5, path=str(tmpdir) + ) + assert isinstance(manifest, Table) + assert len(manifest) == 0 + + ###################### # ZcutClass tests # ###################### diff --git a/astroquery/mast/tests/test_mast_remote.py b/astroquery/mast/tests/test_mast_remote.py index 0d4be9e961..792a739bbd 100644 --- a/astroquery/mast/tests/test_mast_remote.py +++ b/astroquery/mast/tests/test_mast_remote.py @@ -1458,6 +1458,36 @@ def test_tesscut_get_cutouts_mt(self): Tesscut.get_cutouts(objectname=moving_target_name) assert error_nameresolve in str(error_msg.value) + def test_tesscut_get_cutouts_mt_no_sector(self): + """Test get_cutouts with moving target but no sector specified. + + When sector is not specified for moving targets, the method should + automatically fetch available sectors and make individual requests per sector. + """ + moving_target_name = "Eleonora" + # Moving target without specifying sector - should automatically fetch sectors + cutout_hdus_list = Tesscut.get_cutouts(objectname=moving_target_name, moving_target=True, size=1) + assert isinstance(cutout_hdus_list, list) + # Should return cutouts for all available sectors + assert len(cutout_hdus_list) >= 1 + assert isinstance(cutout_hdus_list[0], fits.HDUList) + + def test_tesscut_download_cutouts_mt_no_sector(self, tmpdir): + """Test download_cutouts with moving target but no sector specified. + + When sector is not specified for moving targets, the method should + automatically fetch available sectors and make individual requests per sector. + """ + moving_target_name = "Eleonora" + # Moving target without specifying sector - should automatically fetch sectors + manifest = Tesscut.download_cutouts(objectname=moving_target_name, moving_target=True, size=1, path=str(tmpdir)) + assert isinstance(manifest, Table) + # Should return files for all available sectors + assert len(manifest) >= 1 + assert manifest["Local Path"][0][-4:] == "fits" + for row in manifest: + assert os.path.isfile(row["Local Path"]) + ################### # ZcutClass tests # ###################