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.
@@ -1,159 +1,392 @@
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": [1] * 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
1
+ """
2
+ ISGRI Data File Loaders
3
+ ========================
4
+
5
+ Load INTEGRAL/ISGRI event files and detector response (PIF) files.
6
+
7
+ Functions
8
+ ---------
9
+ load_isgri_events : Load photon events from FITS file
10
+ load_isgri_pif : Load and apply detector response (PIF) file
11
+ verify_events_path : Validate and resolve events file path
12
+ default_pif_metadata : Create default PIF metadata
13
+ merge_metadata : Combine events and PIF metadata
14
+
15
+ Examples
16
+ --------
17
+ >>> from isgri.utils import load_isgri_events, load_isgri_pif
18
+ >>>
19
+ >>> # Load events
20
+ >>> events, gtis, metadata = load_isgri_events("isgri_events.fits")
21
+ >>> print(f"Loaded {len(events)} events")
22
+ >>>
23
+ >>> # Apply PIF weighting
24
+ >>> filtered_events, pif_weights, pif_meta = load_isgri_pif(
25
+ ... "pif_model.fits",
26
+ ... events,
27
+ ... pif_threshold=0.5
28
+ ... )
29
+ >>> print(f"Kept {len(filtered_events)}/{len(events)} events")
30
+ >>> print(f"Coding fraction: {pif_meta['cod']:.2%}")
31
+
32
+ Notes
33
+ -----
34
+ - Events files contain photon arrival times, energies, and detector positions
35
+ - PIF files contain detector response maps for specific source positions
36
+ - PIF weighting corrects for shadowing by the coded mask
37
+ """
38
+
39
+ from astropy.io import fits
40
+ from astropy.table import Table
41
+ import numpy as np
42
+ from numpy.typing import NDArray
43
+ from typing import Tuple, Dict, Optional, Union
44
+ from pathlib import Path
45
+ import os
46
+ from .pif import apply_pif_mask, coding_fraction, estimate_active_modules
47
+
48
+
49
+ def verify_events_path(path: Union[str, Path]) -> str:
50
+ """
51
+ Verify and resolve the events file path.
52
+
53
+ Parameters
54
+ ----------
55
+ path : str or Path
56
+ File path or directory path containing events file.
57
+ If directory, searches for files containing 'isgri_events'.
58
+
59
+ Returns
60
+ -------
61
+ resolved_path : str
62
+ Resolved absolute path to valid events file.
63
+
64
+ Raises
65
+ ------
66
+ FileNotFoundError
67
+ If path doesn't exist, no events file found, or multiple
68
+ candidates found in directory.
69
+ ValueError
70
+ If ISGR-EVTS-ALL extension not found in file.
71
+
72
+ Examples
73
+ --------
74
+ >>> # Direct file path
75
+ >>> path = verify_events_path("isgri_events.fits")
76
+
77
+ >>> # Directory with single events file
78
+ >>> path = verify_events_path("/data/scw/0234/001200340010.001")
79
+
80
+ >>> # Will raise error if multiple files
81
+ >>> verify_events_path("/data/scw/") # Multiple SCWs present
82
+ FileNotFoundError: Multiple isgri_events files found...
83
+ """
84
+ path = Path(path)
85
+
86
+ if path.is_file():
87
+ resolved_path = str(path)
88
+ elif path.is_dir():
89
+ candidate_files = [f for f in os.listdir(path) if "isgri_events" in f]
90
+
91
+ if len(candidate_files) == 0:
92
+ raise FileNotFoundError(f"No isgri_events file found in directory: {path}")
93
+ elif len(candidate_files) > 1:
94
+ raise FileNotFoundError(
95
+ f"Multiple isgri_events files found in directory: {path}\n"
96
+ f"Found: {candidate_files}\n"
97
+ "Please specify the exact file path."
98
+ )
99
+ else:
100
+ resolved_path = str(path / candidate_files[0])
101
+ else:
102
+ raise FileNotFoundError(f"Path does not exist: {path}")
103
+
104
+ # Verify FITS structure
105
+ try:
106
+ with fits.open(resolved_path) as hdu:
107
+ if "ISGR-EVTS-ALL" not in hdu:
108
+ raise ValueError(f"Invalid events file: ISGR-EVTS-ALL extension not found in {resolved_path}")
109
+ except Exception as e:
110
+ if isinstance(e, (FileNotFoundError, ValueError)):
111
+ raise
112
+ raise ValueError(f"Cannot open FITS file {resolved_path}: {e}")
113
+
114
+ return resolved_path
115
+
116
+
117
+ def load_isgri_events(events_path: Union[str, Path]) -> Tuple[Table, NDArray[np.float64], Dict]:
118
+ """
119
+ Load ISGRI photon events from FITS file.
120
+
121
+ Parameters
122
+ ----------
123
+ events_path : str or Path
124
+ Path to events FITS file or directory containing it.
125
+
126
+ Returns
127
+ -------
128
+ events : Table
129
+ Astropy Table with columns:
130
+ - TIME : Event time in IJD
131
+ - ISGRI_ENERGY : Energy in keV
132
+ - DETY : Y detector coordinate (0-129)
133
+ - DETZ : Z detector coordinate (0-133)
134
+ gtis : ndarray, shape (N, 2) or None
135
+ Good Time Intervals [start, stop] pairs in IJD.
136
+ If no GTI extension found, returns None.
137
+ metadata : dict
138
+ Header metadata with keys:
139
+ - REVOL : Revolution number
140
+ - SWID : Science Window ID
141
+ - TSTART, TSTOP : Start/stop times (IJD)
142
+ - RA_SCX, DEC_SCX : Pointing axis coordinates
143
+ - RA_SCZ, DEC_SCZ : Z-axis coordinates
144
+ - NoEVTS : Number of events in file
145
+
146
+ Raises
147
+ ------
148
+ FileNotFoundError
149
+ If events file not found or invalid.
150
+ ValueError
151
+ If required FITS extension missing.
152
+
153
+ Examples
154
+ --------
155
+ >>> events, gtis, meta = load_isgri_events("isgri_events.fits")
156
+ >>> print(f"Loaded {len(events)} events")
157
+ >>> print(f"Time range: {meta['TSTART']:.1f} - {meta['TSTOP']:.1f} IJD")
158
+ >>> print(f"GTIs: {len(gtis)} intervals")
159
+
160
+ >>> # Check energy range
161
+ >>> print(f"Energy: {events['ISGRI_ENERGY'].min():.1f} - "
162
+ ... f"{events['ISGRI_ENERGY'].max():.1f} keV")
163
+
164
+ See Also
165
+ --------
166
+ load_isgri_pif : Apply detector response weighting
167
+ """
168
+ confirmed_path = verify_events_path(events_path)
169
+
170
+ with fits.open(confirmed_path) as hdu:
171
+ # Load events
172
+ events_data = hdu["ISGR-EVTS-ALL"].data
173
+ header = hdu["ISGR-EVTS-ALL"].header
174
+
175
+ # Extract metadata
176
+ metadata = {
177
+ "REVOL": header.get("REVOL"),
178
+ "SWID": header.get("SWID"),
179
+ "TSTART": header.get("TSTART"),
180
+ "TSTOP": header.get("TSTOP"),
181
+ "TELAPSE": header.get("TELAPSE"),
182
+ "OBT_TSTART": header.get("OBTSTART"),
183
+ "OBT_TSTOP": header.get("OBTEND"),
184
+ "RA_SCX": header.get("RA_SCX"),
185
+ "DEC_SCX": header.get("DEC_SCX"),
186
+ "RA_SCZ": header.get("RA_SCZ"),
187
+ "DEC_SCZ": header.get("DEC_SCZ"),
188
+ "NoEVTS": header.get("NAXIS2"),
189
+ }
190
+
191
+ # Load GTIs
192
+ try:
193
+ gti_data = hdu["IBIS-GNRL-GTI"].data
194
+ gtis = np.column_stack([gti_data["START"], gti_data["STOP"]])
195
+ except (KeyError, IndexError):
196
+ # No GTI extension - return empty GTI
197
+ # t_start = events_data["TIME"][0]
198
+ # t_stop = events_data["TIME"][-1]
199
+ # gtis = np.array([[t_start, t_stop]])
200
+ gtis = None
201
+
202
+ # Filter bad events (SELECT_FLAG != 0)
203
+ good_mask = events_data["SELECT_FLAG"] == 0
204
+ events = Table(events_data[good_mask])
205
+
206
+ return events, gtis, metadata
207
+
208
+
209
+ def default_pif_metadata() -> Dict:
210
+ """
211
+ Create default PIF metadata for cases without PIF file.
212
+
213
+ Used when no PIF weighting is applied. All modules are
214
+ considered active with unit weight.
215
+
216
+ Returns
217
+ -------
218
+ metadata : dict
219
+ Default PIF metadata with:
220
+ - SWID : None
221
+ - SRC_RA, SRC_DEC : None (no source position)
222
+ - Source_Name : None
223
+ - cod : None (no coding fraction)
224
+ - No_Modules : [True]*8 (all modules active)
225
+
226
+ Examples
227
+ --------
228
+ >>> meta = default_pif_metadata()
229
+ >>> print(meta['No_Modules'])
230
+ [True, True, True, True, True, True, True, True]
231
+ """
232
+ return {
233
+ "SWID": None,
234
+ "SRC_RA": None,
235
+ "SRC_DEC": None,
236
+ "Source_Name": None,
237
+ "cod": None,
238
+ "No_Modules": [True] * 8, # All modules active
239
+ }
240
+
241
+
242
+ def merge_metadata(events_metadata: Dict, pif_metadata: Dict) -> Dict:
243
+ """
244
+ Merge events and PIF metadata dictionaries.
245
+
246
+ PIF metadata takes precedence except for SWID, which is
247
+ preserved from events metadata.
248
+
249
+ Parameters
250
+ ----------
251
+ events_metadata : dict
252
+ Metadata from events file.
253
+ pif_metadata : dict
254
+ Metadata from PIF file.
255
+
256
+ Returns
257
+ -------
258
+ merged : dict
259
+ Combined metadata with PIF values overwriting events values
260
+ (except SWID).
261
+
262
+ Examples
263
+ --------
264
+ >>> events_meta = {'SWID': '023400100010', 'TSTART': 3000.0}
265
+ >>> pif_meta = {'SWID': '999999999999', 'SRC_RA': 83.63, 'SRC_DEC': 22.01}
266
+ >>> merged = merge_metadata(events_meta, pif_meta)
267
+ >>> print(merged['SWID']) # Preserved from events
268
+ 023400100010
269
+ >>> print(merged['SRC_RA']) # From PIF
270
+ 83.63
271
+ """
272
+ merged = events_metadata.copy()
273
+
274
+ for key, value in pif_metadata.items():
275
+ if key != "SWID": # Preserve SWID from events
276
+ merged[key] = value
277
+
278
+ return merged
279
+
280
+
281
+ def load_isgri_pif(
282
+ pif_path: Union[str, Path],
283
+ events: Table,
284
+ pif_threshold: float = 0.5,
285
+ pif_extension: int = -1,
286
+ ) -> Tuple[Table, NDArray[np.float64], Dict]:
287
+ """
288
+ Load PIF (Pixel Illumination Fraction) file and apply to events.
289
+
290
+ Filters events by PIF threshold and returns PIF weights for
291
+ detector response correction.
292
+
293
+ Parameters
294
+ ----------
295
+ pif_path : str or Path
296
+ Path to PIF FITS file.
297
+ events : Table
298
+ Events table from load_isgri_events() with DETZ, DETY columns.
299
+ pif_threshold : float, default 0.5
300
+ Minimum PIF value to keep event (0.0-1.0).
301
+ Higher values = only well-illuminated pixels.
302
+ pif_extension : int, default -1
303
+ FITS extension index containing PIF data.
304
+ -1 = last extension (typical).
305
+
306
+ Returns
307
+ -------
308
+ filtered_events : Table
309
+ Events with PIF >= threshold.
310
+ pif_weights : ndarray
311
+ PIF value for each filtered event (for response correction).
312
+ pif_metadata : dict
313
+ Metadata with keys:
314
+ - SWID : Science Window ID
315
+ - Source_ID, Source_Name : Source identifiers
316
+ - SRC_RA, SRC_DEC : Source position (degrees)
317
+ - cod : Coding fraction (0.0-1.0)
318
+ - No_Modules : Array of active module flags
319
+
320
+ Raises
321
+ ------
322
+ ValueError
323
+ If PIF file has invalid shape (must be 134×130).
324
+ FileNotFoundError
325
+ If PIF file not found.
326
+
327
+ Examples
328
+ --------
329
+ >>> events, gtis, meta = load_isgri_events("events.fits")
330
+ >>> filtered, weights, pif_meta = load_isgri_pif(
331
+ ... "pif_model.fits",
332
+ ... events,
333
+ ... pif_threshold=0.5
334
+ ... )
335
+ >>>
336
+ >>> print(f"Filtered: {len(filtered)}/{len(events)} events")
337
+ >>> print(f"Coding fraction: {pif_meta['cod']:.2%}")
338
+ >>> print(f"Active modules: {np.sum(pif_meta['No_Modules'])}/8")
339
+
340
+ >>> # Use for light curve with response correction
341
+ >>> from isgri.utils import LightCurve
342
+ >>> lc = LightCurve(
343
+ ... time=filtered['TIME'],
344
+ ... energies=filtered['ISGRI_ENERGY'],
345
+ ... dety=filtered['DETY'],
346
+ ... detz=filtered['DETZ'],
347
+ ... weights=weights,
348
+ ... gtis=gtis,
349
+ ... metadata={**meta, **pif_meta}
350
+ ... )
351
+
352
+ See Also
353
+ --------
354
+ load_isgri_events : Load event data
355
+ apply_pif_mask : Apply PIF filtering
356
+ coding_fraction : Calculate coded fraction
357
+ """
358
+ if not (0 <= pif_threshold <= 1):
359
+ raise ValueError(f"pif_threshold must be in [0, 1], got {pif_threshold}")
360
+
361
+ pif_path = Path(pif_path)
362
+ if not pif_path.exists():
363
+ raise FileNotFoundError(f"PIF file not found: {pif_path}")
364
+
365
+ # Load PIF file
366
+ with fits.open(pif_path) as hdu:
367
+ pif_file = np.array(hdu[pif_extension].data)
368
+ header = hdu[pif_extension].header
369
+
370
+ # Validate shape
371
+ if pif_file.shape != (134, 130):
372
+ raise ValueError(
373
+ f"Invalid PIF file shape: expected (134, 130), got {pif_file.shape}. " "File may be empty or corrupted."
374
+ )
375
+
376
+ # Extract metadata
377
+ pif_metadata = {
378
+ "SWID": header.get("SWID"),
379
+ "Source_ID": header.get("SOURCEID"),
380
+ "Source_Name": header.get("NAME"),
381
+ "SRC_RA": header.get("RA_OBJ"),
382
+ "SRC_DEC": header.get("DEC_OBJ"),
383
+ }
384
+
385
+ # Compute quality metrics
386
+ pif_metadata["cod"] = coding_fraction(pif_file, events)
387
+ pif_metadata["No_Modules"] = estimate_active_modules(pif_file)
388
+
389
+ # Apply PIF mask
390
+ filtered_events, pif_weights = apply_pif_mask(pif_file, events, pif_threshold)
391
+
392
+ return filtered_events, pif_weights, pif_metadata