isgri 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isgri/__init__.py +0 -0
- isgri/utils/__init__.py +5 -0
- isgri/utils/file_loaders.py +159 -0
- isgri/utils/lightcurve.py +265 -0
- isgri/utils/pif.py +41 -0
- isgri/utils/quality.py +166 -0
- isgri/utils/time_conversion.py +39 -0
- isgri-0.1.0.dist-info/METADATA +64 -0
- isgri-0.1.0.dist-info/RECORD +11 -0
- isgri-0.1.0.dist-info/WHEEL +4 -0
- isgri-0.1.0.dist-info/licenses/LICENSE +21 -0
isgri/__init__.py
ADDED
|
File without changes
|
isgri/utils/__init__.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from astropy.io import fits
|
|
2
|
+
from .pif import apply_pif_mask, coding_fraction, estimate_active_modules
|
|
3
|
+
import numpy as np
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def verify_events_path(path):
|
|
8
|
+
"""
|
|
9
|
+
Verifies and resolves the events file path.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
path (str): File path or directory path containing events file.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
str: Resolved path to valid events file.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
FileNotFoundError: If path doesn't exist, no events file found, or multiple events files found.
|
|
19
|
+
ValueError: If ISGR-EVTS-ALL extension not found in file.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
if os.path.isfile(path):
|
|
23
|
+
resolved_path = path
|
|
24
|
+
elif os.path.isdir(path):
|
|
25
|
+
candidate_files = [f for f in os.listdir(path) if "isgri_events" in f]
|
|
26
|
+
if len(candidate_files) == 0:
|
|
27
|
+
raise FileNotFoundError("No isgri_events file found in the provided directory.")
|
|
28
|
+
elif len(candidate_files) > 1:
|
|
29
|
+
raise FileNotFoundError(
|
|
30
|
+
f"Multiple isgri_events files found in the provided directory: {path}.",
|
|
31
|
+
"\nPlease specify the exact file paths.",
|
|
32
|
+
)
|
|
33
|
+
else:
|
|
34
|
+
resolved_path = os.path.join(path, candidate_files[0])
|
|
35
|
+
else:
|
|
36
|
+
raise FileNotFoundError(f"Path does not exist: {path}")
|
|
37
|
+
|
|
38
|
+
with fits.open(resolved_path) as hdu:
|
|
39
|
+
if "ISGR-EVTS-ALL" not in hdu:
|
|
40
|
+
raise ValueError(f"Invalid events file: ISGR-EVTS-ALL extension not found in {resolved_path}")
|
|
41
|
+
return resolved_path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_isgri_events(events_path):
|
|
45
|
+
"""
|
|
46
|
+
Loads ISGRI events from FITS file.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
events_path (str): Path to events file or directory.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
tuple: (events, gtis, metadata) where:
|
|
53
|
+
- events: Structured numpy array with TIME, ISGRI_ENERGY, DETY, DETZ fields
|
|
54
|
+
- gtis: (N, 2) array of Good Time Interval [start, stop] pairs (IJD)
|
|
55
|
+
- metadata: Dictionary with header info (REVOL, SWID, TSTART, etc.)
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
confirmed_path = verify_events_path(events_path)
|
|
59
|
+
with fits.open(confirmed_path) as hdu:
|
|
60
|
+
events = np.array(hdu["ISGR-EVTS-ALL"].data)
|
|
61
|
+
header = hdu["ISGR-EVTS-ALL"].header
|
|
62
|
+
metadata = {
|
|
63
|
+
"REVOL": header.get("REVOL"),
|
|
64
|
+
"SWID": header.get("SWID"),
|
|
65
|
+
"TSTART": header.get("TSTART"),
|
|
66
|
+
"TSTOP": header.get("TSTOP"),
|
|
67
|
+
"TELAPSE": header.get("TELAPSE"),
|
|
68
|
+
"OBT_TSTART": header.get("OBTSTART"),
|
|
69
|
+
"OBT_TSTOP": header.get("OBTEND"),
|
|
70
|
+
"RA_SCX": header.get("RA_SCX"),
|
|
71
|
+
"DEC_SCX": header.get("DEC_SCX"),
|
|
72
|
+
"RA_SCZ": header.get("RA_SCZ"),
|
|
73
|
+
"DEC_SCZ": header.get("DEC_SCZ"),
|
|
74
|
+
}
|
|
75
|
+
try:
|
|
76
|
+
gtis = np.array(hdu["IBIS-GNRL-GTI"].data)
|
|
77
|
+
gtis = np.array([gtis["START"], gtis["STOP"]]).T
|
|
78
|
+
except:
|
|
79
|
+
gtis = np.array([events["TIME"][0], events["TIME"][-1]]).reshape(1, 2)
|
|
80
|
+
events = events[events["SELECT_FLAG"] == 0] # Filter out bad events
|
|
81
|
+
return events, gtis, metadata
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def default_pif_metadata():
|
|
85
|
+
"""
|
|
86
|
+
Creates default PIF metadata dictionary for cases without PIF file.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
dict: Default PIF metadata with all 8 modules active, no source info.
|
|
90
|
+
"""
|
|
91
|
+
return {
|
|
92
|
+
"SWID": None,
|
|
93
|
+
"SRC_RA": None,
|
|
94
|
+
"SRC_DEC": None,
|
|
95
|
+
"Source_Name": None,
|
|
96
|
+
"cod": None,
|
|
97
|
+
"No_Modules": 8,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def merge_metadata(events_metadata, pif_metadata):
|
|
102
|
+
"""
|
|
103
|
+
Merges events and PIF metadata dictionaries.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
events_metadata (dict): Metadata from events file.
|
|
107
|
+
pif_metadata (dict): Metadata from PIF file.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
dict: Combined metadata (PIF metadata overwrites events metadata except SWID).
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
merged_metadata = events_metadata.copy()
|
|
114
|
+
for key in pif_metadata:
|
|
115
|
+
if key == "SWID":
|
|
116
|
+
continue
|
|
117
|
+
merged_metadata[key] = pif_metadata[key]
|
|
118
|
+
return merged_metadata
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_isgri_pif(pif_path, events, pif_threshold=0.5, pif_extension=-1):
|
|
122
|
+
"""
|
|
123
|
+
Loads ISGRI PIF (Pixel Illumination Fraction) file and applies mask to events.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
pif_path (str): Path to PIF FITS file.
|
|
127
|
+
events (ndarray): Events array from load_isgri_events().
|
|
128
|
+
pif_threshold (float, optional): PIF threshold value (0-1). Defaults to 0.5.
|
|
129
|
+
pif_extension (int, optional): PIF file extension index. Defaults to -1.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
tuple: (piffed_events, pif, metadata_pif) where:
|
|
133
|
+
- piffed_events: Filtered events array with PIF mask applied
|
|
134
|
+
- pif: PIF values for filtered events
|
|
135
|
+
- metadata_pif: Dictionary with source info, coding fraction, active modules
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: If PIF file shape is invalid. Usually indicates empty or corrupted file.
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
with fits.open(pif_path) as hdu:
|
|
141
|
+
pif_file = np.array(hdu[pif_extension].data)
|
|
142
|
+
header = hdu[pif_extension].header
|
|
143
|
+
|
|
144
|
+
if pif_file.shape != (134, 130):
|
|
145
|
+
raise ValueError(f"Invalid PIF file shape: expected (134, 130), got {pif_file.shape}")
|
|
146
|
+
|
|
147
|
+
metadata_pif = {
|
|
148
|
+
"SWID": header.get("SWID"),
|
|
149
|
+
"Source_ID": header.get("SOURCEID"),
|
|
150
|
+
"Source_Name": header.get("NAME"),
|
|
151
|
+
"SRC_RA": header.get("RA_OBJ"),
|
|
152
|
+
"SRC_DEC": header.get("DEC_OBJ"),
|
|
153
|
+
}
|
|
154
|
+
metadata_pif["cod"] = coding_fraction(pif_file, events)
|
|
155
|
+
metadata_pif["No_Modules"] = estimate_active_modules(pif_file)
|
|
156
|
+
|
|
157
|
+
piffed_events, pif = apply_pif_mask(pif_file, events, pif_threshold)
|
|
158
|
+
|
|
159
|
+
return piffed_events, pif, metadata_pif
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from astropy.io import fits
|
|
2
|
+
import numpy as np
|
|
3
|
+
import os
|
|
4
|
+
from .file_loaders import load_isgri_events, load_isgri_pif, default_pif_metadata, merge_metadata
|
|
5
|
+
from .pif import DETZ_BOUNDS, DETY_BOUNDS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LightCurve:
|
|
9
|
+
"""
|
|
10
|
+
A class for working with ISGRI events. Works fully with and without ISGRI model file (PIF file).
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
time (ndarray): The IJD time values of the events.
|
|
14
|
+
energies (ndarray): The energy values of the events in keV.
|
|
15
|
+
gtis (ndarray): The Good Time Intervals (GTIs) of the events.
|
|
16
|
+
t0 (float): The first time of the events (IJD).
|
|
17
|
+
local_time (ndarray): The local time values of the events (seconds from t0).
|
|
18
|
+
dety (ndarray): The Y detector coordinates.
|
|
19
|
+
detz (ndarray): The Z detector coordinates.
|
|
20
|
+
pif (ndarray): The PIF values of the events.
|
|
21
|
+
metadata (dict): Event metadata including SWID, source info, etc.
|
|
22
|
+
|
|
23
|
+
Methods:
|
|
24
|
+
load_data: Loads the light curve data from events and PIF files.
|
|
25
|
+
rebin: Rebins the light curve with specified bin size and energy range.
|
|
26
|
+
rebin_by_modules: Rebins the light curve for all 8 detector modules.
|
|
27
|
+
cts: Calculates the counts in specified time and energy range.
|
|
28
|
+
ijd2loc: Converts IJD time to local time (seconds from t0).
|
|
29
|
+
loc2ijd: Converts local time to IJD time.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, time, energies, gtis, dety, detz, pif, metadata):
|
|
33
|
+
"""
|
|
34
|
+
Initialize LightCurve instance.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
time (ndarray): IJD time values.
|
|
38
|
+
energies (ndarray): Energy values in keV.
|
|
39
|
+
gtis (ndarray): Good Time Intervals.
|
|
40
|
+
dety (ndarray): Y detector coordinates.
|
|
41
|
+
detz (ndarray): Z detector coordinates.
|
|
42
|
+
pif (ndarray): PIF mask values.
|
|
43
|
+
metadata (dict): Event metadata.
|
|
44
|
+
"""
|
|
45
|
+
self.time = time
|
|
46
|
+
self.energies = energies
|
|
47
|
+
self.gtis = gtis
|
|
48
|
+
self.t0 = time[0]
|
|
49
|
+
self.local_time = (time - self.t0) * 86400
|
|
50
|
+
|
|
51
|
+
self.dety = dety
|
|
52
|
+
self.detz = detz
|
|
53
|
+
self.pif = pif
|
|
54
|
+
self.metadata = metadata
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def load_data(cls, events_path=None, pif_path=None, scw=None, source=None, pif_threshold=0.5, pif_extension=-1):
|
|
58
|
+
"""
|
|
59
|
+
Loads the events from the given events file and PIF file (optional).
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
events_path (str): The path to the events file or directory.
|
|
63
|
+
pif_path (str, optional): The path to the PIF file. Defaults to None.
|
|
64
|
+
scw (str, optional): SCW identifier for auto-path resolution. Defaults to None.
|
|
65
|
+
source (str, optional): Source name for auto-path resolution. Defaults to None.
|
|
66
|
+
pif_threshold (float, optional): The PIF threshold value. Defaults to 0.5.
|
|
67
|
+
pif_extension (int, optional): PIF file extension index. Defaults to -1.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
LightCurve: An instance of the LightCurve class.
|
|
71
|
+
"""
|
|
72
|
+
events, gtis, metadata = load_isgri_events(events_path)
|
|
73
|
+
if pif_path:
|
|
74
|
+
events, pif, metadata_pif = load_isgri_pif(pif_path, events, pif_threshold, pif_extension)
|
|
75
|
+
else:
|
|
76
|
+
pif = np.ones(len(events))
|
|
77
|
+
metadata_pif = default_pif_metadata()
|
|
78
|
+
|
|
79
|
+
metadata = merge_metadata(metadata, metadata_pif)
|
|
80
|
+
time = events["TIME"]
|
|
81
|
+
energies = events["ISGRI_ENERGY"]
|
|
82
|
+
dety, detz = events["DETY"], events["DETZ"]
|
|
83
|
+
return cls(time, energies, gtis, dety, detz, pif, metadata)
|
|
84
|
+
|
|
85
|
+
def rebin(self, binsize, emin, emax, local_time=True, custom_mask=None):
|
|
86
|
+
"""
|
|
87
|
+
Rebins the events with the specified bin size and energy range.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
binsize (float or array): The bin size in seconds, or array of bin edges.
|
|
91
|
+
emin (float): The minimum energy value in keV.
|
|
92
|
+
emax (float): The maximum energy value in keV.
|
|
93
|
+
local_time (bool, optional): If True, returns local time. If False, returns IJD time. Defaults to True.
|
|
94
|
+
custom_mask (ndarray, optional): Additional boolean mask to apply. Defaults to None.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
tuple: (time, counts) arrays.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If emin >= emax.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
>>> time, counts = lc.rebin(binsize=1.0, emin=30, emax=300)
|
|
104
|
+
>>> time, counts = lc.rebin(binsize=[0, 1, 2, 5, 10], emin=50, emax=200)
|
|
105
|
+
"""
|
|
106
|
+
if emin >= emax:
|
|
107
|
+
raise ValueError("emin must be less than emax")
|
|
108
|
+
|
|
109
|
+
# Select time axis
|
|
110
|
+
time = self.local_time if local_time else self.time
|
|
111
|
+
t0 = 0 if local_time else self.t0
|
|
112
|
+
|
|
113
|
+
# Create bins
|
|
114
|
+
bins, binsize_actual = self._create_bins(binsize, time, t0, local_time)
|
|
115
|
+
|
|
116
|
+
# Apply filters
|
|
117
|
+
mask = self._create_event_mask(emin, emax, custom_mask)
|
|
118
|
+
time_filtered = time[mask]
|
|
119
|
+
pif_filtered = self.pif[mask]
|
|
120
|
+
|
|
121
|
+
# Histogram
|
|
122
|
+
counts, bin_edges = np.histogram(time_filtered, bins=bins, weights=pif_filtered)
|
|
123
|
+
time_centers = bin_edges[:-1] + 0.5 * binsize_actual
|
|
124
|
+
|
|
125
|
+
return time_centers, counts
|
|
126
|
+
|
|
127
|
+
def _create_bins(self, binsize, time, t0, local_time):
|
|
128
|
+
"""
|
|
129
|
+
Create time bins for rebinning.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
binsize (float or array): Bin size or custom bin edges.
|
|
133
|
+
time (ndarray): Time array.
|
|
134
|
+
t0 (float): Start time.
|
|
135
|
+
local_time (bool): Whether using local time.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
tuple: (bins array, actual binsize).
|
|
139
|
+
"""
|
|
140
|
+
if isinstance(binsize, (list, np.ndarray)):
|
|
141
|
+
# Custom bin edges provided
|
|
142
|
+
bins = np.array(binsize)
|
|
143
|
+
binsize_actual = np.mean(np.diff(bins))
|
|
144
|
+
else:
|
|
145
|
+
# Uniform binning
|
|
146
|
+
binsize_actual = binsize if local_time else binsize / 86400
|
|
147
|
+
bins = np.arange(t0, time[-1] + binsize_actual, binsize_actual)
|
|
148
|
+
|
|
149
|
+
return bins, binsize_actual
|
|
150
|
+
|
|
151
|
+
def _create_event_mask(self, emin, emax, custom_mask=None):
|
|
152
|
+
"""
|
|
153
|
+
Create combined event filter mask.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
emin (float): Minimum energy in keV.
|
|
157
|
+
emax (float): Maximum energy in keV.
|
|
158
|
+
custom_mask (ndarray, optional): Additional mask to apply.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
ndarray: Boolean mask for events.
|
|
162
|
+
"""
|
|
163
|
+
# Energy filter
|
|
164
|
+
mask = (self.energies >= emin) & (self.energies < emax)
|
|
165
|
+
|
|
166
|
+
# Custom filter (optional)
|
|
167
|
+
if custom_mask is not None:
|
|
168
|
+
mask &= custom_mask
|
|
169
|
+
|
|
170
|
+
return mask
|
|
171
|
+
|
|
172
|
+
def rebin_by_modules(self, binsize, emin, emax, local_time=True, custom_mask=None):
|
|
173
|
+
"""
|
|
174
|
+
Rebins the events by all 8 detector modules with the specified bin size and energy range.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
binsize (float): The bin size in seconds.
|
|
178
|
+
emin (float): The minimum energy value in keV.
|
|
179
|
+
emax (float): The maximum energy value in keV.
|
|
180
|
+
local_time (bool, optional): If True, returns local time. Defaults to True.
|
|
181
|
+
custom_mask (ndarray, optional): A custom mask to apply. Defaults to None.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
tuple: (times, counts) where:
|
|
185
|
+
- times: array of time bin centers
|
|
186
|
+
- counts: list of 8 arrays, one for each module
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ValueError: If emin >= emax.
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
>>> times, counts = lc.rebin_by_modules(binsize=1.0, emin=30, emax=300)
|
|
193
|
+
>>> module_3_lc = counts[3] # Get lightcurve for module 3
|
|
194
|
+
"""
|
|
195
|
+
if emin >= emax:
|
|
196
|
+
raise ValueError("emin must be less than emax")
|
|
197
|
+
|
|
198
|
+
time = self.local_time if local_time else self.time
|
|
199
|
+
t0 = 0 if local_time else self.t0
|
|
200
|
+
binsize_adj = binsize if local_time else binsize / 86400
|
|
201
|
+
bins = np.arange(t0, time[-1] + binsize_adj, binsize_adj)
|
|
202
|
+
times = bins[:-1] + 0.5 * binsize_adj
|
|
203
|
+
|
|
204
|
+
energy_mask = (self.energies >= emin) & (self.energies < emax)
|
|
205
|
+
if custom_mask is not None:
|
|
206
|
+
energy_mask &= custom_mask
|
|
207
|
+
|
|
208
|
+
time_filtered = time[energy_mask]
|
|
209
|
+
dety_filtered = self.dety[energy_mask]
|
|
210
|
+
detz_filtered = self.detz[energy_mask]
|
|
211
|
+
pif_filtered = self.pif[energy_mask]
|
|
212
|
+
|
|
213
|
+
# Compute module indices using digitize
|
|
214
|
+
dety_bin = np.digitize(dety_filtered, DETY_BOUNDS) - 1 # 0 or 1
|
|
215
|
+
detz_bin = np.digitize(detz_filtered, DETZ_BOUNDS) - 1 # 0, 1, 2, or 3
|
|
216
|
+
module_idx = dety_bin + detz_bin * 2 # Flat index: 0-7
|
|
217
|
+
|
|
218
|
+
counts = []
|
|
219
|
+
for i in range(8):
|
|
220
|
+
mask = module_idx == i
|
|
221
|
+
counts.append(np.histogram(time_filtered[mask], bins=bins, weights=pif_filtered[mask])[0])
|
|
222
|
+
|
|
223
|
+
return times, counts
|
|
224
|
+
|
|
225
|
+
def cts(self, t1, t2, emin, emax, local_time=True, bkg=False):
|
|
226
|
+
"""
|
|
227
|
+
Calculates the counts in the specified time and energy range.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
t1 (float): The start time (seconds or IJD depending on local_time).
|
|
231
|
+
t2 (float): The end time (seconds or IJD depending on local_time).
|
|
232
|
+
emin (float): The minimum energy value in keV.
|
|
233
|
+
emax (float): The maximum energy value in keV.
|
|
234
|
+
local_time (bool, optional): If True, uses local time. Defaults to True.
|
|
235
|
+
bkg (bool, optional): Reserved for future background subtraction. Defaults to False.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
float: The total counts in the specified range.
|
|
239
|
+
"""
|
|
240
|
+
time = self.local_time if local_time else self.time
|
|
241
|
+
return np.sum(self.pif[(time >= t1) & (time < t2) & (self.energies >= emin) & (self.energies < emax)])
|
|
242
|
+
|
|
243
|
+
def ijd2loc(self, ijd_time):
|
|
244
|
+
"""
|
|
245
|
+
Converts IJD (INTEGRAL Julian Date) time to local time.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
ijd_time (float or ndarray): The IJD time value(s).
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
float or ndarray: The local time in seconds from t0.
|
|
252
|
+
"""
|
|
253
|
+
return (ijd_time - self.t0) * 86400
|
|
254
|
+
|
|
255
|
+
def loc2ijd(self, evt_time):
|
|
256
|
+
"""
|
|
257
|
+
Converts local time to IJD (INTEGRAL Julian Date) time.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
evt_time (float or ndarray): The local time in seconds from t0.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
float or ndarray: The IJD time value(s).
|
|
264
|
+
"""
|
|
265
|
+
return evt_time / 86400 + self.t0
|
isgri/utils/pif.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
DETZ_BOUNDS, DETY_BOUNDS = [0, 32, 66, 100, 134], [0, 64, 130] # Detector module boundaries
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def select_isgri_module(module_no):
|
|
7
|
+
col = 0 if module_no % 2 == 0 else 1
|
|
8
|
+
row = module_no // 2
|
|
9
|
+
x1, x2 = DETZ_BOUNDS[row], DETZ_BOUNDS[row + 1]
|
|
10
|
+
y1, y2 = DETY_BOUNDS[col], DETY_BOUNDS[col + 1]
|
|
11
|
+
return x1, x2, y1, y2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def apply_pif_mask(pif_file, events, pif_threshold=0.5):
|
|
15
|
+
pif_filter = pif_file > pif_threshold
|
|
16
|
+
piffed_events = events[pif_filter[events["DETZ"], events["DETY"]]]
|
|
17
|
+
pif = pif_file[piffed_events["DETZ"], piffed_events["DETY"]]
|
|
18
|
+
return piffed_events, pif
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def coding_fraction(pif_file, events):
|
|
22
|
+
pif_cod = pif_file == 1
|
|
23
|
+
pif_cod = events[pif_cod[events["DETZ"], events["DETY"]]]
|
|
24
|
+
cody = (np.max(pif_cod["DETY"]) - np.min(pif_cod["DETY"])) / 129
|
|
25
|
+
codz = (np.max(pif_cod["DETZ"]) - np.min(pif_cod["DETZ"])) / 133
|
|
26
|
+
pif_cod = codz * cody
|
|
27
|
+
return pif_cod
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def estimate_active_modules(mask):
|
|
31
|
+
m, n = DETZ_BOUNDS, DETY_BOUNDS # Separate modules
|
|
32
|
+
mods = []
|
|
33
|
+
for module_no in range(8):
|
|
34
|
+
x1, x2, y1, y2 = select_isgri_module(module_no)
|
|
35
|
+
a = mask[x1:x2, y1:y2].flatten()
|
|
36
|
+
if len(a[a > 0.01]) / len(a) > 0.2:
|
|
37
|
+
mods.append(1)
|
|
38
|
+
else:
|
|
39
|
+
mods.append(0)
|
|
40
|
+
mods = np.array(mods)
|
|
41
|
+
return mods
|
isgri/utils/quality.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .lightcurve import LightCurve
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class QualityMetrics:
|
|
6
|
+
"""
|
|
7
|
+
A class for computing statistical quality metrics for ISGRI lightcurves.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
lightcurve (LightCurve): The LightCurve instance to analyze.
|
|
11
|
+
binsize (float): The bin size in seconds.
|
|
12
|
+
emin (float): The minimum energy value in keV.
|
|
13
|
+
emax (float): The maximum energy value in keV.
|
|
14
|
+
local_time (bool): If True, uses local time. If False, uses IJD time. GTIs are always in IJD.
|
|
15
|
+
module_data (dict): Cached rebinned data for all modules.
|
|
16
|
+
|
|
17
|
+
Methods:
|
|
18
|
+
raw_chi_squared: Computes raw reduced chi-squared.
|
|
19
|
+
sigma_clip_chi_squared: Computes sigma-clipped reduced chi-squared.
|
|
20
|
+
gti_chi_squared: Computes GTI-filtered reduced chi-squared.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, lightcurve: LightCurve | None = None, binsize=1.0, emin=1.0, emax=1000.0, local_time=False):
|
|
24
|
+
"""
|
|
25
|
+
Initialize QualityMetrics instance.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
lightcurve (LightCurve, optional): The LightCurve instance to analyze. Defaults to None.
|
|
29
|
+
binsize (float, optional): The bin size in seconds. Defaults to 1.0.
|
|
30
|
+
emin (float, optional): The minimum energy value in keV. Defaults to 1.0.
|
|
31
|
+
emax (float, optional): The maximum energy value in keV. Defaults to 1000.0.
|
|
32
|
+
local_time (bool, optional): If True, uses local time. If False, uses IJD time. Defaults to False.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
TypeError: If lightcurve is not a LightCurve instance or None.
|
|
36
|
+
"""
|
|
37
|
+
if type(lightcurve) not in [LightCurve, type(None)]:
|
|
38
|
+
raise TypeError("lightcurve must be an instance of LightCurve or None")
|
|
39
|
+
self.lightcurve = lightcurve
|
|
40
|
+
self.binsize = binsize
|
|
41
|
+
self.emin = emin
|
|
42
|
+
self.emax = emax
|
|
43
|
+
self.local_time = local_time
|
|
44
|
+
self.module_data = None
|
|
45
|
+
|
|
46
|
+
def _compute_counts(self):
|
|
47
|
+
"""
|
|
48
|
+
Compute or retrieve cached rebinned counts for all modules.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
None
|
|
52
|
+
Returns:
|
|
53
|
+
dict: Dictionary with 'time' and 'counts' arrays.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If lightcurve is not set.
|
|
57
|
+
"""
|
|
58
|
+
if self.lightcurve is None:
|
|
59
|
+
raise ValueError("Lightcurve is not set.")
|
|
60
|
+
if self.module_data is not None:
|
|
61
|
+
return self.module_data
|
|
62
|
+
time, counts = self.lightcurve.rebin_by_modules(
|
|
63
|
+
binsize=self.binsize, emin=self.emin, emax=self.emax, local_time=self.local_time
|
|
64
|
+
)
|
|
65
|
+
module_data = {"time": time, "counts": counts}
|
|
66
|
+
self.module_data = module_data
|
|
67
|
+
return module_data
|
|
68
|
+
|
|
69
|
+
def _compute_chi_squared_red(self, counts, return_all=False):
|
|
70
|
+
"""
|
|
71
|
+
Compute reduced chi-squared for count data.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
counts (ndarray): Count array(s) to analyze.
|
|
75
|
+
return_all (bool, optional): If True, returns chi-squared for each array. If False, returns mean. Defaults to False.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
float or ndarray: Reduced chi-squared value(s).
|
|
79
|
+
"""
|
|
80
|
+
counts = np.asarray(counts)
|
|
81
|
+
counts = np.where(counts == 0, np.nan, counts)
|
|
82
|
+
mean_counts = np.nanmean(counts, axis=-1, keepdims=True)
|
|
83
|
+
chi_squared = np.nansum((counts - mean_counts) ** 2 / mean_counts, axis=-1)
|
|
84
|
+
dof = counts.shape[-1] - 1
|
|
85
|
+
if return_all:
|
|
86
|
+
return chi_squared / dof
|
|
87
|
+
return np.nanmean(chi_squared / dof)
|
|
88
|
+
|
|
89
|
+
def raw_chi_squared(self, counts=None, return_all=False):
|
|
90
|
+
"""
|
|
91
|
+
Computes raw reduced chi-squared for lightcurve data.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
counts (ndarray, optional): Count array(s) to analyze. If None, uses cached module data. Defaults to None.
|
|
95
|
+
return_all (bool, optional): If True, returns chi-squared for each module. If False, returns mean. Defaults to False.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
float or ndarray: Reduced chi-squared value(s).
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
>>> qm = QualityMetrics(lc, binsize=1.0, emin=30, emax=300)
|
|
102
|
+
>>> chi = qm.raw_chi_squared()
|
|
103
|
+
>>> chi_all_modules = qm.raw_chi_squared(return_all=True)
|
|
104
|
+
"""
|
|
105
|
+
if counts is None:
|
|
106
|
+
counts = self._compute_counts()["counts"]
|
|
107
|
+
return self._compute_chi_squared_red(counts, return_all=return_all)
|
|
108
|
+
|
|
109
|
+
def sigma_clip_chi_squared(self, sigma=1.0, counts=None, return_all=False):
|
|
110
|
+
"""
|
|
111
|
+
Computes sigma-clipped reduced chi-squared for lightcurve data.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
sigma (float, optional): Sigma clipping threshold in standard deviations. Defaults to 1.0.
|
|
115
|
+
counts (ndarray, optional): Count array(s) to analyze. If None, uses cached module data. Defaults to None.
|
|
116
|
+
return_all (bool, optional): If True, returns chi-squared for each module. If False, returns mean. Defaults to False.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
float or ndarray: Reduced chi-squared value(s) after sigma clipping.
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
>>> qm = QualityMetrics(lc, binsize=1.0, emin=30, emax=300)
|
|
123
|
+
>>> chi = qm.sigma_clip_chi_squared(sigma=3.0)
|
|
124
|
+
"""
|
|
125
|
+
if counts is None:
|
|
126
|
+
counts = self._compute_counts()["counts"]
|
|
127
|
+
mean_count = np.mean(counts, axis=-1, keepdims=True)
|
|
128
|
+
std_count = np.std(counts, axis=-1, keepdims=True)
|
|
129
|
+
mask = np.abs(counts - mean_count) < sigma * std_count
|
|
130
|
+
filtered_counts = np.where(mask, counts, np.nan)
|
|
131
|
+
return self._compute_chi_squared_red(filtered_counts, return_all=return_all)
|
|
132
|
+
|
|
133
|
+
def gti_chi_squared(self, time=None, counts=None, gtis=None, return_all=False):
|
|
134
|
+
"""
|
|
135
|
+
Computes GTI-filtered reduced chi-squared for lightcurve data.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
time (ndarray, optional): Time array. If None, uses cached module data. Defaults to None.
|
|
139
|
+
counts (ndarray, optional): Count array(s) to analyze. If None, uses cached module data. Defaults to None.
|
|
140
|
+
gtis (ndarray, optional): Good Time Intervals (N, 2) array. If None, uses lightcurve GTIs. Defaults to None.
|
|
141
|
+
return_all (bool, optional): If True, returns chi-squared for each module. If False, returns mean. Defaults to False.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
float or ndarray: Reduced chi-squared value(s) within GTIs only.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError: If no overlap between GTIs and lightcurve time range.
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
>>> qm = QualityMetrics(lc, binsize=1.0, emin=30, emax=300)
|
|
151
|
+
>>> chi = qm.gti_chi_squared()
|
|
152
|
+
"""
|
|
153
|
+
if counts is None or time is None:
|
|
154
|
+
data = self._compute_counts()
|
|
155
|
+
time, counts = data["time"], data["counts"]
|
|
156
|
+
if gtis is None:
|
|
157
|
+
gtis = self.lightcurve.gtis
|
|
158
|
+
if gtis[0, 0] > time[-1] or gtis[-1, 1] < time[0]:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
"No overlap between GTIs and lightcurve time. If Lightcurve is set, verify time is in IJD."
|
|
161
|
+
)
|
|
162
|
+
gti_mask = np.zeros_like(time, dtype=bool)
|
|
163
|
+
for gti_start, gti_stop in gtis:
|
|
164
|
+
gti_mask |= (time >= gti_start) & (time <= gti_stop)
|
|
165
|
+
filtered_counts = np.where(gti_mask, counts, np.nan)
|
|
166
|
+
return self._compute_chi_squared_red(filtered_counts, return_all=return_all)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from astropy.time import Time
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def ijd2utc(t):
|
|
5
|
+
"""
|
|
6
|
+
Converts IJD (INTEGRAL Julian Date) time to UTC ISO format.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
t (float or ndarray): IJD time value(s).
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
str or ndarray: UTC time in ISO format (YYYY-MM-DD HH:MM:SS.sss).
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
>>> ijd2utc(0.0)
|
|
16
|
+
'1999-12-31 23:58:55.817'
|
|
17
|
+
>>> ijd2utc(1000.5)
|
|
18
|
+
'2002-09-27 11:58:55.816'
|
|
19
|
+
"""
|
|
20
|
+
return Time(t + 51544, format="mjd", scale="tt").utc.iso
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def utc2ijd(t):
|
|
24
|
+
"""
|
|
25
|
+
Converts UTC ISO format time to IJD (INTEGRAL Julian Date).
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
t (str or ndarray): UTC time in ISO format (YYYY-MM-DD HH:MM:SS).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
float or ndarray: IJD time value(s).
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
>>> utc2ijd('1999-12-31 23:58:55.817')
|
|
35
|
+
0.0
|
|
36
|
+
>>> utc2ijd('2002-09-27 00:00:00')
|
|
37
|
+
1000.0
|
|
38
|
+
"""
|
|
39
|
+
return Time(t, format="iso", scale="utc").tt.mjd - 51544
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: isgri
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python package for INTEGRAL IBIS/ISGRI lightcurve analysis
|
|
5
|
+
Author: Dominik Patryk Pacholski
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: astropy>=7.2.0
|
|
10
|
+
Requires-Dist: numpy>=2.3.5
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# isgri
|
|
14
|
+
|
|
15
|
+
Python package for ISGRI (INTEGRAL Soft Gamma-Ray Imager) lightcurve analysis.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install isgri
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or from source:
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/yourusername/isgri.git
|
|
26
|
+
cd isgri
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from isgri.utils import LightCurve, QualityMetrics
|
|
34
|
+
|
|
35
|
+
# Load data
|
|
36
|
+
lc = LightCurve.load_data(
|
|
37
|
+
events_path='/path/to/isgri_events.fits',
|
|
38
|
+
pif_path='/path/to/source_model.fits'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Create lightcurve
|
|
42
|
+
time, counts = lc.rebin(binsize=1.0, emin=30, emax=300)
|
|
43
|
+
|
|
44
|
+
# Create lightcurve per each module
|
|
45
|
+
times, module_counts = lc.rebin_by_modules(binsize=1.0, emin=30, emax=300)
|
|
46
|
+
|
|
47
|
+
# Compute quality metrics
|
|
48
|
+
qm = QualityMetrics(lc, binsize=1.0, emin=30, emax=300)
|
|
49
|
+
chi = qm.raw_chi_squared()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- Load ISGRI events and PIF files
|
|
55
|
+
- Rebin lightcurves with custom binning
|
|
56
|
+
- Module-by-module analysis (8 detector modules)
|
|
57
|
+
- Quality metrics (chi-squared variability tests)
|
|
58
|
+
- Time conversions (IJD ↔ UTC)
|
|
59
|
+
- Custom event filtering
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
- **Tutorial**: See [demo/lightcurve_walkthrough.ipynb](demo/lightcurve_walkthrough.ipynb)
|
|
64
|
+
- **API docs**: All functions have docstrings - use `help(LightCurve)`
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
isgri/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
isgri/utils/__init__.py,sha256=H83Al7urc6LNW5KUzUBRdtRBUTahiZmkehKFiK90RrU,183
|
|
3
|
+
isgri/utils/file_loaders.py,sha256=c0k7B4Uk1OfNKC_TkHtRbHuEeAu4lHxcbCoYLPuRw5E,5503
|
|
4
|
+
isgri/utils/lightcurve.py,sha256=qhD5NvHnaSWPWaUh0nWXObj93f-ii6TQu92FIL7aznk,10191
|
|
5
|
+
isgri/utils/pif.py,sha256=oxndfoZp-y9FO_TXcCP3dmesvh-sC7hAy2u_1YwGitw,1317
|
|
6
|
+
isgri/utils/quality.py,sha256=vNOkIFOkOTP1FAy_1-t5Xvi38gp6UNgXJN3T-DtM8es,7209
|
|
7
|
+
isgri/utils/time_conversion.py,sha256=47aTXn6cvi-LjOptDY2W-rjeF_PjthZer61VIsJqsro,908
|
|
8
|
+
isgri-0.1.0.dist-info/METADATA,sha256=6rocF7iR95JEZnw51lyDBIIiBd8dp5wMBa8Q7xfJ4kE,1487
|
|
9
|
+
isgri-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
+
isgri-0.1.0.dist-info/licenses/LICENSE,sha256=Q8oxmHR1cSnEXSHCjY3qeXMtupZI_1ZQZ1MBt4oeANE,1102
|
|
11
|
+
isgri-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dominik Patryk Pacholski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|