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/__init__.py +1 -0
- isgri/__version__.py +1 -0
- isgri/catalog/__init__.py +3 -0
- isgri/catalog/builder.py +90 -0
- isgri/catalog/scwquery.py +524 -0
- isgri/catalog/wcs.py +190 -0
- isgri/cli.py +224 -0
- isgri/config.py +151 -0
- isgri/utils/file_loaders.py +392 -159
- isgri/utils/lightcurve.py +409 -265
- isgri/utils/pif.py +286 -41
- isgri/utils/quality.py +389 -182
- isgri/utils/time_conversion.py +210 -39
- isgri-0.5.0.dist-info/METADATA +164 -0
- isgri-0.5.0.dist-info/RECORD +19 -0
- isgri-0.5.0.dist-info/entry_points.txt +2 -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.5.0.dist-info}/WHEEL +0 -0
- {isgri-0.3.0.dist-info → isgri-0.5.0.dist-info}/licenses/LICENSE +0 -0
isgri/utils/pif.py
CHANGED
|
@@ -1,41 +1,286 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
"""
|
|
2
|
+
ISGRI Detector Pixel Illumination Fraction (PIF) Tools
|
|
3
|
+
========================================================
|
|
4
|
+
|
|
5
|
+
Functions for working with ISGRI detector response maps (PIF files).
|
|
6
|
+
PIF values indicate what fraction of source flux reaches each detector pixel,
|
|
7
|
+
accounting for shadowing by the coded mask.
|
|
8
|
+
|
|
9
|
+
PIF values range from 0 (fully shadowed) to 1 (fully illuminated).
|
|
10
|
+
|
|
11
|
+
Functions
|
|
12
|
+
---------
|
|
13
|
+
select_isgri_module : Get detector coordinate bounds for a module
|
|
14
|
+
apply_pif_mask : Filter events by PIF threshold
|
|
15
|
+
coding_fraction : Calculate coded fraction for source position
|
|
16
|
+
estimate_active_modules : Determine which modules have significant PIF coverage
|
|
17
|
+
|
|
18
|
+
Examples
|
|
19
|
+
--------
|
|
20
|
+
>>> import numpy as np
|
|
21
|
+
>>> from astropy.table import Table
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Load PIF and events
|
|
24
|
+
>>> pif_file = np.random.rand(134, 130) # Mock PIF
|
|
25
|
+
>>> events = Table({'DETZ': [10, 20], 'DETY': [15, 25]})
|
|
26
|
+
>>>
|
|
27
|
+
>>> # Apply PIF weighting
|
|
28
|
+
>>> filtered_events, weights = apply_pif_mask(pif_file, events, pif_threshold=0.5)
|
|
29
|
+
>>>
|
|
30
|
+
>>> # Check which modules are active
|
|
31
|
+
>>> active = estimate_active_modules(pif_file)
|
|
32
|
+
>>> print(f"Active modules: {np.where(active)[0]}")
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
from numpy.typing import NDArray
|
|
37
|
+
from typing import Tuple
|
|
38
|
+
from astropy.table import Table
|
|
39
|
+
|
|
40
|
+
# ISGRI detector module boundaries (mm coordinates)
|
|
41
|
+
# 8 modules total: 4 rows × 2 columns
|
|
42
|
+
# Z-axis (rows): 4 modules
|
|
43
|
+
# Y-axis (cols): 2 modules
|
|
44
|
+
DETZ_BOUNDS = [0, 32, 66, 100, 134] # 5 boundaries for 4 rows
|
|
45
|
+
DETY_BOUNDS = [0, 64, 130] # 3 boundaries for 2 columns
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def select_isgri_module(module_no: int) -> Tuple[int, int, int, int]:
|
|
49
|
+
"""
|
|
50
|
+
Get detector coordinate bounds for specified module.
|
|
51
|
+
|
|
52
|
+
ISGRI has 8 modules arranged in 4 rows × 2 columns:
|
|
53
|
+
|
|
54
|
+
Module layout:
|
|
55
|
+
[0] [1]
|
|
56
|
+
[2] [3]
|
|
57
|
+
[4] [5]
|
|
58
|
+
[6] [7]
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
module_no : int
|
|
63
|
+
Module number (0-7)
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
z1, z2, y1, y2 : int
|
|
68
|
+
Detector coordinate bounds (DETZ min/max, DETY min/max)
|
|
69
|
+
|
|
70
|
+
Raises
|
|
71
|
+
------
|
|
72
|
+
ValueError
|
|
73
|
+
If module_no not in range [0, 7]
|
|
74
|
+
|
|
75
|
+
Examples
|
|
76
|
+
--------
|
|
77
|
+
>>> z1, z2, y1, y2 = select_isgri_module(0)
|
|
78
|
+
>>> print(f"Module 0: DETZ=[{z1},{z2}], DETY=[{y1},{y2}]")
|
|
79
|
+
Module 0: DETZ=[0,32], DETY=[0,64]
|
|
80
|
+
|
|
81
|
+
>>> # Module 3 is bottom-right
|
|
82
|
+
>>> select_isgri_module(3)
|
|
83
|
+
(66, 100, 64, 130)
|
|
84
|
+
"""
|
|
85
|
+
if not (0 <= module_no <= 7):
|
|
86
|
+
raise ValueError(f"module_no must be in [0, 7], got {module_no}")
|
|
87
|
+
|
|
88
|
+
col = module_no % 2 # 0=left, 1=right
|
|
89
|
+
row = module_no // 2 # 0-3 from top to bottom
|
|
90
|
+
|
|
91
|
+
z1, z2 = DETZ_BOUNDS[row], DETZ_BOUNDS[row + 1]
|
|
92
|
+
y1, y2 = DETY_BOUNDS[col], DETY_BOUNDS[col + 1]
|
|
93
|
+
|
|
94
|
+
return z1, z2, y1, y2
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def apply_pif_mask(
|
|
98
|
+
pif_file: NDArray[np.float64],
|
|
99
|
+
events: Table,
|
|
100
|
+
pif_threshold: float = 0.5,
|
|
101
|
+
) -> Tuple[Table, NDArray[np.float64]]:
|
|
102
|
+
"""
|
|
103
|
+
Filter events by PIF threshold and return PIF weights.
|
|
104
|
+
|
|
105
|
+
Events with PIF < threshold are removed. Remaining events are
|
|
106
|
+
weighted by their PIF values for response correction.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
pif_file : ndarray, shape (134, 130)
|
|
111
|
+
2D PIF array (DETZ x DETY coordinates)
|
|
112
|
+
events : Table
|
|
113
|
+
Event table with 'DETZ' and 'DETY' columns
|
|
114
|
+
pif_threshold : float, default 0.5
|
|
115
|
+
Minimum PIF value to keep event (0.0-1.0)
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
filtered_events : Table
|
|
120
|
+
Events with PIF >= threshold
|
|
121
|
+
pif_weights : ndarray
|
|
122
|
+
PIF value for each filtered event
|
|
123
|
+
|
|
124
|
+
Raises
|
|
125
|
+
------
|
|
126
|
+
ValueError
|
|
127
|
+
If pif_threshold not in [0, 1]
|
|
128
|
+
If events missing 'DETZ' or 'DETY' columns
|
|
129
|
+
If PIF dimensions don't match expected (134, 130)
|
|
130
|
+
|
|
131
|
+
Examples
|
|
132
|
+
--------
|
|
133
|
+
>>> pif = np.random.rand(134, 130)
|
|
134
|
+
>>> events = Table({'DETZ': [10, 20, 30], 'DETY': [15, 25, 35]})
|
|
135
|
+
>>>
|
|
136
|
+
>>> # Keep only well-illuminated events
|
|
137
|
+
>>> filtered, weights = apply_pif_mask(pif, events, pif_threshold=0.7)
|
|
138
|
+
>>> print(f"Kept {len(filtered)}/{len(events)} events")
|
|
139
|
+
>>> print(f"Mean weight: {weights.mean():.3f}")
|
|
140
|
+
"""
|
|
141
|
+
# Validate inputs
|
|
142
|
+
if not (0 <= pif_threshold <= 1):
|
|
143
|
+
raise ValueError(f"pif_threshold must be in [0, 1], got {pif_threshold}")
|
|
144
|
+
|
|
145
|
+
if pif_file.shape != (134, 130):
|
|
146
|
+
raise ValueError(f"PIF file must have shape (134, 130), got {pif_file.shape}")
|
|
147
|
+
|
|
148
|
+
if "DETZ" not in events.colnames or "DETY" not in events.colnames:
|
|
149
|
+
raise ValueError("Events table must have 'DETZ' and 'DETY' columns")
|
|
150
|
+
|
|
151
|
+
# Create mask for events above threshold
|
|
152
|
+
pif_filter = pif_file > pif_threshold
|
|
153
|
+
|
|
154
|
+
# Get PIF values at event positions
|
|
155
|
+
event_pif = pif_file[events["DETZ"], events["DETY"]]
|
|
156
|
+
|
|
157
|
+
# Apply filter
|
|
158
|
+
mask = event_pif > pif_threshold
|
|
159
|
+
filtered_events = events[mask]
|
|
160
|
+
pif_weights = event_pif[mask]
|
|
161
|
+
|
|
162
|
+
return filtered_events, pif_weights
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def coding_fraction(
|
|
166
|
+
pif_file: NDArray[np.float64],
|
|
167
|
+
events: Table,
|
|
168
|
+
) -> float:
|
|
169
|
+
"""
|
|
170
|
+
Calculate fraction of detector that is fully coded.
|
|
171
|
+
|
|
172
|
+
Uses events with PIF=1.0 (fully illuminated) to estimate
|
|
173
|
+
the size of the fully coded field of view.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
pif_file : ndarray, shape (134, 130)
|
|
178
|
+
2D PIF array
|
|
179
|
+
events : Table
|
|
180
|
+
Event table with 'DETZ' and 'DETY' columns
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
coding_fraction : float
|
|
185
|
+
Fraction of detector area that is fully coded (0.0-1.0)
|
|
186
|
+
|
|
187
|
+
Notes
|
|
188
|
+
-----
|
|
189
|
+
Fully coded region has PIF=1.0 for on-axis sources.
|
|
190
|
+
Partially coded region has 0 < PIF < 1.
|
|
191
|
+
|
|
192
|
+
Examples
|
|
193
|
+
--------
|
|
194
|
+
>>> pif = np.ones((134, 130))
|
|
195
|
+
>>> pif[50:80, 40:90] = 1.0 # Fully coded region
|
|
196
|
+
>>> events = Table({'DETZ': np.arange(134), 'DETY': np.arange(130)})
|
|
197
|
+
>>>
|
|
198
|
+
>>> frac = coding_fraction(pif, events)
|
|
199
|
+
>>> print(f"Coding fraction: {frac:.2%}")
|
|
200
|
+
"""
|
|
201
|
+
if pif_file.shape != (134, 130):
|
|
202
|
+
raise ValueError(f"PIF must have shape (134, 130), got {pif_file.shape}")
|
|
203
|
+
|
|
204
|
+
if "DETZ" not in events.colnames or "DETY" not in events.colnames:
|
|
205
|
+
raise ValueError("Events must have 'DETZ' and 'DETY' columns")
|
|
206
|
+
|
|
207
|
+
# Find fully coded pixels (PIF = 1.0)
|
|
208
|
+
fully_coded = pif_file == 1.0
|
|
209
|
+
|
|
210
|
+
# Get events in fully coded region
|
|
211
|
+
coded_events = events[fully_coded[events["DETZ"], events["DETY"]]]
|
|
212
|
+
|
|
213
|
+
if len(coded_events) == 0:
|
|
214
|
+
return 0.0
|
|
215
|
+
|
|
216
|
+
# Calculate extent in Y and Z
|
|
217
|
+
dety_range = np.max(coded_events["DETY"]) - np.min(coded_events["DETY"])
|
|
218
|
+
detz_range = np.max(coded_events["DETZ"]) - np.min(coded_events["DETZ"])
|
|
219
|
+
|
|
220
|
+
# Normalize by detector size (Y: 0-129, Z: 0-133)
|
|
221
|
+
frac_y = dety_range / 129.0
|
|
222
|
+
frac_z = detz_range / 133.0
|
|
223
|
+
|
|
224
|
+
# Area fraction
|
|
225
|
+
coding_frac = frac_y * frac_z
|
|
226
|
+
|
|
227
|
+
return coding_frac
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def estimate_active_modules(
|
|
231
|
+
pif_file: NDArray[np.float64],
|
|
232
|
+
threshold: float = 0.2,
|
|
233
|
+
) -> NDArray[np.bool_]:
|
|
234
|
+
"""
|
|
235
|
+
Determine which detector modules have significant PIF coverage.
|
|
236
|
+
|
|
237
|
+
A module is considered active if more than `threshold` fraction
|
|
238
|
+
of its pixels have PIF > 0.01.
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
pif_file : ndarray, shape (134, 130)
|
|
243
|
+
2D PIF array
|
|
244
|
+
threshold : float, default 0.2
|
|
245
|
+
Minimum fraction of illuminated pixels (0.0-1.0)
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
active_modules : ndarray of bool, shape (8,)
|
|
250
|
+
True if module is active, False otherwise
|
|
251
|
+
|
|
252
|
+
Examples
|
|
253
|
+
--------
|
|
254
|
+
>>> pif = np.random.rand(134, 130)
|
|
255
|
+
>>> pif[:50, :] = 0 # Top modules dark
|
|
256
|
+
>>>
|
|
257
|
+
>>> active = estimate_active_modules(pif, threshold=0.2)
|
|
258
|
+
>>> print(f"Active modules: {np.where(active)[0]}")
|
|
259
|
+
Active modules: [2 3 4 5 6 7]
|
|
260
|
+
|
|
261
|
+
>>> # Get list of active module numbers
|
|
262
|
+
>>> active_list = np.where(active)[0].tolist()
|
|
263
|
+
"""
|
|
264
|
+
if pif_file.shape != (134, 130):
|
|
265
|
+
raise ValueError(f"PIF must have shape (134, 130), got {pif_file.shape}")
|
|
266
|
+
|
|
267
|
+
if not (0 <= threshold <= 1):
|
|
268
|
+
raise ValueError(f"threshold must be in [0, 1], got {threshold}")
|
|
269
|
+
|
|
270
|
+
active_modules = np.zeros(8, dtype=bool)
|
|
271
|
+
|
|
272
|
+
for module_no in range(8):
|
|
273
|
+
z1, z2, y1, y2 = select_isgri_module(module_no)
|
|
274
|
+
|
|
275
|
+
# Get PIF values for this module
|
|
276
|
+
module_pif = pif_file[z1:z2, y1:y2].flatten()
|
|
277
|
+
|
|
278
|
+
# Count illuminated pixels (PIF > 0.01)
|
|
279
|
+
n_illuminated = np.sum(module_pif > 0.01)
|
|
280
|
+
n_total = len(module_pif)
|
|
281
|
+
|
|
282
|
+
# Check if fraction exceeds threshold
|
|
283
|
+
if n_illuminated / n_total > threshold:
|
|
284
|
+
active_modules[module_no] = True
|
|
285
|
+
|
|
286
|
+
return active_modules
|