diff --git a/astrocut/__init__.py b/astrocut/__init__.py index fc30bdd3..8862d5da 100644 --- a/astrocut/__init__.py +++ b/astrocut/__init__.py @@ -28,3 +28,4 @@ class UnsupportedPythonError(Exception): from .cutout_processing import (path_to_footprints, center_on_path, # noqa CutoutsCombiner, build_default_combine_function) # noqa from .asdf_cutouts import asdf_cut, get_center_pixel # noqa + from .footprint_cutouts import cube_cut_from_footprint # noqa diff --git a/astrocut/footprint_cutouts.py b/astrocut/footprint_cutouts.py new file mode 100644 index 00000000..5eb7aee1 --- /dev/null +++ b/astrocut/footprint_cutouts.py @@ -0,0 +1,341 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +"""This module creates cutouts from data cubes found in the cloud.""" + +import os +import re +from typing import List, Union +import warnings +from threading import Lock + +from astropy.coordinates import SkyCoord +from astropy.table import Table, Column +import astropy.units as u +from astropy.utils.exceptions import AstropyWarning +from cachetools import TTLCache, cached +import numpy as np +import requests +from spherical_geometry.polygon import SphericalPolygon + +from astrocut.exceptions import InvalidQueryError +from astrocut.cube_cut import CutoutFactory + +from .utils.utils import parse_size_input + +TESS_ARCSEC_PER_PX = 21 # Number of arcseconds per pixel in a TESS image +FFI_TTLCACHE = TTLCache(maxsize=10, ttl=900) # Cache for FFI footprint files +CUBE_CUT_THREADS = 8 # Number of threads to use in `cube_cut` function + + +def _s_region_to_polygon(s_region: Column): + """ + Takes in a s_region string of type POLYGON and returns it as a spherical_region Polygon. + + Example input: + 'POLYGON 229.80771900 -75.17048500 241.67788000 -63.95992300 269.94872000 -64.39276400 277.87862300 -75.57754400' + """ + + def ind_sregion_to_polygon(s_reg): + sr_list = s_reg.strip().split() + reg_type = sr_list[0].upper() + + if reg_type == 'POLYGON': + ras = np.array(sr_list[1::2], dtype=float) + ras[ras < 0] = ras[ras < 0] + 360 + decs = np.array(sr_list[2::2], dtype=float) + return SphericalPolygon.from_radec(ras, decs) + else: + raise ValueError('Unsupported S_Region type.') + + return np.vectorize(ind_sregion_to_polygon)(s_region) + + +@cached(cache=FFI_TTLCACHE, lock=Lock()) +def get_caom_ffis(product: str = 'SPOC'): + """ + Fetches footprints for Full Frame Images (FFIs) from the Common Archive Observation Model. The resulting + table contains each (FFI) and a 'polygon' column that describes the image's footprints as polygon points + and vectors. + + Parameters + ---------- + product : str, optional + Default 'SPOC'. The product type for which to fetch footprints. + + Returns + ------- + all_ffis : `~astropy.table.Table` + Table containing information about FFIs and their footprints. + """ + # Define the URL and parameters for the query + obs_collection = 'TESS' if product == 'SPOC' else 'HLSP' + target_name = 'TESS FFI' if product == 'SPOC' else 'TICA FFI' + url = 'https://mast.stsci.edu/vo-tap/api/v0.1/caom/sync' + params = { + 'FORMAT': 'csv', + 'LANG': 'ADQL', + 'QUERY': 'SELECT obs_id, t_min, t_max, s_region, target_name, sequence_number ' + 'FROM dbo.ObsPointing ' + f"WHERE obs_collection='{obs_collection}' AND dataproduct_type='image' AND target_name='{target_name}'" + } + + # Send the GET request + response = requests.get(url, params=params) + response.raise_for_status() # Raise HTTPError if response is not 200 + + # Load CSV data into a Table + all_ffis = Table.read(response.text, format='csv') + all_ffis.sort('obs_id') + + # Convert regions to polygons + all_ffis['polygon'] = _s_region_to_polygon(all_ffis['s_region']) + + return all_ffis + + +def _ffi_intersect(ffi_list: Table, polygon: SphericalPolygon): + """ + Vectorizing the spherical_coordinate intersects_polygon function + """ + def single_intersect(ffi, polygon): + return ffi.intersects_poly(polygon) + + return np.vectorize(single_intersect)(ffi_list['polygon'], polygon) + + +def ra_dec_crossmatch(all_ffis: Table, coordinates: SkyCoord, cutout_size, arcsec_per_px: int = TESS_ARCSEC_PER_PX): + """ + Returns the Full Frame Images (FFIs) whose footprints overlap with a cutout of a given position and size. + + Parameters + ---------- + all_ffis : `~astropy.table.Table` + Table of FFIs to crossmatch with the cutout. + coordinates : str or `astropy.coordinates.SkyCoord` object + The position around which to cutout. + It may be specified as a string ("ra dec" in degrees) + or as the appropriate `~astropy.coordinates.SkyCoord` object. + cutout_size : int, array-like, `~astropy.units.Quantity` + The size of the cutout array. If ``cutout_size`` + is a scalar number or a scalar `~astropy.units.Quantity`, + then a square cutout of ``cutout_size`` will be created. If + ``cutout_size`` has two elements, they should be in ``(ny, nx)`` + order. Scalar numbers in ``cutout_size`` are assumed to be in + units of pixels. `~astropy.units.Quantity` objects must be in pixel or + angular units. + arcsec_per_px : int, optional + Default 21. The number of arcseconds per pixel in an image. Used to determine + the footprint of the cutout. Default is the number of arcseconds per pixel in + a TESS image. + + Returns + ------- + matching_ffis : `~astropy.table.Table` + Table containing information about FFIs whose footprints overlap those of the cutout. + """ + # Convert coordinates to SkyCoord + if not isinstance(coordinates, SkyCoord): + coordinates = SkyCoord(coordinates, unit='deg') + + # Parse cutout size + cutout_size = parse_size_input(cutout_size) + + ra, dec = coordinates.ra, coordinates.dec + ffi_inds = [] + + # Create polygon for intersection + # Convert dimensions from pixels to arcseconds and divide by 2 to get offset from center + ra_offset = ((cutout_size[0] * arcsec_per_px) / 2) * u.arcsec + dec_offset = ((cutout_size[1] * arcsec_per_px) / 2) * u.arcsec + + # Calculate RA and Dec boundaries + ra_bounds = [ra - ra_offset, ra + ra_offset] + dec_bounds = [dec - dec_offset, dec + dec_offset] + + # Get RA and Dec for four corners of rectangle + ras = [ra_bounds[0].value, ra_bounds[1].value, ra_bounds[1].value, ra_bounds[0].value] + decs = [dec_bounds[0].value, dec_bounds[0].value, dec_bounds[1].value, dec_bounds[1].value] + + # Create SphericalPolygon for comparison + cutout_fp = SphericalPolygon.from_radec(ras, decs, center=(ra, dec)) + ffi_inds = _ffi_intersect(all_ffis, cutout_fp) + + return all_ffis[ffi_inds] + + +def _extract_sequence_information(sector_name: str, product: str): + """Extract the sector, camera, and ccd information from the sector name""" + if product == 'SPOC': + pattern = re.compile(r"(tess-s)(?P\d{4})-(?P\d{1,4})-(?P\d{1,4})") + elif product == 'TICA': + pattern = re.compile(r"(hlsp_tica_s)(?P\d{4})-(cam)(?P\d{1,4})-(ccd)(?P\d{1,4})") + else: + return {} + sector_match = re.match(pattern, sector_name) + + if not sector_match: + return {} + + sector = sector_match.group("sector") + camera = sector_match.group("camera") + ccd = sector_match.group("ccd") + + # Rename the TICA sector because of the naming convention in Astrocut + if product == 'TICA': + sector_name = f"tica-s{sector}-{camera}-{ccd}" + + return {"sectorName": sector_name, "sector": sector, "camera": camera, "ccd": ccd} + + +def _create_sequence_list(observations: Table, product: str): + """Extracts sequence information from a list of observations""" + target_name = "TESS FFI" if product == 'SPOC' else "TICA FFI" + obs_filtered = [obs for obs in observations if obs["target_name"].upper() == target_name] + + sequence_results = [] + for row in obs_filtered: + sequence_extraction = _extract_sequence_information(row["obs_id"], product=product) + if sequence_extraction: + sequence_results.append(sequence_extraction) + + return sequence_results + + +def _get_cube_files_from_sequence_obs(sequences: list): + """Convert obs_id sequence information into cube file names""" + cube_files = [ + { + "folder": "s" + sector["sector"].rjust(4, "0"), + "cube": sector["sectorName"] + "-cube.fits", + "sectorName": sector["sectorName"], + } + for sector in sequences + ] + return cube_files + + +def cube_cut_from_footprint(coordinates: Union[str, SkyCoord], cutout_size, + sequence: Union[int, List[int], None] = None, product: str = 'SPOC', + output_dir: str = '.', verbose: bool = False): + """ + Generates cutouts around `coordinates` of size `cutout_size` from image cube files hosted on the S3 cloud. + + Parameters + ---------- + coordinates : str or `astropy.coordinates.SkyCoord` object + The position around which to cutout. + It may be specified as a string ("ra dec" in degrees) + or as the appropriate `~astropy.coordinates.SkyCoord` object. + cutout_size : int, array-like, `~astropy.units.Quantity` + The size of the cutout array. If ``cutout_size`` + is a scalar number or a scalar `~astropy.units.Quantity`, + then a square cutout of ``cutout_size`` will be created. If + ``cutout_size`` has two elements, they should be in ``(ny, nx)`` + order. Scalar numbers in ``cutout_size`` are assumed to be in + units of pixels. `~astropy.units.Quantity` objects must be in pixel or + angular units. + sequence : int, List[int], optional + Default None. Sequence(s) from which to generate cutouts. Can provide a single + sequence number as an int or a list of sequence numbers. If not specified, + cutouts will be generated from all sequences that contain the cutout. + For the TESS mission, this parameter corresponds to sectors. + product : str, optional + Default 'SPOC'. The product type to make the cutouts from. + output_dir : str, optional + Default '.'. The path to which output files are saved. + The current directory is default. + verbose : bool, optional + Default False. If True, intermediate information is printed. + + Returns + ------- + cutout_files : list + List of paths to cutout files. + + Examples + -------- + >>> from astrocut.footprint_cutouts import cube_cut_from_footprint + >>> cube_cut_from_footprint( #doctest: +SKIP + ... coordinates='83.40630967798376 -62.48977125108528', + ... cutout_size=64, + ... sequence=[1, 2], # TESS sectors + ... product='SPOC', + ... output_dir='./cutouts') + ['./cutouts/tess-s0001-4-4/tess-s0001-4-4_83.406310_-62.489771_64x64_astrocut.fits', + './cutouts/tess-s0002-4-1/tess-s0002-4-1_83.406310_-62.489771_64x64_astrocut.fits'] + """ + + # Convert to SkyCoord + if not isinstance(coordinates, SkyCoord): + coordinates = SkyCoord(coordinates, unit='deg') + if verbose: + print('Coordinates:', coordinates) + + # Parse cutout size + cutout_size = parse_size_input(cutout_size) + if verbose: + print('Cutout size:', cutout_size) + + # Get FFI footprints from the cloud + # s3_uri = 's3://tesscut-ops-footprints/tess_ffi_footprint_cache.json' if product == 'SPOC' \ + # else 's3://tesscut-ops-footprints/tica_ffi_footprint_cache.json' + # all_ffis = _get_s3_ffis(s3_uri=s3_uri, as_table=True, load_polys=True) + all_ffis = get_caom_ffis(product) + if verbose: + print(f'Found {len(all_ffis)} footprint files.') + + # Filter FFIs by provided sectors + if sequence: + # Convert to list + if isinstance(sequence, int): + sequence = [sequence] + all_ffis = all_ffis[np.isin(all_ffis['sequence_number'], sequence)] + + if len(all_ffis) == 0: + raise InvalidQueryError('No FFI cube files were found for sequences: ' + + ', '.join(str(s) for s in sequence)) + + if verbose: + print(f'Filtered to {len(all_ffis)} footprints for sequences: {", ".join(str(s) for s in sequence)}') + + # Get sector names and cube files that contain the cutout + cone_results = ra_dec_crossmatch(all_ffis, coordinates, cutout_size, TESS_ARCSEC_PER_PX) + if not cone_results: + raise InvalidQueryError('The given coordinates were not found within the specified sequence(s).') + seq_list = _create_sequence_list(cone_results, product) + cube_files_mapping = _get_cube_files_from_sequence_obs(seq_list) + if verbose: + print(f'Found {len(cube_files_mapping)} matching cube files.') + base_file_path = "s3://stpubdata/tess/public/mast/" if product == 'SPOC' \ + else "s3://stpubdata/tess/public/mast/tica/" + + # Make sure the output directory exists + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # Executor function to generate cutouts from a cube file + def process_file(file): + try: + factory = CutoutFactory() + file_path = os.path.join(base_file_path, file['cube']) + output_path = os.path.join(output_dir, file['sectorName']) + cutout = factory.cube_cut( + file_path, + coordinates, + cutout_size=cutout_size, + product=product, + output_path=output_path, + threads=CUBE_CUT_THREADS, + verbose=verbose + ) + return cutout + except Exception as e: + warnings.warn(f'Unable to generate cutout from {file_path}: {e}', AstropyWarning) + return None + + # Generate cutout from each cube file + if verbose: + print('Generating cutouts...') + cutout_files = [process_file(file) for file in cube_files_mapping] + + return cutout_files diff --git a/astrocut/tests/test_footprint_cutouts.py b/astrocut/tests/test_footprint_cutouts.py new file mode 100644 index 00000000..b4585a53 --- /dev/null +++ b/astrocut/tests/test_footprint_cutouts.py @@ -0,0 +1,154 @@ +import re +import pytest + +from astrocut.exceptions import InvalidQueryError +from astrocut.footprint_cutouts import (cube_cut_from_footprint, _extract_sequence_information, _s_region_to_polygon, + get_caom_ffis, _ffi_intersect, ra_dec_crossmatch, _create_sequence_list) +from astropy.coordinates import SkyCoord +from astropy.io import fits +from astropy.table import Table +from spherical_geometry.polygon import SphericalPolygon + + +def check_output_file(path, ffi_type, sequences=[]): + """Helper function to check the validity of output cutout files""" + # Check that cutout path point to valid FITS file + tpf = fits.open(path) + tpf_table = tpf[1].data + + # SPOC cutouts have 1 extra columns in EXT 1 + ncols = 12 if ffi_type == 'SPOC' else 11 + assert len(tpf_table.columns) == ncols + assert tpf_table[0]['FLUX'].shape == (5, 5) + + # Check that sector matches a provided sequence + if sequences: + assert tpf[0].header['SECTOR'] in sequences + + tpf.close() + + +def test_s_region_to_polygon_unsupported_region(): + """Test that ValueError is raised if s_region is not a polygon""" + s_region = 'CIRCLE' + err = 'Unsupported S_Region type.' + with pytest.raises(ValueError, match=err): + _s_region_to_polygon(s_region) + + +@pytest.mark.parametrize("lon, lat, center, expected", [ + ((345, 355, 355, 345), (-15, -15, -5, -5), (350, -10), True), # intersecting + ((335, 345, 345, 335), (-15, -15, -5, -5), (340, -10), False), # non-intersecting + ((340, 350, 350, 340), (-15, -15, -5, -5), (345, -10), True), # edge object that intersects + ((340, 349, 349, 340), (-15, -15, -5, -5), (345, -10), False), # edge object that does not intersect +]) +def test_ffi_intersect(lon, lat, center, expected): + """Test that FFI intersection with cutout outputs proper results.""" + # SphericalPolygon object for cutout + cutout_sp = SphericalPolygon.from_radec(lon=(350, 10, 10, 350), + lat=(-10, -10, 10, 10), + center=(0, 0)) + + # Create a SphericalPolygon with the parametrized lon, lat, and center + polygon = SphericalPolygon.from_radec(lon=lon, lat=lat, center=center) + + # Create a table with this polygon + polygon_table = Table(names=['polygon'], dtype=[SphericalPolygon]) + polygon_table['polygon'] = [polygon] + + # Perform the intersection check + intersection = _ffi_intersect(polygon_table, cutout_sp) + + # Assert the intersection result matches the expected value + assert intersection.value[0] == expected + + +def test_extract_sequence_information_unknown_product(): + """Test that an empty dict is returned if product is not recognized""" + info = _extract_sequence_information('tess-s0044-4-1', product='UNKNOWN') + assert info == {} + + info = _extract_sequence_information('tess-s0044-4-1', product=None) + assert info == {} + + +def test_extract_sequence_information_no_match(): + """Test that an empty dict is returned if name does not match product pattern""" + info = _extract_sequence_information('tess-s0044-4-1', product='TICA') + assert info == {} + + +@pytest.mark.parametrize('ffi_type', ['SPOC', 'TICA']) +def test_cube_cut_from_footprint(tmpdir, capsys, ffi_type): + """Test that data cube is cut from FFI file using parallel processing""" + cutout = cube_cut_from_footprint(coordinates='130 30', + cutout_size=5, + product=ffi_type, + output_dir=tmpdir, + sequence=44, + verbose=True) + + # Assert that messages were printed + captured = capsys.readouterr() + output = captured.out + assert 'Coordinates:' in output + assert 'Cutout size: [5 5]' in output + assert re.search(r'Found \d+ footprint files.', output) + assert re.search(r'Filtered to \d+ footprints for sequences: 44', output) + assert re.search(r'Found \d+ matching cube files.', output) + assert 'Generating cutouts...' in output + check_output_file(cutout[0], ffi_type, [44]) + + +def test_cube_cut_from_footprint_multi_sequence(tmpdir): + """Test that a cube is created for each sequence when multiple are provided""" + sequences = [1, 13] + cutouts = cube_cut_from_footprint(coordinates='350 -80', + cutout_size=5, + product='SPOC', + output_dir=tmpdir, + sequence=sequences) + + assert len(cutouts) == 2 + for path in cutouts: + check_output_file(path, 'SPOC', sequences) + + +def test_cube_cut_from_footprint_all_sequences(tmpdir): + """Test that cubes are created for all sequences that intersect the cutout""" + # Create cutouts for all possible sequences + coordinates = SkyCoord('350 -80', unit='deg') + cutout_size = (5, 5) + product = 'SPOC' + cutouts = cube_cut_from_footprint(coordinates=coordinates, + cutout_size=cutout_size, + product=product, + output_dir=tmpdir) + + # Crossmatch to get sectors that contain cutout + all_ffis = get_caom_ffis(product) + cone_results = ra_dec_crossmatch(all_ffis, coordinates, cutout_size, 21) + seq_list = _create_sequence_list(cone_results, product) + sequences = [int(seq['sector']) for seq in seq_list] + + assert len(seq_list) == len(cutouts) + for path in cutouts: + check_output_file(path, 'SPOC', sequences) + + +def test_cube_cut_from_footprint_invalid_sequence(): + """Test that InvalidQueryError is raised if sequence does not have cube files""" + err = 'No FFI cube files were found for sequences: -1' + with pytest.raises(InvalidQueryError, match=err): + cube_cut_from_footprint(coordinates='130 30', + cutout_size=5, + sequence=-1) + + +def test_cube_cut_from_footprint_outside_coords(): + """Test that InvalidQueryError is raised if coordinates are not found in sequence""" + err = 'The given coordinates were not found within the specified sequence(s).' + with pytest.raises(InvalidQueryError, match=re.escape(err)): + cube_cut_from_footprint(coordinates='130 30', + cutout_size=5, + sequence=1) diff --git a/astrocut/tests/test_utils.py b/astrocut/tests/test_utils.py index 22069ecb..762e0bda 100644 --- a/astrocut/tests/test_utils.py +++ b/astrocut/tests/test_utils.py @@ -5,6 +5,9 @@ from astropy.coordinates import SkyCoord from astropy import units as u from astropy.utils.data import get_pkg_data_filename +import pytest + +from astrocut.exceptions import InputWarning, InvalidQueryError from ..utils import utils @@ -13,7 +16,41 @@ with open(get_pkg_data_filename('data/ex_ffi_wcs.txt'), "r") as FLE: WCS_STR = FLE.read() + +@pytest.mark.parametrize("input_value, expected", [ + (5, np.array((5, 5))), # scalar + (10 * u.pix, np.array((10, 10)) * u.pix), # Astropy quantity + ((5, 10), np.array((5, 10))), # tuple + ([10, 5], np.array((10, 5))), # list + (np.array((5, 10)), np.array((5, 10))), # array +]) +def test_parse_size_input(input_value, expected): + """Test that different types of input are accurately parsed into cutout sizes.""" + cutout_size = utils.parse_size_input(input_value) + assert np.array_equal(cutout_size, expected) + + +def test_parse_size_input_dimension_warning(): + """Test that a warning is output when input has too many dimensions""" + warning = "Too many dimensions in cutout size, only the first two will be used." + with pytest.warns(InputWarning, match=warning): + cutout_size = utils.parse_size_input((5, 5, 10)) + assert np.array_equal(cutout_size, np.array((5, 5))) + + +def test_parse_size_input_invalid(): + """Test that an error is raised when one of the size dimensions is not positive""" + err = ('Cutout size dimensions must be greater than zero.') + with pytest.raises(InvalidQueryError, match=err): + utils.parse_size_input(0) + + with pytest.raises(InvalidQueryError, match=err): + utils.parse_size_input((0, 5)) + + with pytest.raises(InvalidQueryError, match=err): + utils.parse_size_input((0, 5)) + def test_get_cutout_limits(): test_img_wcs_kwds = fits.Header(cards=[('NAXIS', 2, 'number of array dimensions'), diff --git a/astrocut/utils/utils.py b/astrocut/utils/utils.py index e1bb6355..561de763 100644 --- a/astrocut/utils/utils.py +++ b/astrocut/utils/utils.py @@ -43,12 +43,19 @@ def parse_size_input(cutout_size): cutout_size = np.atleast_1d(cutout_size) if len(cutout_size) == 1: cutout_size = np.repeat(cutout_size, 2) + elif not isinstance(cutout_size, np.ndarray): + cutout_size = np.array(cutout_size) if len(cutout_size) > 2: warnings.warn("Too many dimensions in cutout size, only the first two will be used.", InputWarning) cutout_size = cutout_size[:2] + ny, nx = cutout_size + if ny == 0 or nx == 0: + raise InvalidQueryError('Cutout size dimensions must be greater than zero. ' + f'Provided size: ({cutout_size[0]}, {cutout_size[1]})') + return cutout_size diff --git a/docs/astrocut/index.rst b/docs/astrocut/index.rst index 5113df94..500ea7fa 100644 --- a/docs/astrocut/index.rst +++ b/docs/astrocut/index.rst @@ -164,18 +164,14 @@ treated as the R, G, and B channels, respectively. TESS Full-Frame Image Cutouts ============================= -There are two parts of the package involved in creating cutouts from TESS full-frame images (FFIs). +Astrocut can be used to create cutouts from TESS full-frame images (FFIs). First, the `~astrocut.CubeFactory` (if working with SPOC products, or `~astrocut.TicaCubeFactory` if working with TICA FFIs) class allows you to create a large image cube from a list of FFI files. This is what allows the cutout operation to be performed efficiently. Next, the `~astrocut.CutoutFactory` class performs the actual cutout and builds -a target pixel file (TPF) that is similar to the TESS Mission-produced TPFs. +a target pixel file (TPF) that is similar to the TESS Mission-produced TPFs. Finally, the `~astrocut.cube_cut_from_footprint` +function generates cutouts from image cube files stored in MAST's AWS Open Data Bucket. -The basic procedure is to first create an image cube from individual FFI files -(this only needs to be completed once per set of FFIs), and to then make individual cutout TPFs from this -large cube file for targets of interest. Note, you can only make cubes from a set of FFIs -with the same product type (i.e., only SPOC *or* only TICA products) that were observed in -the same Sector, camera, and CCD. If you are creating a small number of cutouts, the TESSCut web service may suit your needs: `mast.stsci.edu/tesscut `_ @@ -215,6 +211,9 @@ create a cube using the `~astrocut.CubeFactory.make_cube` method (or and `~astrocut.TicaCubeFactory.make_cube` run in verbose mode and prints out progress; setting `verbose` to false will silence all output. +Note, you can only make cubes from a set of FFIs with the same product type (i.e., only SPOC *or* +only TICA products) that were observed in the same sector, camera, and CCD. + The output image cube file format is described `here `__. .. code-block:: python @@ -253,7 +252,6 @@ The output image cube file format is described `here >> from astrocut import cube_cut_from_footprint + + >>> cube_cut_from_footprint( #doctest: +SKIP + ... coordinates='83.40630967798376 -62.48977125108528', + ... cutout_size=10, + ... sequence=[1, 2], # TESS sectors + ... product='SPOC') + ['./cutouts/tess-s0001-4-4/tess-s0001-4-4_83.406310_-62.489771_10x10_astrocut.fits', + './cutouts/tess-s0002-4-1/tess-s0002-4-1_83.406310_-62.489771_10x10_astrocut.fits'] + +Alternatively, you can provide the S3 URI for a cube file directly to the `~astrocut.cube_cut` function. Multithreading --------------- -To use multithreading for cloud-based cutouts, set the ``threads`` argument in ``cube_cut`` to the number of threads you want to use. Alternatively, you -can set ``threads`` to ``"auto"``, which will set the number of threads based on the CPU count of your machine. +Using cube files stored on the cloud allows you the option to implement multithreading to improve cutout generation +speed. See below for a multithreaded example, using a TESS cube file stored on AWS. + +To use multithreading for cloud-based cutouts, set the ``threads`` argument in ``cube_cut`` to the number of threads you want to use. +Alternatively, you can set ``threads`` to ``"auto"``, which will set the number of threads based on the CPU count of your machine. Note that ``Total Time`` results may vary from machine to machine. .. code-block:: python diff --git a/setup.cfg b/setup.cfg index 2323f0bd..f1131b7d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,11 +18,13 @@ setup_requires = setuptools_scm install_requires = asdf>=2.15.0 # for ASDF file format astropy>=5.2 # astropy with s3fs support + cachetools>=5.3.2 # for caching data fsspec[http]>=2022.8.2 # for remote cutouts s3fs>=2022.8.2 # for remote cutouts s3path>=0.5.7 # for remote file paths roman_datamodels>=0.17.0 # for roman file support requests>=2.32.3 # for making HTTP requests + spherical_geometry>=1.3.0 scipy Pillow