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.
@@ -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 .pif import apply_pif_mask, coding_fraction, estimate_active_modules
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
- Verifies and resolves the events file path.
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
- Args:
12
- path (str): File path or directory path containing events file.
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
- Returns:
15
- str: Resolved path to valid events file.
72
+ Examples
73
+ --------
74
+ >>> # Direct file path
75
+ >>> path = verify_events_path("isgri_events.fits")
16
76
 
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.
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
- if os.path.isfile(path):
23
- resolved_path = path
24
- elif os.path.isdir(path):
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 the provided directory.")
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 the provided directory: {path}.",
31
- "\nPlease specify the exact file paths.",
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 = os.path.join(path, candidate_files[0])
100
+ resolved_path = str(path / candidate_files[0])
35
101
  else:
36
102
  raise FileNotFoundError(f"Path does not exist: {path}")
37
103
 
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}")
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
- Loads ISGRI events from FITS file.
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
- Args:
49
- events_path (str): Path to events file or directory.
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
- 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.)
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
- events = np.array(hdu["ISGR-EVTS-ALL"].data)
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
- 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
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
- Creates default PIF metadata dictionary for cases without PIF file.
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
- Returns:
89
- dict: Default PIF metadata with all 8 modules active, no source info.
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": [1] * 8,
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
- Merges events and PIF metadata dictionaries.
241
+ Merge events and PIF metadata dictionaries.
104
242
 
105
- Args:
106
- events_metadata (dict): Metadata from events file.
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
- Returns:
110
- dict: Combined metadata (PIF metadata overwrites events metadata except SWID).
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
- 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
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
- def load_isgri_pif(pif_path, events, pif_threshold=0.5, pif_extension=-1):
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
- 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.
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(f"Invalid PIF file shape: expected (134, 130), got {pif_file.shape}")
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
- metadata_pif = {
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
- piffed_events, pif = apply_pif_mask(pif_file, events, pif_threshold)
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 piffed_events, pif, metadata_pif
389
+ return filtered_events, pif_weights, pif_metadata