isgri 0.4.0__py3-none-any.whl → 0.5.1__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 -3
- isgri/catalog/builder.py +90 -0
- isgri/catalog/scwquery.py +524 -517
- isgri/catalog/wcs.py +190 -190
- isgri/cli.py +224 -0
- isgri/config.py +151 -0
- isgri/utils/file_loaders.py +392 -389
- isgri/utils/lightcurve.py +409 -409
- isgri/utils/pif.py +286 -286
- isgri/utils/quality.py +389 -389
- isgri/utils/time_conversion.py +210 -210
- {isgri-0.4.0.dist-info → isgri-0.5.1.dist-info}/METADATA +68 -11
- isgri-0.5.1.dist-info/RECORD +19 -0
- {isgri-0.4.0.dist-info → isgri-0.5.1.dist-info}/WHEEL +1 -1
- isgri-0.5.1.dist-info/entry_points.txt +2 -0
- isgri-0.4.0.dist-info/RECORD +0 -14
- {isgri-0.4.0.dist-info → isgri-0.5.1.dist-info}/licenses/LICENSE +0 -0
isgri/catalog/wcs.py
CHANGED
|
@@ -1,190 +1,190 @@
|
|
|
1
|
-
"""
|
|
2
|
-
WCS coordinate transformations for celestial coordinates.
|
|
3
|
-
|
|
4
|
-
Implements spherical coordinate rotations following Calabretta & Greisen (2002),
|
|
5
|
-
"Representations of celestial coordinates in FITS", A&A 395, 1077-1122.
|
|
6
|
-
https://doi.org/10.1051/0004-6361:20021327
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from typing import Union
|
|
10
|
-
import numpy.typing as npt
|
|
11
|
-
import numpy as np
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def spherical_to_cartesian(lon, lat):
|
|
15
|
-
"""
|
|
16
|
-
Convert spherical coordinates to Cartesian unit vectors.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
lon: Longitude in degrees
|
|
20
|
-
lat: Latitude in degrees
|
|
21
|
-
|
|
22
|
-
Returns:
|
|
23
|
-
tuple: (x, y, z) Cartesian coordinates on unit sphere
|
|
24
|
-
"""
|
|
25
|
-
lon_rad = np.radians(lon)
|
|
26
|
-
lat_rad = np.radians(lat)
|
|
27
|
-
|
|
28
|
-
cos_lat = np.cos(lat_rad)
|
|
29
|
-
x = cos_lat * np.cos(lon_rad)
|
|
30
|
-
y = cos_lat * np.sin(lon_rad)
|
|
31
|
-
z = np.sin(lat_rad)
|
|
32
|
-
|
|
33
|
-
return x, y, z
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def cartesian_to_spherical(x, y, z):
|
|
37
|
-
"""
|
|
38
|
-
Convert Cartesian unit vectors to spherical coordinates.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
x, y, z: Cartesian coordinates
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
tuple: (lon, lat) in degrees
|
|
45
|
-
"""
|
|
46
|
-
# Clamp z to valid range for arcsin
|
|
47
|
-
z = np.clip(z, -1.0, 1.0)
|
|
48
|
-
|
|
49
|
-
lat = np.degrees(np.arcsin(z))
|
|
50
|
-
lon = np.degrees(np.arctan2(y, x))
|
|
51
|
-
|
|
52
|
-
return lon, lat
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def rotation_matrix(alpha_p, delta_p, phi_p=np.pi):
|
|
56
|
-
"""
|
|
57
|
-
Compute rotation matrix for coordinate transformation.
|
|
58
|
-
|
|
59
|
-
Following Calabretta & Greisen (2002), equations (5) and (7).
|
|
60
|
-
Assumes theta_0 = 90° (most common case).
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
alpha_p: Reference point RA in radians
|
|
64
|
-
delta_p: Reference point Dec in radians
|
|
65
|
-
phi_p: Native longitude of celestial pole (default: π for standard orientation)
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
ndarray: 3x3 rotation matrix
|
|
69
|
-
"""
|
|
70
|
-
sa = np.sin(alpha_p)
|
|
71
|
-
ca = np.cos(alpha_p)
|
|
72
|
-
sd = np.sin(delta_p)
|
|
73
|
-
cd = np.cos(delta_p)
|
|
74
|
-
sp = np.sin(phi_p)
|
|
75
|
-
cp = np.cos(phi_p)
|
|
76
|
-
|
|
77
|
-
# Rotation matrix from Calabretta & Greisen (2002), eq. (5)
|
|
78
|
-
R = np.array(
|
|
79
|
-
[
|
|
80
|
-
[-sa * sp - ca * cp * sd, ca * sp - sa * cp * sd, cp * cd],
|
|
81
|
-
[sa * cp - ca * sp * sd, -ca * cp - sa * sp * sd, sp * cd],
|
|
82
|
-
[ca * cd, sa * cd, sd],
|
|
83
|
-
]
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
return R
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def celestial_to_native(lon, lat, crval, longpole=180.0):
|
|
90
|
-
"""
|
|
91
|
-
Transform from celestial (RA/Dec) to native spherical coordinates.
|
|
92
|
-
|
|
93
|
-
Args:
|
|
94
|
-
lon: Celestial longitude (RA) in degrees
|
|
95
|
-
lat: Celestial latitude (Dec) in degrees
|
|
96
|
-
crval: Reference point [RA, Dec] in degrees
|
|
97
|
-
longpole: Native longitude of celestial north pole (default: 180°)
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
tuple: (phi, theta) native coordinates in degrees
|
|
101
|
-
"""
|
|
102
|
-
alpha_p = np.radians(crval[0])
|
|
103
|
-
delta_p = np.radians(crval[1])
|
|
104
|
-
phi_p = np.radians(longpole)
|
|
105
|
-
|
|
106
|
-
x, y, z = spherical_to_cartesian(lon, lat)
|
|
107
|
-
|
|
108
|
-
R = rotation_matrix(alpha_p, delta_p, phi_p)
|
|
109
|
-
x_rot = R[0, 0] * x + R[0, 1] * y + R[0, 2] * z
|
|
110
|
-
y_rot = R[1, 0] * x + R[1, 1] * y + R[1, 2] * z
|
|
111
|
-
z_rot = R[2, 0] * x + R[2, 1] * y + R[2, 2] * z
|
|
112
|
-
|
|
113
|
-
phi, theta = cartesian_to_spherical(x_rot, y_rot, z_rot)
|
|
114
|
-
|
|
115
|
-
return phi, theta
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def native_to_celestial(phi, theta, crval, longpole=180.0):
|
|
119
|
-
"""
|
|
120
|
-
Transform from native spherical to celestial (RA/Dec) coordinates.
|
|
121
|
-
|
|
122
|
-
Args:
|
|
123
|
-
phi: Native longitude in degrees
|
|
124
|
-
theta: Native latitude in degrees
|
|
125
|
-
crval: Reference point [RA, Dec] in degrees
|
|
126
|
-
longpole: Native longitude of celestial north pole (default: 180°)
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
tuple: (lon, lat) celestial coordinates in degrees
|
|
130
|
-
"""
|
|
131
|
-
alpha_p = np.radians(crval[0])
|
|
132
|
-
delta_p = np.radians(crval[1])
|
|
133
|
-
phi_p = np.radians(longpole)
|
|
134
|
-
|
|
135
|
-
x, y, z = spherical_to_cartesian(phi, theta)
|
|
136
|
-
|
|
137
|
-
R = rotation_matrix(alpha_p, delta_p, phi_p).T # Transpose for inverse rotation
|
|
138
|
-
x_rot = R[0, 0] * x + R[0, 1] * y + R[0, 2] * z
|
|
139
|
-
y_rot = R[1, 0] * x + R[1, 1] * y + R[1, 2] * z
|
|
140
|
-
z_rot = R[2, 0] * x + R[2, 1] * y + R[2, 2] * z
|
|
141
|
-
|
|
142
|
-
lon, lat = cartesian_to_spherical(x_rot, y_rot, z_rot)
|
|
143
|
-
|
|
144
|
-
return lon, lat
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def compute_detector_offset(
|
|
148
|
-
src_ra: Union[float, npt.ArrayLike],
|
|
149
|
-
src_dec: Union[float, npt.ArrayLike],
|
|
150
|
-
pointing_ra: float,
|
|
151
|
-
pointing_dec: float,
|
|
152
|
-
z_ra: float,
|
|
153
|
-
z_dec: float,
|
|
154
|
-
) -> tuple[Union[float, np.ndarray], Union[float, np.ndarray]]:
|
|
155
|
-
"""
|
|
156
|
-
Compute source offset in INTEGRAL detector coordinates.
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
src_ra: Source RA in degrees
|
|
160
|
-
src_dec: Source Dec in degrees
|
|
161
|
-
pointing_ra: Pointing axis RA in degrees
|
|
162
|
-
pointing_dec: Pointing axis Dec in degrees
|
|
163
|
-
z_ra: Z-axis RA in degrees
|
|
164
|
-
z_dec: Z-axis Dec in degrees
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
tuple: (y_offset, z_offset) in degrees (absolute values)
|
|
168
|
-
"""
|
|
169
|
-
# Transform Z-axis to native coordinates to get roll angle
|
|
170
|
-
scZ_phi, _ = celestial_to_native(z_ra, z_dec, [pointing_ra, pointing_dec])
|
|
171
|
-
roll = scZ_phi - 180.0
|
|
172
|
-
|
|
173
|
-
# Transform source to native coordinates
|
|
174
|
-
phi, theta = celestial_to_native(src_ra, src_dec, [pointing_ra, pointing_dec])
|
|
175
|
-
|
|
176
|
-
# Convert to detector coordinates
|
|
177
|
-
# theta is elevation from pointing axis
|
|
178
|
-
theta = 90.0 - theta
|
|
179
|
-
|
|
180
|
-
# phi is azimuth, correct for roll
|
|
181
|
-
phi = phi + 90.0 - roll
|
|
182
|
-
|
|
183
|
-
# Project onto detector Y and Z axes
|
|
184
|
-
theta_rad = np.radians(theta)
|
|
185
|
-
phi_rad = np.radians(phi)
|
|
186
|
-
|
|
187
|
-
y = np.degrees(np.arctan(np.tan(theta_rad) * np.cos(phi_rad)))
|
|
188
|
-
z = np.degrees(np.arctan(np.tan(theta_rad) * np.sin(phi_rad)))
|
|
189
|
-
|
|
190
|
-
return np.abs(y), np.abs(z)
|
|
1
|
+
"""
|
|
2
|
+
WCS coordinate transformations for celestial coordinates.
|
|
3
|
+
|
|
4
|
+
Implements spherical coordinate rotations following Calabretta & Greisen (2002),
|
|
5
|
+
"Representations of celestial coordinates in FITS", A&A 395, 1077-1122.
|
|
6
|
+
https://doi.org/10.1051/0004-6361:20021327
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Union
|
|
10
|
+
import numpy.typing as npt
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def spherical_to_cartesian(lon, lat):
|
|
15
|
+
"""
|
|
16
|
+
Convert spherical coordinates to Cartesian unit vectors.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
lon: Longitude in degrees
|
|
20
|
+
lat: Latitude in degrees
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
tuple: (x, y, z) Cartesian coordinates on unit sphere
|
|
24
|
+
"""
|
|
25
|
+
lon_rad = np.radians(lon)
|
|
26
|
+
lat_rad = np.radians(lat)
|
|
27
|
+
|
|
28
|
+
cos_lat = np.cos(lat_rad)
|
|
29
|
+
x = cos_lat * np.cos(lon_rad)
|
|
30
|
+
y = cos_lat * np.sin(lon_rad)
|
|
31
|
+
z = np.sin(lat_rad)
|
|
32
|
+
|
|
33
|
+
return x, y, z
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cartesian_to_spherical(x, y, z):
|
|
37
|
+
"""
|
|
38
|
+
Convert Cartesian unit vectors to spherical coordinates.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
x, y, z: Cartesian coordinates
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
tuple: (lon, lat) in degrees
|
|
45
|
+
"""
|
|
46
|
+
# Clamp z to valid range for arcsin
|
|
47
|
+
z = np.clip(z, -1.0, 1.0)
|
|
48
|
+
|
|
49
|
+
lat = np.degrees(np.arcsin(z))
|
|
50
|
+
lon = np.degrees(np.arctan2(y, x))
|
|
51
|
+
|
|
52
|
+
return lon, lat
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def rotation_matrix(alpha_p, delta_p, phi_p=np.pi):
|
|
56
|
+
"""
|
|
57
|
+
Compute rotation matrix for coordinate transformation.
|
|
58
|
+
|
|
59
|
+
Following Calabretta & Greisen (2002), equations (5) and (7).
|
|
60
|
+
Assumes theta_0 = 90° (most common case).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
alpha_p: Reference point RA in radians
|
|
64
|
+
delta_p: Reference point Dec in radians
|
|
65
|
+
phi_p: Native longitude of celestial pole (default: π for standard orientation)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ndarray: 3x3 rotation matrix
|
|
69
|
+
"""
|
|
70
|
+
sa = np.sin(alpha_p)
|
|
71
|
+
ca = np.cos(alpha_p)
|
|
72
|
+
sd = np.sin(delta_p)
|
|
73
|
+
cd = np.cos(delta_p)
|
|
74
|
+
sp = np.sin(phi_p)
|
|
75
|
+
cp = np.cos(phi_p)
|
|
76
|
+
|
|
77
|
+
# Rotation matrix from Calabretta & Greisen (2002), eq. (5)
|
|
78
|
+
R = np.array(
|
|
79
|
+
[
|
|
80
|
+
[-sa * sp - ca * cp * sd, ca * sp - sa * cp * sd, cp * cd],
|
|
81
|
+
[sa * cp - ca * sp * sd, -ca * cp - sa * sp * sd, sp * cd],
|
|
82
|
+
[ca * cd, sa * cd, sd],
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return R
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def celestial_to_native(lon, lat, crval, longpole=180.0):
|
|
90
|
+
"""
|
|
91
|
+
Transform from celestial (RA/Dec) to native spherical coordinates.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
lon: Celestial longitude (RA) in degrees
|
|
95
|
+
lat: Celestial latitude (Dec) in degrees
|
|
96
|
+
crval: Reference point [RA, Dec] in degrees
|
|
97
|
+
longpole: Native longitude of celestial north pole (default: 180°)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
tuple: (phi, theta) native coordinates in degrees
|
|
101
|
+
"""
|
|
102
|
+
alpha_p = np.radians(crval[0])
|
|
103
|
+
delta_p = np.radians(crval[1])
|
|
104
|
+
phi_p = np.radians(longpole)
|
|
105
|
+
|
|
106
|
+
x, y, z = spherical_to_cartesian(lon, lat)
|
|
107
|
+
|
|
108
|
+
R = rotation_matrix(alpha_p, delta_p, phi_p)
|
|
109
|
+
x_rot = R[0, 0] * x + R[0, 1] * y + R[0, 2] * z
|
|
110
|
+
y_rot = R[1, 0] * x + R[1, 1] * y + R[1, 2] * z
|
|
111
|
+
z_rot = R[2, 0] * x + R[2, 1] * y + R[2, 2] * z
|
|
112
|
+
|
|
113
|
+
phi, theta = cartesian_to_spherical(x_rot, y_rot, z_rot)
|
|
114
|
+
|
|
115
|
+
return phi, theta
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def native_to_celestial(phi, theta, crval, longpole=180.0):
|
|
119
|
+
"""
|
|
120
|
+
Transform from native spherical to celestial (RA/Dec) coordinates.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
phi: Native longitude in degrees
|
|
124
|
+
theta: Native latitude in degrees
|
|
125
|
+
crval: Reference point [RA, Dec] in degrees
|
|
126
|
+
longpole: Native longitude of celestial north pole (default: 180°)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
tuple: (lon, lat) celestial coordinates in degrees
|
|
130
|
+
"""
|
|
131
|
+
alpha_p = np.radians(crval[0])
|
|
132
|
+
delta_p = np.radians(crval[1])
|
|
133
|
+
phi_p = np.radians(longpole)
|
|
134
|
+
|
|
135
|
+
x, y, z = spherical_to_cartesian(phi, theta)
|
|
136
|
+
|
|
137
|
+
R = rotation_matrix(alpha_p, delta_p, phi_p).T # Transpose for inverse rotation
|
|
138
|
+
x_rot = R[0, 0] * x + R[0, 1] * y + R[0, 2] * z
|
|
139
|
+
y_rot = R[1, 0] * x + R[1, 1] * y + R[1, 2] * z
|
|
140
|
+
z_rot = R[2, 0] * x + R[2, 1] * y + R[2, 2] * z
|
|
141
|
+
|
|
142
|
+
lon, lat = cartesian_to_spherical(x_rot, y_rot, z_rot)
|
|
143
|
+
|
|
144
|
+
return lon, lat
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def compute_detector_offset(
|
|
148
|
+
src_ra: Union[float, npt.ArrayLike],
|
|
149
|
+
src_dec: Union[float, npt.ArrayLike],
|
|
150
|
+
pointing_ra: float,
|
|
151
|
+
pointing_dec: float,
|
|
152
|
+
z_ra: float,
|
|
153
|
+
z_dec: float,
|
|
154
|
+
) -> tuple[Union[float, np.ndarray], Union[float, np.ndarray]]:
|
|
155
|
+
"""
|
|
156
|
+
Compute source offset in INTEGRAL detector coordinates.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
src_ra: Source RA in degrees
|
|
160
|
+
src_dec: Source Dec in degrees
|
|
161
|
+
pointing_ra: Pointing axis RA in degrees
|
|
162
|
+
pointing_dec: Pointing axis Dec in degrees
|
|
163
|
+
z_ra: Z-axis RA in degrees
|
|
164
|
+
z_dec: Z-axis Dec in degrees
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
tuple: (y_offset, z_offset) in degrees (absolute values)
|
|
168
|
+
"""
|
|
169
|
+
# Transform Z-axis to native coordinates to get roll angle
|
|
170
|
+
scZ_phi, _ = celestial_to_native(z_ra, z_dec, [pointing_ra, pointing_dec])
|
|
171
|
+
roll = scZ_phi - 180.0
|
|
172
|
+
|
|
173
|
+
# Transform source to native coordinates
|
|
174
|
+
phi, theta = celestial_to_native(src_ra, src_dec, [pointing_ra, pointing_dec])
|
|
175
|
+
|
|
176
|
+
# Convert to detector coordinates
|
|
177
|
+
# theta is elevation from pointing axis
|
|
178
|
+
theta = 90.0 - theta
|
|
179
|
+
|
|
180
|
+
# phi is azimuth, correct for roll
|
|
181
|
+
phi = phi + 90.0 - roll
|
|
182
|
+
|
|
183
|
+
# Project onto detector Y and Z axes
|
|
184
|
+
theta_rad = np.radians(theta)
|
|
185
|
+
phi_rad = np.radians(phi)
|
|
186
|
+
|
|
187
|
+
y = np.degrees(np.arctan(np.tan(theta_rad) * np.cos(phi_rad)))
|
|
188
|
+
z = np.degrees(np.arctan(np.tan(theta_rad) * np.sin(phi_rad)))
|
|
189
|
+
|
|
190
|
+
return np.abs(y), np.abs(z)
|
isgri/cli.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from .catalog import ScwQuery
|
|
4
|
+
from .__version__ import __version__
|
|
5
|
+
from .config import Config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
@click.version_option(version=__version__)
|
|
10
|
+
def main():
|
|
11
|
+
"""ISGRI - INTEGRAL/ISGRI data analysis toolkit."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_time(time_str):
|
|
16
|
+
"""
|
|
17
|
+
Parse time string as IJD float or ISO date string.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
time_str : str or None
|
|
22
|
+
Time as "YYYY-MM-DD" or IJD number
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
float or str or None
|
|
27
|
+
Parsed time value
|
|
28
|
+
"""
|
|
29
|
+
if time_str is None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
return float(time_str)
|
|
34
|
+
except ValueError:
|
|
35
|
+
return time_str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@main.command()
|
|
39
|
+
@click.option("--catalog", type=click.Path(), help="Path to catalog FITS file. If not provided, uses config value.")
|
|
40
|
+
@click.option("--tstart", help="Start time (YYYY-MM-DD or IJD)")
|
|
41
|
+
@click.option("--tstop", help="Stop time (YYYY-MM-DD or IJD)")
|
|
42
|
+
@click.option("--ra", type=float, help="Right ascension (degrees)")
|
|
43
|
+
@click.option("--dec", type=float, help="Declination (degrees)")
|
|
44
|
+
@click.option("--fov", type=click.Choice(["full", "any"]), default="any", help="Field of view mode")
|
|
45
|
+
@click.option("--max-chi", type=float, help="Maximum chi-squared value")
|
|
46
|
+
@click.option("--chi-type", type=click.Choice(["RAW", "CUT", "GTI"]), default="CUT", help="Type of chi-squared value")
|
|
47
|
+
@click.option("--revolution", "-r", help="Revolution number")
|
|
48
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file (.fits or .csv)")
|
|
49
|
+
@click.option("--list-swids", is_flag=True, help="Only output SWID list")
|
|
50
|
+
@click.option("--count", is_flag=True, help="Only show count")
|
|
51
|
+
def query(catalog, tstart, tstop, ra, dec, fov, max_chi, chi_type, revolution, output, list_swids, count):
|
|
52
|
+
"""
|
|
53
|
+
Query INTEGRAL science window catalog.
|
|
54
|
+
|
|
55
|
+
If no catalog path is provided, uses the default from configuration.
|
|
56
|
+
Multiple filters can be combined.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
Query by time range (IJD):
|
|
60
|
+
|
|
61
|
+
isgri query --tstart 3000 --tstop 3100
|
|
62
|
+
|
|
63
|
+
Query by time range (ISO date):
|
|
64
|
+
|
|
65
|
+
isgri query --tstart 2010-01-01 --tstop 2010-12-31
|
|
66
|
+
|
|
67
|
+
Query by sky position:
|
|
68
|
+
|
|
69
|
+
isgri query --ra 83.63 --dec 22.01 --fov full
|
|
70
|
+
|
|
71
|
+
Query with quality cut:
|
|
72
|
+
|
|
73
|
+
isgri query --max-chi 2.0 --chi-type CUT
|
|
74
|
+
|
|
75
|
+
Save results to file:
|
|
76
|
+
|
|
77
|
+
isgri query --tstart 3000 --tstop 3100 --output results.fits
|
|
78
|
+
|
|
79
|
+
Get only SWID list:
|
|
80
|
+
|
|
81
|
+
isgri query --tstart 3000 --tstop 3100 --list-swids
|
|
82
|
+
|
|
83
|
+
Count matching science windows:
|
|
84
|
+
|
|
85
|
+
isgri query --ra 83.63 --dec 22.01 --count
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
# Load catalog
|
|
89
|
+
q = ScwQuery(catalog)
|
|
90
|
+
initial_count = len(q.catalog)
|
|
91
|
+
|
|
92
|
+
# Parse times (handle both IJD and ISO)
|
|
93
|
+
tstart = parse_time(tstart)
|
|
94
|
+
tstop = parse_time(tstop)
|
|
95
|
+
|
|
96
|
+
# Apply filters
|
|
97
|
+
if tstart or tstop:
|
|
98
|
+
q = q.time(tstart=tstart, tstop=tstop)
|
|
99
|
+
|
|
100
|
+
if ra is not None and dec is not None:
|
|
101
|
+
q = q.position(ra=ra, dec=dec, fov_mode=fov)
|
|
102
|
+
|
|
103
|
+
if max_chi is not None:
|
|
104
|
+
q = q.quality(max_chi=max_chi, chi_type=chi_type)
|
|
105
|
+
|
|
106
|
+
if revolution:
|
|
107
|
+
q = q.revolution(revolution)
|
|
108
|
+
|
|
109
|
+
results = q.get()
|
|
110
|
+
|
|
111
|
+
if count:
|
|
112
|
+
click.echo(len(results))
|
|
113
|
+
|
|
114
|
+
elif list_swids:
|
|
115
|
+
for swid in results["SWID"]:
|
|
116
|
+
click.echo(swid)
|
|
117
|
+
|
|
118
|
+
elif output:
|
|
119
|
+
if output.endswith(".csv"):
|
|
120
|
+
results.write(output, format="ascii.csv", overwrite=True)
|
|
121
|
+
else:
|
|
122
|
+
results.write(output, format="fits", overwrite=True)
|
|
123
|
+
click.echo(f"Saved {len(results)} SCWs to {output}")
|
|
124
|
+
|
|
125
|
+
else:
|
|
126
|
+
click.echo(f"Found {len(results)}/{initial_count} SCWs")
|
|
127
|
+
if len(results) > 0:
|
|
128
|
+
display_cols = ["SWID", "TSTART", "TSTOP", "RA_SCX", "DEC_SCX"]
|
|
129
|
+
chi_col = f"{chi_type}_CHI" if chi_type != "RAW" else "CHI"
|
|
130
|
+
if chi_col in results.colnames:
|
|
131
|
+
display_cols.append(chi_col)
|
|
132
|
+
click.echo(results[display_cols][:10])
|
|
133
|
+
if len(results) > 10:
|
|
134
|
+
click.echo(f"... and {len(results) - 10} more")
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
click.echo(f"Error: {e}", err=True)
|
|
138
|
+
raise click.Abort()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@main.command()
|
|
142
|
+
def config():
|
|
143
|
+
"""
|
|
144
|
+
Show current configuration.
|
|
145
|
+
|
|
146
|
+
Displays paths to config file, archive directory, and catalog file,
|
|
147
|
+
along with their existence status.
|
|
148
|
+
"""
|
|
149
|
+
cfg = Config()
|
|
150
|
+
|
|
151
|
+
click.echo(f"Config file: {cfg.path}")
|
|
152
|
+
click.echo(f" Exists: {cfg.path.exists()}")
|
|
153
|
+
click.echo()
|
|
154
|
+
|
|
155
|
+
archive = cfg.archive_path
|
|
156
|
+
click.echo(f"Archive path: {archive if archive else '(not set)'}")
|
|
157
|
+
if archive:
|
|
158
|
+
click.echo(f" Exists: {archive.exists()}")
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
catalog = cfg.catalog_path
|
|
162
|
+
click.echo(f"Catalog path: {catalog if catalog else '(not set)'}")
|
|
163
|
+
if catalog:
|
|
164
|
+
click.echo(f" Exists: {catalog.exists()}")
|
|
165
|
+
except FileNotFoundError as e:
|
|
166
|
+
click.echo(f"Catalog path: (configured but file not found)")
|
|
167
|
+
click.echo(f" Error: {e}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@main.command()
|
|
171
|
+
@click.option("--archive", type=click.Path(), help="INTEGRAL archive directory path")
|
|
172
|
+
@click.option("--catalog", type=click.Path(), help="Catalog FITS file path")
|
|
173
|
+
def config_set(archive, catalog):
|
|
174
|
+
"""
|
|
175
|
+
Set configuration values.
|
|
176
|
+
|
|
177
|
+
Set default paths for archive directory and/or catalog file.
|
|
178
|
+
Paths are expanded (~ becomes home directory) and resolved to absolute paths.
|
|
179
|
+
Warns if path doesn't exist but allows setting anyway.
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
|
|
183
|
+
Set archive path:
|
|
184
|
+
|
|
185
|
+
isgri config-set --archive /anita/archivio/
|
|
186
|
+
|
|
187
|
+
Set catalog path:
|
|
188
|
+
|
|
189
|
+
isgri config-set --catalog ~/data/scw_catalog.fits
|
|
190
|
+
|
|
191
|
+
Set both at once:
|
|
192
|
+
|
|
193
|
+
isgri config-set --archive /anita/archivio/ --catalog ~/data/scw_catalog.fits
|
|
194
|
+
"""
|
|
195
|
+
if not archive and not catalog:
|
|
196
|
+
click.echo("Error: Specify at least one option (--archive or --catalog)", err=True)
|
|
197
|
+
raise click.Abort()
|
|
198
|
+
|
|
199
|
+
cfg = Config()
|
|
200
|
+
|
|
201
|
+
if archive:
|
|
202
|
+
archive_path = Path(archive).expanduser().resolve()
|
|
203
|
+
if not archive_path.exists():
|
|
204
|
+
click.echo(f"Warning: Archive path does not exist: {archive_path}", err=True)
|
|
205
|
+
if not click.confirm("Set anyway?"):
|
|
206
|
+
raise click.Abort()
|
|
207
|
+
cfg.set(archive_path=archive_path)
|
|
208
|
+
click.echo(f"✓ Archive path set to: {archive_path}")
|
|
209
|
+
|
|
210
|
+
if catalog:
|
|
211
|
+
catalog_path = Path(catalog).expanduser().resolve()
|
|
212
|
+
if not catalog_path.exists():
|
|
213
|
+
click.echo(f"Warning: Catalog file does not exist: {catalog_path}", err=True)
|
|
214
|
+
if not click.confirm("Set anyway?"):
|
|
215
|
+
raise click.Abort()
|
|
216
|
+
cfg.set(catalog_path=catalog_path)
|
|
217
|
+
click.echo(f"✓ Catalog path set to: {catalog_path}")
|
|
218
|
+
|
|
219
|
+
click.echo()
|
|
220
|
+
click.echo(f"Configuration saved to: {cfg.path}")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if __name__ == "__main__":
|
|
224
|
+
main()
|