isgri 0.3.0__py3-none-any.whl → 0.5.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/utils/lightcurve.py CHANGED
@@ -1,265 +1,409 @@
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, weights, 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.weights = weights
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, weights, metadata_pif = load_isgri_pif(pif_path, events, pif_threshold, pif_extension)
75
- else:
76
- weights = 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, weights, 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
- weights_filtered = self.weights[mask]
120
-
121
- # Histogram
122
- counts, bin_edges = np.histogram(time_filtered, bins=bins, weights=weights_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
- weights_filtered = self.weights[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=weights_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.weights[(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
1
+ """
2
+ ISGRI Light Curve Analysis
3
+ ===========================
4
+
5
+ Tools for working with INTEGRAL/ISGRI event data and creating light curves.
6
+
7
+ Classes
8
+ -------
9
+ LightCurve : Main light curve class
10
+
11
+ Examples
12
+ --------
13
+ >>> from isgri.utils import LightCurve
14
+ >>>
15
+ >>> # Load events with PIF weighting
16
+ >>> lc = LightCurve.load_data(
17
+ ... events_path="events.fits",
18
+ ... pif_path="model.fits",
19
+ ... pif_threshold=0.5
20
+ ... )
21
+ >>>
22
+ >>> # Create 1-second binned light curve
23
+ >>> time, counts = lc.rebin(binsize=1.0, emin=20, emax=100)
24
+ >>>
25
+ >>> # Analyze by detector module
26
+ >>> times, module_lcs = lc.rebin_by_modules(1.0, 20, 100)
27
+ """
28
+
29
+ from astropy.io import fits
30
+ import numpy as np
31
+ from numpy.typing import NDArray
32
+ from typing import Optional, Union, Tuple, List
33
+ from pathlib import Path
34
+ import os
35
+ from .file_loaders import load_isgri_events, load_isgri_pif, default_pif_metadata, merge_metadata
36
+ from .pif import DETZ_BOUNDS, DETY_BOUNDS
37
+
38
+
39
+ class LightCurve:
40
+ """
41
+ ISGRI light curve analysis class.
42
+
43
+ Handles event data with optional detector response (PIF) weighting.
44
+ Provides rebinning, module-level analysis, and time conversions.
45
+
46
+ Parameters
47
+ ----------
48
+ time : ndarray
49
+ IJD time values of events
50
+ energies : ndarray
51
+ Energy values in keV
52
+ gtis : ndarray
53
+ Good Time Intervals (start, stop) in IJD
54
+ dety : ndarray
55
+ Y detector coordinates (mm)
56
+ detz : ndarray
57
+ Z detector coordinates (mm)
58
+ weights : ndarray
59
+ PIF weight for each event (1.0 if no PIF)
60
+ metadata : dict
61
+ Event metadata (SWID, source info, etc.)
62
+
63
+ Attributes
64
+ ----------
65
+ t0 : float
66
+ Reference time (first event time in IJD)
67
+ local_time : ndarray
68
+ Time relative to t0 in seconds
69
+
70
+ Examples
71
+ --------
72
+ >>> lc = LightCurve.load_data("events.fits", pif_path="model.fits")
73
+ >>> time, counts = lc.rebin(binsize=1.0, emin=20, emax=100)
74
+ >>> print(f"Total counts: {counts.sum()}")
75
+
76
+ >>> # Module-by-module analysis
77
+ >>> times, module_counts = lc.rebin_by_modules(1.0, 20, 100)
78
+ >>> print(f"Module 3 counts: {module_counts[3].sum()}")
79
+
80
+ See Also
81
+ --------
82
+ load_data : Load events from FITS files
83
+ rebin : Rebin light curve
84
+ rebin_by_modules : Rebin by detector module
85
+ """
86
+
87
+ time: NDArray[np.float64]
88
+ energies: NDArray[np.float64]
89
+ gtis: NDArray[np.float64]
90
+ t0: float
91
+ local_time: NDArray[np.float64]
92
+ dety: NDArray[np.float64]
93
+ detz: NDArray[np.float64]
94
+ weights: NDArray[np.float64]
95
+ metadata: dict
96
+
97
+ def __init__(
98
+ self,
99
+ time: NDArray[np.float64],
100
+ energies: NDArray[np.float64],
101
+ gtis: NDArray[np.float64],
102
+ dety: NDArray[np.float64],
103
+ detz: NDArray[np.float64],
104
+ weights: NDArray[np.float64],
105
+ metadata: dict,
106
+ ) -> None:
107
+ """
108
+ Initialize LightCurve instance.
109
+
110
+ Parameters
111
+ ----------
112
+ time : ndarray
113
+ IJD time values
114
+ energies : ndarray
115
+ Energy values in keV
116
+ gtis : ndarray
117
+ Good Time Intervals
118
+ dety : ndarray
119
+ Y detector coordinates
120
+ detz : ndarray
121
+ Z detector coordinates
122
+ weights : ndarray
123
+ PIF weights for each event
124
+ metadata : dict
125
+ Event metadata
126
+ """
127
+ self.time = time
128
+ self.energies = energies
129
+ self.gtis = gtis
130
+ self.t0 = time[0]
131
+ self.local_time = (time - self.t0) * 86400
132
+ self.dety = dety
133
+ self.detz = detz
134
+ self.weights = weights
135
+ self.metadata = metadata
136
+
137
+ @classmethod
138
+ def load_data(
139
+ cls,
140
+ events_path: Optional[Union[str, Path]] = None,
141
+ pif_path: Optional[Union[str, Path]] = None,
142
+ scw: Optional[str] = None,
143
+ source: Optional[str] = None,
144
+ pif_threshold: float = 0.5,
145
+ pif_extension: int = -1,
146
+ ) -> "LightCurve":
147
+ """
148
+ Loads the events from the given events file and PIF file (optional).
149
+
150
+ Args:
151
+ events_path (str): The path to the events file or directory.
152
+ pif_path (str, optional): The path to the PIF file. Defaults to None.
153
+ scw (str, optional): SCW identifier for auto-path resolution. Defaults to None.
154
+ source (str, optional): Source name for auto-path resolution. Defaults to None.
155
+ pif_threshold (float, optional): The PIF threshold value. Defaults to 0.5.
156
+ pif_extension (int, optional): PIF file extension index. Defaults to -1.
157
+
158
+ Returns:
159
+ LightCurve: An instance of the LightCurve class.
160
+ """
161
+ events, gtis, metadata = load_isgri_events(events_path)
162
+ if pif_path:
163
+ if pif_threshold < 0 or pif_threshold > 1:
164
+ raise ValueError(f"pif_threshold must be in [0, 1], got {pif_threshold}")
165
+
166
+ events, weights, metadata_pif = load_isgri_pif(pif_path, events, pif_threshold, pif_extension)
167
+ else:
168
+ weights = np.ones(len(events))
169
+ metadata_pif = default_pif_metadata()
170
+
171
+ metadata = merge_metadata(metadata, metadata_pif)
172
+ time = events["TIME"]
173
+ energies = events["ISGRI_ENERGY"]
174
+ dety, detz = events["DETY"], events["DETZ"]
175
+ return cls(time, energies, gtis, dety, detz, weights, metadata)
176
+
177
+ def rebin(
178
+ self,
179
+ binsize: Union[float, NDArray[np.float64], List[float]],
180
+ emin: float,
181
+ emax: float,
182
+ local_time: bool = True,
183
+ custom_mask: Optional[NDArray[np.bool_]] = None,
184
+ ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
185
+ """
186
+ Rebins the events with the specified bin size and energy range.
187
+
188
+ Args:
189
+ binsize (float or array): The bin size in seconds, or array of bin edges.
190
+ emin (float): The minimum energy value in keV.
191
+ emax (float): The maximum energy value in keV.
192
+ local_time (bool, optional): If True, returns local time. If False, returns IJD time. Defaults to True.
193
+ custom_mask (ndarray, optional): Additional boolean mask to apply. Defaults to None.
194
+
195
+ Returns:
196
+ tuple: (time, counts) arrays.
197
+
198
+ Raises:
199
+ ValueError: If emin >= emax.
200
+
201
+ Examples:
202
+ >>> time, counts = lc.rebin(binsize=1.0, emin=30, emax=300)
203
+ >>> time, counts = lc.rebin(binsize=[0, 1, 2, 5, 10], emin=50, emax=200)
204
+ """
205
+ # Validate inputs
206
+ if emin >= emax:
207
+ raise ValueError(f"emin ({emin}) must be less than emax ({emax})")
208
+
209
+ if emin < 0:
210
+ raise ValueError(f"emin must be non-negative, got {emin}")
211
+
212
+ if isinstance(binsize, (int, float)) and binsize <= 0:
213
+ raise ValueError(f"binsize must be positive, got {binsize}")
214
+
215
+ if custom_mask is not None and len(custom_mask) != len(self.time):
216
+ raise ValueError(
217
+ f"custom_mask length ({len(custom_mask)}) must match " f"number of events ({len(self.time)})"
218
+ )
219
+
220
+ # Select time axis
221
+ time = self.local_time if local_time else self.time
222
+ t0 = 0 if local_time else self.t0
223
+
224
+ # Create bins
225
+ bins, binsize_actual = self._create_bins(binsize, time, t0, local_time)
226
+
227
+ # Apply filters
228
+ mask = self._create_event_mask(emin, emax, custom_mask)
229
+ time_filtered = time[mask]
230
+ weights_filtered = self.weights[mask]
231
+
232
+ # Histogram
233
+ counts, bin_edges = np.histogram(time_filtered, bins=bins, weights=weights_filtered)
234
+ time_centers = bin_edges[:-1] + 0.5 * binsize_actual
235
+
236
+ return time_centers, counts
237
+
238
+ def _create_bins(
239
+ self,
240
+ binsize: Union[float, NDArray[np.float64], List[float]],
241
+ time: NDArray[np.float64],
242
+ t0: float,
243
+ local_time: bool,
244
+ ) -> Tuple[NDArray[np.float64], float]:
245
+ """
246
+ Create time bins for rebinning.
247
+
248
+ Args:
249
+ binsize (float or array): Bin size or custom bin edges.
250
+ time (ndarray): Time array.
251
+ t0 (float): Start time.
252
+ local_time (bool): Whether using local time.
253
+
254
+ Returns:
255
+ tuple: (bins array, actual binsize).
256
+ """
257
+ if isinstance(binsize, (list, np.ndarray)):
258
+ # Custom bin edges provided
259
+ bins = np.array(binsize)
260
+ binsize_actual = np.mean(np.diff(bins))
261
+ else:
262
+ # Uniform binning
263
+ binsize_actual = binsize if local_time else binsize / 86400
264
+ bins = np.arange(t0, time[-1] + binsize_actual, binsize_actual)
265
+
266
+ return bins, binsize_actual
267
+
268
+ def _create_event_mask(
269
+ self,
270
+ emin: float,
271
+ emax: float,
272
+ custom_mask: Optional[NDArray[np.bool_]] = None,
273
+ ) -> NDArray[np.bool_]:
274
+ """
275
+ Create combined event filter mask.
276
+
277
+ Args:
278
+ emin (float): Minimum energy in keV.
279
+ emax (float): Maximum energy in keV.
280
+ custom_mask (ndarray, optional): Additional mask to apply.
281
+
282
+ Returns:
283
+ ndarray: Boolean mask for events.
284
+ """
285
+ # Energy filter
286
+ mask = (self.energies >= emin) & (self.energies < emax)
287
+
288
+ # Custom filter (optional)
289
+ if custom_mask is not None:
290
+ mask &= custom_mask
291
+
292
+ return mask
293
+
294
+ def rebin_by_modules(
295
+ self,
296
+ binsize: float,
297
+ emin: float,
298
+ emax: float,
299
+ local_time: bool = True,
300
+ custom_mask: Optional[NDArray[np.bool_]] = None,
301
+ ) -> Tuple[NDArray[np.float64], List[NDArray[np.float64]]]:
302
+ """
303
+ Rebins the events by all 8 detector modules with the specified bin size and energy range.
304
+
305
+ Args:
306
+ binsize (float): The bin size in seconds.
307
+ emin (float): The minimum energy value in keV.
308
+ emax (float): The maximum energy value in keV.
309
+ local_time (bool, optional): If True, returns local time. Defaults to True.
310
+ custom_mask (ndarray, optional): A custom mask to apply. Defaults to None.
311
+
312
+ Returns:
313
+ tuple: (times, counts) where:
314
+ - times: array of time bin centers
315
+ - counts: list of 8 arrays, one for each module
316
+
317
+ Raises:
318
+ ValueError: If emin >= emax.
319
+
320
+ Examples:
321
+ >>> times, counts = lc.rebin_by_modules(binsize=1.0, emin=30, emax=300)
322
+ >>> module_3_lc = counts[3] # Get lightcurve for module 3
323
+ """
324
+ if emin >= emax:
325
+ raise ValueError("emin must be less than emax")
326
+
327
+ time = self.local_time if local_time else self.time
328
+ t0 = 0 if local_time else self.t0
329
+ binsize_adj = binsize if local_time else binsize / 86400
330
+ bins = np.arange(t0, time[-1] + binsize_adj, binsize_adj)
331
+ times = bins[:-1] + 0.5 * binsize_adj
332
+
333
+ energy_mask = (self.energies >= emin) & (self.energies < emax)
334
+ if custom_mask is not None:
335
+ energy_mask &= custom_mask
336
+
337
+ time_filtered = time[energy_mask]
338
+ dety_filtered = self.dety[energy_mask]
339
+ detz_filtered = self.detz[energy_mask]
340
+ weights_filtered = self.weights[energy_mask]
341
+
342
+ # Compute module indices using digitize
343
+ dety_bin = np.digitize(dety_filtered, DETY_BOUNDS) - 1 # 0 or 1
344
+ detz_bin = np.digitize(detz_filtered, DETZ_BOUNDS) - 1 # 0, 1, 2, or 3
345
+ module_idx = dety_bin + detz_bin * 2 # Flat index: 0-7
346
+
347
+ counts = []
348
+ for i in range(8):
349
+ mask = module_idx == i
350
+ counts.append(np.histogram(time_filtered[mask], bins=bins, weights=weights_filtered[mask])[0])
351
+
352
+ return times, counts
353
+
354
+ def cts(
355
+ self,
356
+ t1: float,
357
+ t2: float,
358
+ emin: float,
359
+ emax: float,
360
+ local_time: bool = True,
361
+ ) -> float:
362
+ """
363
+ Calculates the counts in the specified time and energy range.
364
+
365
+ Args:
366
+ t1 (float): The start time (seconds or IJD depending on local_time).
367
+ t2 (float): The end time (seconds or IJD depending on local_time).
368
+ emin (float): The minimum energy value in keV.
369
+ emax (float): The maximum energy value in keV.
370
+ local_time (bool, optional): If True, uses local time. Defaults to True.
371
+
372
+ Returns:
373
+ float: The total counts in the specified range.
374
+ """
375
+ time = self.local_time if local_time else self.time
376
+ return np.sum(self.weights[(time >= t1) & (time < t2) & (self.energies >= emin) & (self.energies < emax)])
377
+
378
+ def ijd2loc(self, ijd_time: Union[float, NDArray[np.float64]]) -> Union[float, NDArray[np.float64]]:
379
+ """
380
+ Converts IJD (INTEGRAL Julian Date) time to local time.
381
+
382
+ Args:
383
+ ijd_time (float or ndarray): The IJD time value(s).
384
+
385
+ Returns:
386
+ float or ndarray: The local time in seconds from t0.
387
+ """
388
+ return (ijd_time - self.t0) * 86400
389
+
390
+ def loc2ijd(self, evt_time: Union[float, NDArray[np.float64]]) -> Union[float, NDArray[np.float64]]:
391
+ """
392
+ Converts local time to IJD (INTEGRAL Julian Date) time.
393
+
394
+ Args:
395
+ evt_time (float or ndarray): The local time in seconds from t0.
396
+
397
+ Returns:
398
+ float or ndarray: The IJD time value(s).
399
+ """
400
+ return evt_time / 86400 + self.t0
401
+
402
+ def __repr__(self) -> str:
403
+ """String representation."""
404
+ return (
405
+ f"LightCurve(n_events={len(self.time)}, "
406
+ f"time_range=({self.time[0]:.3f}, {self.time[-1]:.3f}) IJD, "
407
+ f"energy_range=({self.energies.min():.1f}, {self.energies.max():.1f}) keV, "
408
+ f"scw={self.metadata.get('SWID', 'Unknown')})"
409
+ )