Skip to content

Commit a416865

Browse files
authored
Merge ab086c9 into f4511d1
2 parents f4511d1 + ab086c9 commit a416865

6 files changed

Lines changed: 823 additions & 0 deletions

File tree

cosipy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818

1919
from .background_estimation import LineBackgroundEstimation
2020
from .background_estimation import ContinuumEstimation
21+

cosipy/event_selection/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .good_time_interval import GoodTimeInterval
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import logging
2+
logger = logging.getLogger(__name__)
3+
4+
import numpy as np
5+
from astropy.time import Time
6+
from astropy.io import fits
7+
8+
class GoodTimeInterval():
9+
10+
def __init__(self, tstart_list, tstop_list):
11+
"""
12+
Initialize GTI object.
13+
14+
Parameters
15+
----------
16+
tstart_list : astropy.time.Time (array)
17+
Start times of GTI intervals
18+
tstop_list : astropy.time.Time (array)
19+
Stop times of GTI intervals
20+
"""
21+
# Check that starts and stops are scalar
22+
if tstart_list.isscalar == True:
23+
tstart_list = Time([tstart_list])
24+
if tstop_list.isscalar == True:
25+
tstop_list = Time([tstop_list])
26+
27+
# Check that starts and stops have the same length
28+
if len(tstart_list) != len(tstop_list):
29+
raise ValueError(f"Length mismatch between starts ({len(tstart_list)}) and stops ({len(tstop_list)})")
30+
31+
self._tstart_list = tstart_list
32+
self._tstop_list = tstop_list
33+
34+
# Sort by start time
35+
self._sort()
36+
37+
@property
38+
def tstart_list(self):
39+
return self._tstart_list
40+
41+
@property
42+
def tstop_list(self):
43+
return self._tstop_list
44+
45+
def __len__(self):
46+
"""Return the number of GTI intervals."""
47+
return len(self._tstart_list)
48+
49+
def __getitem__(self, index):
50+
"""
51+
Get GTI interval(s) by index.
52+
53+
Parameters
54+
----------
55+
index : int, slice, or array-like
56+
Index, slice, or boolean/integer array to retrieve
57+
58+
Returns
59+
-------
60+
tuple of (Time, Time)
61+
(tstart_list, tstop_list) for the indexed interval(s)
62+
"""
63+
return self._tstart_list[index], self._tstop_list[index]
64+
65+
def __iter__(self):
66+
"""
67+
Iterate over GTI intervals.
68+
69+
Yields
70+
------
71+
tuple of (Time, Time)
72+
Each (start, stop) pair
73+
"""
74+
for start, stop in zip(self._tstart_list, self._tstop_list):
75+
yield start, stop
76+
77+
def _sort(self):
78+
"""
79+
Sort GTI by start time in ascending order.
80+
81+
Modifies the GTI in place.
82+
Stops are sorted according to the start time order.
83+
"""
84+
sort_idx = np.argsort(self._tstart_list)
85+
self._tstart_list = self._tstart_list[sort_idx]
86+
self._tstop_list = self._tstop_list[sort_idx]
87+
88+
def save_as_fits(self, filename, overwrite=False, output_format='unix'):
89+
"""
90+
Save GTI data to a FITS file.
91+
92+
Parameters
93+
----------
94+
filename : str
95+
Output FITS filename
96+
overwrite : bool, optional
97+
If True, overwrite existing file (default: False)
98+
output_format : str, optional
99+
Time format for output (e.g., 'unix', 'mjd'). Default: 'unix'
100+
"""
101+
# Get values in the specified output format using getattr
102+
if not hasattr(self._tstart_list, output_format):
103+
raise ValueError(f"Unsupported output format: {output_format}")
104+
105+
start_times = getattr(self._tstart_list, output_format)
106+
stop_times = getattr(self._tstop_list, output_format)
107+
108+
# Use the scale from the stored Time objects
109+
output_scale = self._tstart_list.scale
110+
111+
output_unit = 's'
112+
if output_format in ['jd', 'mjd']:
113+
output_unit = 'd'
114+
115+
# Create primary HDU
116+
primary_hdu = fits.PrimaryHDU()
117+
118+
# Define table columns
119+
col1 = fits.Column(name='TSTART', format='D', unit=output_unit, array=start_times)
120+
col2 = fits.Column(name='TSTOP', format='D', unit=output_unit, array=stop_times)
121+
122+
# Create table HDU
123+
table_hdu = fits.BinTableHDU.from_columns([col1, col2])
124+
table_hdu.header['EXTNAME'] = 'GTI'
125+
table_hdu.header['TIMESYS'] = output_scale.upper()
126+
table_hdu.header['TIMEUNIT'] = output_unit
127+
table_hdu.header['TIMEFORMAT'] = output_format
128+
129+
# Create HDUList and write to FITS file
130+
hdul = fits.HDUList([primary_hdu, table_hdu])
131+
hdul.writeto(filename, overwrite=overwrite)
132+
133+
@classmethod
134+
def from_fits(cls, filename):
135+
"""
136+
Load GTI from a FITS file.
137+
138+
Reads time format and scale from FITS header.
139+
140+
Parameters
141+
----------
142+
filename : str
143+
Input FITS filename
144+
145+
Returns
146+
-------
147+
GoodTimeIntervals
148+
GTI object
149+
"""
150+
infile = fits.open(filename)
151+
152+
# Search for GTI extension
153+
gti_hdu = None
154+
for hdu in infile:
155+
if isinstance(hdu, fits.BinTableHDU) and hdu.name in ['GTI']:
156+
gti_hdu = hdu
157+
break
158+
159+
if gti_hdu is None:
160+
infile.close()
161+
logger.error("GTI table not found in FITS file")
162+
163+
# Read time system/format from header
164+
time_scale = gti_hdu.header.get('TIMESYS').lower()
165+
time_format = gti_hdu.header.get('TIMEFORMAT').lower()
166+
167+
# Read start and stop times as arrays
168+
tstart_list = Time(gti_hdu.data['TSTART'], format=time_format, scale=time_scale)
169+
tstop_list = Time(gti_hdu.data['TSTOP'], format=time_format, scale=time_scale)
170+
171+
infile.close()
172+
return cls(tstart_list, tstop_list)
173+
174+
@classmethod
175+
def intersection(cls, *gti_list):
176+
"""
177+
Find the intersection of multiple GTI objects.
178+
179+
Returns a new GTI object containing only time intervals that overlap
180+
in all input GTI objects.
181+
182+
Assumes all GTI objects are sorted by start time (guaranteed by _sort() in __init__).
183+
184+
Parameters
185+
----------
186+
*gti_list : GoodTimeInterval
187+
Variable number of GTI objects to intersect
188+
189+
Returns
190+
-------
191+
GoodTimeInterval
192+
New GTI object with intersected intervals
193+
194+
Examples
195+
--------
196+
>>> gti1 = GoodTimeInterval(tstart1, tstop1)
197+
>>> gti2 = GoodTimeInterval(tstart2, tstop2)
198+
>>> gti3 = GoodTimeInterval(tstart3, tstop3)
199+
>>> intersected = GoodTimeInterval.intersection(gti1, gti2, gti3)
200+
"""
201+
if len(gti_list) == 0:
202+
raise ValueError("At least one GTI object is required")
203+
204+
if len(gti_list) == 1:
205+
# Return a copy of the single GTI
206+
gti = gti_list[0]
207+
return cls(gti.tstart_list.copy(), gti.tstop_list.copy())
208+
209+
# Start with intervals from the first GTI
210+
current_starts = list(gti_list[0].tstart_list)
211+
current_stops = list(gti_list[0].tstop_list)
212+
213+
# Iteratively intersect with each subsequent GTI
214+
for gti in gti_list[1:]:
215+
new_starts = []
216+
new_stops = []
217+
218+
i = 0 # Index for current intervals
219+
j = 0 # Index for gti intervals
220+
221+
# Two-pointer approach for sorted intervals
222+
while i < len(current_starts) and j < len(gti):
223+
start1, stop1 = current_starts[i], current_stops[i]
224+
start2, stop2 = gti[j]
225+
226+
# Check if intervals overlap
227+
if start1 < stop2 and start2 < stop1:
228+
# Calculate intersection
229+
max_start = max(start1, start2)
230+
min_stop = min(stop1, stop2)
231+
232+
if max_start < min_stop:
233+
new_starts.append(max_start)
234+
new_stops.append(min_stop)
235+
236+
# Move the pointer for the interval that ends first
237+
if stop1 <= stop2:
238+
i += 1
239+
else:
240+
j += 1
241+
242+
# Update current intervals for next iteration
243+
current_starts = new_starts
244+
current_stops = new_stops
245+
246+
# If no overlaps found, we can stop early
247+
if len(current_starts) == 0:
248+
break
249+
250+
# Handle case with no overlapping intervals
251+
if len(current_starts) == 0:
252+
# Return empty GTI with appropriate time format
253+
empty_time = Time([], format=gti_list[0].tstart_list.format,
254+
scale=gti_list[0].tstart_list.scale)
255+
return cls(empty_time, empty_time.copy())
256+
257+
return cls(Time(current_starts), Time(current_stops))

0 commit comments

Comments
 (0)