Skip to content

Commit 7512241

Browse files
Merge pull request #940 from StingraySoftware/allow_remote
Allow loading of remote files, avoid extra loading of FITS files
2 parents 32b3943 + 7aac80d commit 7512241

File tree

7 files changed

+103
-17
lines changed

7 files changed

+103
-17
lines changed

docs/changes/940.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use the power of fits.open to load remote datasets

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ all = [
7777
"etils",
7878
"tfp-nightly",
7979
"typing_extensions",
80+
"fsspec",
8081
]
8182
docs = [
8283
"tomli>=1.1.0; python_version < '3.11'",

stingray/gti.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
from collections.abc import Iterable
55
import copy
66

7-
from astropy.io import fits
87
from .utils import contiguous_regions, jit, HAS_NUMBA
98
from .utils import assign_value_if_none, apply_function_if_none
109
from .utils import check_iterables_close, is_sorted
10+
from .utils import fits_open_including_remote
11+
1112
from stingray.exceptions import StingrayError
1213
from stingray.loggingconfig import setup_logger
1314

@@ -122,7 +123,7 @@ def load_gtis(fits_file, gtistring=None):
122123

123124
gtistring = assign_value_if_none(gtistring, "GTI")
124125
logger.info("Loading GTIS from file %s" % fits_file)
125-
lchdulist = fits.open(fits_file, checksum=True, ignore_missing_end=True)
126+
lchdulist = fits_open_including_remote(fits_file, checksum=True, ignore_missing_end=True)
126127
lchdulist.verify("warn")
127128

128129
gtitable = lchdulist[gtistring].data
@@ -274,7 +275,7 @@ def get_gti_from_all_extensions(lchdulist, accepted_gtistrings=["GTI"], det_numb
274275
>>> assert np.allclose(gti, [[200, 250]])
275276
"""
276277
if isinstance(lchdulist, str):
277-
lchdulist = fits.open(lchdulist)
278+
lchdulist = fits_open_including_remote(lchdulist)
278279
acc_gti_strs = copy.deepcopy(accepted_gtistrings)
279280
if det_numbers is not None:
280281
for i in det_numbers:

stingray/io.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from collections.abc import Iterable
88

99
import numpy as np
10-
from astropy.io import fits
1110
from astropy.table import Table
1211
from astropy.logger import AstropyUserWarning
1312
import matplotlib.pyplot as plt
@@ -16,6 +15,7 @@
1615

1716
import stingray.utils as utils
1817
from stingray.loggingconfig import setup_logger
18+
from stingray.utils import fits_open_including_remote
1919

2020

2121
from .utils import (
@@ -765,7 +765,7 @@ def __init__(
765765
additional_columns = [self.detector_key]
766766
elif self.detector_key != "NONE":
767767
additional_columns.append(self.detector_key)
768-
self.data_hdu = fits.open(self.fname)[self.hduname]
768+
self.data_hdu = self.data_hdus[self.hduname]
769769
self.gti_file = gti_file
770770
self._read_gtis(self.gti_file)
771771

@@ -914,8 +914,8 @@ def _initialize_header_events(self, fname, force_hduname=None):
914914
If not None, the name of the HDU to read. If None, an extension called
915915
EVENTS or the first extension.
916916
"""
917-
hdulist = fits.open(fname)
918-
917+
hdulist = fits_open_including_remote(fname)
918+
self.data_hdus = hdulist
919919
if not force_hduname:
920920
for hdu in hdulist:
921921
if "TELESCOP" in hdu.header or "MISSION" in hdu.header:
@@ -1052,11 +1052,11 @@ def _read_gtis(self, gti_file=None, det_numbers=None):
10521052
# So, here I'm reading a bunch of rows hoping that they represent the
10531053
# detector number population
10541054
if self.detector_key is not None:
1055-
with fits.open(self.fname) as hdul:
1056-
data = hdul[self.hduname].data
1057-
if self.detector_key in data.dtype.names:
1058-
probe_vals = data[:100][self.detector_key]
1059-
det_numbers = list(set(probe_vals))
1055+
hdul = self.data_hdus
1056+
data = hdul[self.hduname].data
1057+
if self.detector_key in data.dtype.names:
1058+
probe_vals = data[:100][self.detector_key]
1059+
det_numbers = list(set(probe_vals))
10601060
del hdul
10611061

10621062
accepted_gtistrings = self.gtistring.split(",")
@@ -1127,7 +1127,7 @@ def _get_idx_from_time_range(self, start, stop):
11271127
time_edges[raw_max_idx] - stop >= 0
11281128
), f"Stop: {stop}; {time_edges[raw_max_idx] - stop} < 0"
11291129

1130-
with fits.open(self.fname) as hdulist:
1130+
with fits_open_including_remote(self.fname) as hdulist:
11311131
filtered_times = hdulist[self.hduname].data[self.time_column][
11321132
raw_lower_edge : raw_upper_edge + 1
11331133
]
@@ -1219,7 +1219,7 @@ def _trace_nphots_in_file(self, nedges=1001):
12191219

12201220
fname = self.fname
12211221

1222-
with fits.open(fname) as hdul:
1222+
with fits_open_including_remote(fname) as hdul:
12231223
size = hdul[1].header["NAXIS2"]
12241224
nedges = min(nedges, size // 10 + 2)
12251225

@@ -1335,7 +1335,7 @@ def read_header_key(fits_file, key, hdu=1):
13351335
The value stored under ``key`` in ``fits_file``
13361336
"""
13371337

1338-
hdulist = fits.open(fits_file, ignore_missing_end=True)
1338+
hdulist = fits_open_including_remote(fits_file, ignore_missing_end=True)
13391339
try:
13401340
value = hdulist[hdu].header[key]
13411341
except KeyError: # pragma: no cover
@@ -1367,7 +1367,7 @@ def ref_mjd(fits_file, hdu=1):
13671367
fits_file = fits_file[0]
13681368
logger.info("opening %s" % fits_file)
13691369

1370-
hdulist = fits.open(fits_file, ignore_missing_end=True)
1370+
hdulist = fits_open_including_remote(fits_file, ignore_missing_end=True)
13711371

13721372
ref_mjd_val = high_precision_keyword_read(hdulist[hdu].header, "MJDREF")
13731373

stingray/mission_support/rxte.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from astropy.table import Table
66

77
from astropy.io import fits
8+
from stingray.utils import fits_open_including_remote
89

910
c_match = re.compile(r"C\[(.*)\]")
1011

@@ -343,7 +344,7 @@ def rxte_pca_event_file_interpretation(input_data, header=None, hduname=None):
343344

344345
if isinstance(input_data, str):
345346
return rxte_pca_event_file_interpretation(
346-
fits.open(input_data), header=header, hduname=hduname
347+
fits_open_including_remote(input_data), header=header, hduname=hduname
347348
)
348349

349350
if isinstance(input_data, fits.HDUList):

stingray/tests/test_events.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
_HAS_PANDAS = importlib.util.find_spec("pandas") is not None
1616
_HAS_H5PY = importlib.util.find_spec("h5py") is not None
1717
_HAS_YAML = importlib.util.find_spec("yaml") is not None
18+
_HAS_FSSPEC = importlib.util.find_spec("fsspec") is not None
1819

1920

2021
class TestEvents(object):
@@ -248,6 +249,20 @@ def test_fits_standard(self):
248249
ev = ev.read(fname, fmt="hea")
249250
assert np.isclose(ev.mjdref, 55197.00076601852)
250251

252+
@pytest.mark.skipif("not _HAS_FSSPEC")
253+
@pytest.mark.remote_data
254+
def test_fits_standard_remote(self):
255+
"""Test that fits works with a standard event list
256+
file.
257+
"""
258+
fname = (
259+
"s3://nasa-heasarc/swift/data/obs/2015_12/00037258040/xrt/event/"
260+
"sw00037258040xwtw2st_cl.evt.gz"
261+
)
262+
263+
ev = EventList.read(fname, fmt="hea")
264+
assert np.isclose(ev.mjdref, 51910.00074287037)
265+
251266
def test_fits_with_standard_file_and_calibrate_directly(self):
252267
"""Test that fits works and calibration works."""
253268
fname = os.path.join(datadir, "monol_testA.evt")

stingray/utils.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from numpy import histogram as histogram_np
1515
from numpy import histogram2d as histogram2d_np
1616
from numpy import histogramdd as histogramdd_np
17+
from astropy.io import fits
1718
from .loggingconfig import setup_logger
1819

1920
logger = setup_logger()
@@ -2410,3 +2411,69 @@ def _int_sum_non_zero(array):
24102411
if a > 0:
24112412
sum += int(a)
24122413
return sum
2414+
2415+
2416+
def fits_open_remote(filename, **kwargs):
2417+
"""Open a remote FITS file.
2418+
2419+
This function attempts to open a FITS file using `astropy.io.fits.open`. If a
2420+
`PermissionError` is raised and the filename appears to be a remote URL,
2421+
it retries opening the file with fsspec.
2422+
2423+
Requires the `botocore` package to be installed.
2424+
2425+
Parameters
2426+
----------
2427+
filename : str
2428+
The path or URL to the FITS file to open. Can be a local file path or a remote URL.
2429+
**kwargs
2430+
Additional keyword arguments passed to `astropy.io.fits.open`.
2431+
2432+
Returns
2433+
-------
2434+
hdulist : astropy.io.fits.HDUList
2435+
The opened FITS file as an HDUList object.
2436+
2437+
Raises
2438+
------
2439+
PermissionError
2440+
If the file cannot be opened and anonymous access is not possible or fails.
2441+
2442+
"""
2443+
import botocore
2444+
2445+
try:
2446+
# This will work for local files and remote files with proper permissions
2447+
return fits.open(filename, **kwargs)
2448+
except (PermissionError, botocore.exceptions.NoCredentialsError) as e:
2449+
if "://" in filename:
2450+
logger.info(f"Permission denied for {filename}, trying with fsspec.")
2451+
return fits.open(filename, use_fsspec=True, fsspec_kwargs={"anon": True}, **kwargs)
2452+
raise e
2453+
2454+
2455+
def fits_open_including_remote(filename, **kwargs):
2456+
"""Open a FITS file, including remote files with anonymous access if needed.
2457+
2458+
If the filename appears to be a remote URL, it calls `fits_open_remote` to handle
2459+
potential permission issues. Otherwise, it opens the file directly with
2460+
`astropy.io.fits.open`.
2461+
2462+
Parameters
2463+
----------
2464+
filename : str
2465+
The path or URL to the FITS file to open. Can be a local file path or a remote URL.
2466+
**kwargs
2467+
Additional keyword arguments passed to `astropy.io.fits.open`.
2468+
2469+
Returns
2470+
-------
2471+
hdulist : astropy.io.fits.HDUList
2472+
The opened FITS file as an HDUList object.
2473+
2474+
"""
2475+
2476+
if "://" in filename:
2477+
return fits_open_remote(filename, **kwargs)
2478+
2479+
return fits.open(filename, **kwargs)

0 commit comments

Comments
 (0)