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