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 ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ from .lightcurve import LightCurve
2
+ from .time_conversion import ijd2utc, utc2ijd
3
+ from .quality import QualityMetrics
4
+
5
+ __all__ = ["LightCurve", "ijd2utc", "utc2ijd", "QualityMetrics"]
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.