Skip to content

Commit 8b1315e

Browse files
authored
Merge pull request #14 from fact-project/exposure
Exposure
2 parents 8aa48ad + 48d3202 commit 8b1315e

File tree

5 files changed

+266
-2
lines changed

5 files changed

+266
-2
lines changed

irf/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .collection_area import collection_area
2+
from .exposure_map import estimate_exposure_time, build_exposure_map
23

3-
4-
__all__ = ['collection_area']
4+
__all__ = ['collection_area', 'estimate_exposure_time', 'build_exposure_map']

irf/exposure_map.py

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import astropy.units as u
2+
from astropy.coordinates import Angle, SkyCoord
3+
from astropy import wcs
4+
from regions import CircleSkyRegion
5+
import numpy as np
6+
from scipy.stats import expon
7+
8+
9+
def estimate_exposure_time(timestamps):
10+
'''
11+
Takes numpy datetime64[ns] timestamps and returns an estimates of the exposure time in seconds.
12+
'''
13+
delta_s = np.diff(timestamps).astype(int) * float(1e-9)
14+
15+
# take only events that came within 30 seconds or so
16+
delta_s = delta_s[delta_s < 30]
17+
scale = delta_s.mean()
18+
19+
exposure_time = len(delta_s) * scale
20+
loc = min(delta_s)
21+
22+
# this percentile is somewhat abritrary but seems to work well.
23+
live_time_fraction = 1 - expon.ppf(0.1, loc=loc, scale=scale)
24+
25+
return (exposure_time * live_time_fraction) * u.s
26+
27+
28+
@u.quantity_input(ra=u.hourangle, dec=u.deg, fov=u.deg)
29+
def build_exposure_regions(pointing_coords, fov=4.5 * u.deg):
30+
'''
31+
Takes a list of pointing positions and a field of view and returns
32+
the unique pointing positions and the astropy.regions.
33+
34+
For an observation with N wobble positions this will return N unique
35+
pointing positions and N circular regions.
36+
'''
37+
unique_pointing_positions = SkyCoord(
38+
ra=np.unique(pointing_coords.ra),
39+
dec=np.unique(pointing_coords.dec)
40+
)
41+
42+
regions = [CircleSkyRegion(
43+
center=pointing,
44+
radius=Angle(fov) / 2
45+
) for pointing in unique_pointing_positions]
46+
47+
return unique_pointing_positions, regions
48+
49+
50+
def _build_standard_wcs(image_center, shape, naxis=2, fov=9 * u.deg):
51+
width, height = shape
52+
53+
w = wcs.WCS(naxis=2)
54+
w.wcs.crpix = [width / 2 + 0.5, height / 2 + 0.5]
55+
w.wcs.cdelt = np.array([-fov.value / width, fov.value / height])
56+
w.wcs.crval = [image_center.ra.deg, image_center.dec.deg]
57+
w.wcs.ctype = ["RA---TAN", "DEC--TAN"]
58+
w.wcs.radesys = 'FK5'
59+
w.wcs.equinox = 2000.0
60+
w.wcs.cunit = ['deg', 'deg']
61+
w._naxis = [width, height]
62+
return w
63+
64+
65+
@u.quantity_input(event_ra=u.hourangle, event_dec=u.deg, fov=u.deg)
66+
def build_exposure_map(pointing_coords, event_time, fov=4.5 * u.deg, wcs=None, shape=(1000, 1000)):
67+
'''
68+
Takes pointing coordinates for each event and the corresponding timestamp.
69+
Returns a masked array containing the estimated exposure time in hours
70+
and a WCS object so the mask can be plotted.
71+
'''
72+
if not wcs:
73+
image_center = SkyCoord(ra=pointing_coords.ra.mean(), dec=pointing_coords.dec.mean())
74+
wcs = _build_standard_wcs(image_center, shape, fov=2 * fov)
75+
76+
unique_pointing_positions, regions = build_exposure_regions(pointing_coords)
77+
78+
times = []
79+
for p in unique_pointing_positions:
80+
m = (pointing_coords.ra == p.ra) & (pointing_coords.dec == p.dec)
81+
exposure_time = estimate_exposure_time(event_time[m])
82+
times.append(exposure_time)
83+
84+
masks = [r.to_pixel(wcs).to_mask().to_image(shape) for r in regions]
85+
cutout = sum(masks).astype(bool)
86+
mask = sum([w.to('h').value * m for w, m in zip(times, masks)])
87+
88+
return np.ma.masked_array(mask, mask=~cutout), wcs
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from irf import estimate_exposure_time, build_exposure_map
2+
import click
3+
import fact.io as fio
4+
import pandas as pd
5+
from astroquery.skyview import SkyView
6+
from astropy.wcs import WCS
7+
from astropy.visualization import ImageNormalize, ZScaleInterval, AsinhStretch
8+
import astropy.units as u
9+
from astropy.coordinates import SkyCoord
10+
11+
import matplotlib.pyplot as plt
12+
13+
14+
@u.quantity_input(fov=u.deg)
15+
def get_sdss_sky_image(img_center, fov=9 * u.deg, n_pix=1000):
16+
'''
17+
A small helper method which uses astroquery to get an image from the sdss.
18+
This requires internet access and fails pretty often due to http timeouts.
19+
'''
20+
hdu = SkyView.get_images(
21+
position=img_center,
22+
pixels=n_pix,
23+
survey=['DSS'],
24+
width=fov,
25+
height=fov)[0][0]
26+
27+
img = hdu.data
28+
wcs = WCS(hdu.header)
29+
return img, wcs
30+
31+
32+
@click.command()
33+
@click.argument(
34+
'dl3_path',
35+
type=click.Path(file_okay=True, dir_okay=False),
36+
)
37+
@click.argument(
38+
'output_path',
39+
type=click.Path(file_okay=True, dir_okay=False, exists=False),
40+
)
41+
@click.option(
42+
'-n',
43+
'--n_pix',
44+
default=1000,
45+
help='number of pixels along the edge of the produced image',
46+
)
47+
@click.option(
48+
'-s',
49+
'--source_name',
50+
default=None,
51+
help=
52+
'If supplied, e.g. "Crab Nebula" will draw the position of that source into the image',
53+
)
54+
@click.option(
55+
'--background/--no-background',
56+
default=True,
57+
help='If true, downloads SDSS image for backgrund image in the plot')
58+
def main(dl3_path, output_path, n_pix, source_name, background):
59+
'''
60+
Takes FACT dl3 output and plots a skymap which is being saved to the output_path.
61+
'''
62+
runs = fio.read_data(dl3_path, key='runs')
63+
dl3 = fio.read_data(dl3_path, key='events')
64+
65+
data = pd.merge(runs, dl3, on=['run_id', 'night'])
66+
67+
timestamps = pd.to_datetime(data.timestamp).values
68+
total_ontime = estimate_exposure_time(timestamps)
69+
print('Total estimated exposure time: {}'.format(total_ontime.to(u.h)))
70+
71+
ra_pointing = data.right_ascension.values * u.hourangle
72+
dec_pointing = data.declination.values * u.deg
73+
pointing = SkyCoord(ra=ra_pointing, dec=dec_pointing)
74+
75+
img = None
76+
wcs = None
77+
if background:
78+
img_center = SkyCoord(ra=pointing.ra.mean(), dec=pointing.dec.mean())
79+
img, wcs = get_sdss_sky_image(
80+
img_center=img_center, n_pix=n_pix, fov=9 * u.deg)
81+
82+
mask, wcs = build_exposure_map(pointing, timestamps, shape=(n_pix, n_pix))
83+
84+
ax = plot_exposure(mask, wcs, image=img)
85+
if source_name:
86+
source = SkyCoord.from_name('Crab Nebula')
87+
ax.scatter(
88+
source.ra.deg,
89+
source.dec.deg,
90+
transform=ax.get_transform('icrs'),
91+
label=source_name,
92+
s=10**2,
93+
facecolors='none',
94+
edgecolors='r',
95+
)
96+
ax.legend()
97+
98+
plt.savefig(output_path, dpi=200)
99+
100+
101+
def plot_exposure(mask, wcs, image=None):
102+
plt.figure(figsize=(10, 10))
103+
ax = plt.subplot(projection=wcs)
104+
105+
if image is not None:
106+
norm = ImageNormalize(
107+
image,
108+
interval=ZScaleInterval(contrast=0.05),
109+
stretch=AsinhStretch(a=0.2))
110+
ax.imshow(image, cmap='gray', norm=norm, interpolation='nearest')
111+
112+
d = ax.imshow(mask, alpha=0.7)
113+
cb = plt.colorbar(d)
114+
cb.set_label('Live Time / Hours')
115+
ax.set_xlabel('Galactic Longitude')
116+
ax.set_ylabel('Galactic Latitude')
117+
118+
return ax
119+
120+
121+
if __name__ == '__main__':
122+
main()

irf/tests/test_exposure_map.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import os
2+
import pytest
3+
import fact.io as fio
4+
import pandas as pd
5+
6+
import astropy.units as u
7+
from astropy.coordinates import SkyCoord
8+
9+
from irf import estimate_exposure_time, build_exposure_map
10+
11+
12+
FIXTURE_DIR = os.path.join(
13+
os.path.dirname(os.path.realpath(__file__)),
14+
'test_files',
15+
)
16+
17+
18+
@pytest.fixture
19+
def events():
20+
return fio.read_data(os.path.join(FIXTURE_DIR, 'crab_dl3_sample.hdf5'), key='events')
21+
22+
23+
@pytest.fixture
24+
def runs():
25+
return fio.read_data(os.path.join(FIXTURE_DIR, 'crab_dl3_sample.hdf5'), key='runs')
26+
27+
28+
29+
def test_exposure_map(runs, events):
30+
data = pd.merge(runs, events, on=['run_id', 'night'])
31+
32+
timestamps = pd.to_datetime(data.timestamp).values
33+
34+
ra_pointing = data.right_ascension.values * u.hourangle
35+
dec_pointing = data.declination.values * u.deg
36+
pointing = SkyCoord(ra=ra_pointing, dec=dec_pointing)
37+
38+
mask, wcs = build_exposure_map(pointing, timestamps, fov=4.5 * u.deg)
39+
40+
assert mask.mask.any() # assert whether some region is masked
41+
42+
43+
44+
def test_exposure_time(runs, events):
45+
n_crab_sample = 687226
46+
n_test_sample = len(events)
47+
48+
live_time_crab = runs.ontime.sum() / 3600 # to hours
49+
50+
expected_live_time_sample = n_test_sample / n_crab_sample * live_time_crab
51+
52+
timestamps = pd.to_datetime(events.timestamp).values
53+
exposure = estimate_exposure_time(timestamps).to('h').value
54+
assert expected_live_time_sample == pytest.approx(exposure, 0.05)
4.97 MB
Binary file not shown.

0 commit comments

Comments
 (0)