splusdata 5.1__tar.gz → 5.2__tar.gz
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.
- {splusdata-5.1/splusdata.egg-info → splusdata-5.2}/PKG-INFO +2 -1
- {splusdata-5.1 → splusdata-5.2}/pyproject.toml +4 -2
- {splusdata-5.1 → splusdata-5.2}/splusdata/__init__.py +1 -1
- splusdata-5.2/splusdata/core.py +657 -0
- splusdata-5.2/splusdata/features/io.py +100 -0
- splusdata-5.2/splusdata/features/zeropoints/zp_image.py +131 -0
- splusdata-5.2/splusdata/features/zeropoints/zp_map.py +145 -0
- splusdata-5.2/splusdata/scripts/args.py +77 -0
- splusdata-5.2/splusdata/scubes/__init__.py +1 -0
- splusdata-5.2/splusdata/scubes/constants.py +8 -0
- splusdata-5.2/splusdata/scubes/core.py +327 -0
- splusdata-5.2/splusdata/variability/__init__.py +0 -0
- splusdata-5.2/splusdata/vars.py +47 -0
- {splusdata-5.1 → splusdata-5.2/splusdata.egg-info}/PKG-INFO +2 -1
- {splusdata-5.1 → splusdata-5.2}/splusdata.egg-info/SOURCES.txt +9 -1
- {splusdata-5.1 → splusdata-5.2}/splusdata.egg-info/entry_points.txt +1 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata.egg-info/requires.txt +1 -0
- splusdata-5.1/splusdata/core.py +0 -225
- splusdata-5.1/splusdata/vars.py +0 -2
- {splusdata-5.1 → splusdata-5.2}/LICENSE +0 -0
- {splusdata-5.1 → splusdata-5.2}/README.md +0 -0
- {splusdata-5.1 → splusdata-5.2}/setup.cfg +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/connect.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/features/__init__.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/features/extinction.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/features/filterbw.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/features/hipscat.py +0 -0
- {splusdata-5.1/splusdata/models → splusdata-5.2/splusdata/features/zeropoints}/__init__.py +0 -0
- /splusdata-5.1/splusdata/features/zeropoints.py → /splusdata-5.2/splusdata/features/zeropointsdr4.py +0 -0
- {splusdata-5.1/splusdata/variability → splusdata-5.2/splusdata/models}/__init__.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/models/star_gal_quasar.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/readconf.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/vacs/__init__.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/vacs/pdfs.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata/vacs/sqg.py +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata.egg-info/dependency_links.txt +0 -0
- {splusdata-5.1 → splusdata-5.2}/splusdata.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splusdata
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2
|
|
4
4
|
Summary: Download SPLUS catalogs, FITS and more
|
|
5
5
|
Author-email: Gustavo Schwarz <gustavo.b.schwarz@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -19,6 +19,7 @@ Requires-Dist: pandas
|
|
|
19
19
|
Requires-Dist: scipy
|
|
20
20
|
Requires-Dist: pillow
|
|
21
21
|
Requires-Dist: pyyaml
|
|
22
|
+
Requires-Dist: tqdm
|
|
22
23
|
Dynamic: license-file
|
|
23
24
|
|
|
24
25
|
## Source for pip package splusdata
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "splusdata"
|
|
7
|
-
version = "5.
|
|
7
|
+
version = "5.2"
|
|
8
8
|
description = "Download SPLUS catalogs, FITS and more"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Gustavo Schwarz", email = "gustavo.b.schwarz@gmail.com" }
|
|
@@ -26,11 +26,13 @@ dependencies = [
|
|
|
26
26
|
"pandas",
|
|
27
27
|
"scipy",
|
|
28
28
|
"pillow",
|
|
29
|
-
"pyyaml"
|
|
29
|
+
"pyyaml",
|
|
30
|
+
"tqdm",
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
[project.scripts]
|
|
33
34
|
splusdata = "splusdata.readconf:main"
|
|
35
|
+
scubes_dr6 = "splusdata.scubes.core:scubes"
|
|
34
36
|
|
|
35
37
|
[tool.setuptools.packages.find]
|
|
36
38
|
where = ["."]
|
|
@@ -4,7 +4,7 @@ from splusdata.features import filterbw
|
|
|
4
4
|
from splusdata.features.hipscat import get_hipscats
|
|
5
5
|
|
|
6
6
|
from splusdata.features.extinction import SplusExtinction
|
|
7
|
-
from splusdata.features.
|
|
7
|
+
from splusdata.features.zeropointsdr4 import get_zeropoint
|
|
8
8
|
|
|
9
9
|
import splusdata.vacs
|
|
10
10
|
|
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import adss
|
|
2
|
+
import getpass
|
|
3
|
+
|
|
4
|
+
from PIL import Image
|
|
5
|
+
from astropy.io import fits
|
|
6
|
+
import io
|
|
7
|
+
|
|
8
|
+
from splusdata.features.io import print_level
|
|
9
|
+
|
|
10
|
+
class SplusdataError(Exception):
|
|
11
|
+
"""Custom exception type for S-PLUS data errors raised by this helper module.
|
|
12
|
+
|
|
13
|
+
Use this to catch and handle issues such as:
|
|
14
|
+
- Missing collections or files on the server.
|
|
15
|
+
- Invalid filter/field combinations.
|
|
16
|
+
- Empty results (e.g., zero candidates for a filename pattern).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def open_image(image_bytes):
|
|
21
|
+
"""Open an image from raw bytes and return a PIL Image.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
image_bytes : bytes
|
|
26
|
+
Raw image bytes (e.g., returned by ADSS endpoints).
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
PIL.Image.Image
|
|
31
|
+
A Pillow image instance.
|
|
32
|
+
|
|
33
|
+
Raises
|
|
34
|
+
------
|
|
35
|
+
OSError
|
|
36
|
+
If Pillow cannot identify or open the image.
|
|
37
|
+
"""
|
|
38
|
+
from PIL import Image
|
|
39
|
+
im = Image.open(io.BytesIO(image_bytes))
|
|
40
|
+
return im
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def save_image(image_bytes, filename):
|
|
44
|
+
"""Save image bytes to a file on disk.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
image_bytes : bytes
|
|
49
|
+
Raw image bytes (e.g., returned by ADSS endpoints).
|
|
50
|
+
filename : str or pathlib.Path
|
|
51
|
+
Output file path, including the desired extension.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
None
|
|
56
|
+
|
|
57
|
+
Raises
|
|
58
|
+
------
|
|
59
|
+
OSError
|
|
60
|
+
If the image cannot be opened or saved.
|
|
61
|
+
"""
|
|
62
|
+
im = open_image(image_bytes)
|
|
63
|
+
im.save(filename)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# field frame
|
|
67
|
+
class Core:
|
|
68
|
+
"""Convenience interface around `adss.ADSSClient` for S-PLUS images and queries.
|
|
69
|
+
|
|
70
|
+
This wrapper streamlines common tasks:
|
|
71
|
+
- Listing available image collections.
|
|
72
|
+
- Fetching full field FITS frames or small cutouts (stamps).
|
|
73
|
+
- Generating Lupton or Trilogy RGB composites.
|
|
74
|
+
- Submitting SQL/ADQL queries (with optional table upload).
|
|
75
|
+
- Retrieving and applying per-field zero points (DR6).
|
|
76
|
+
|
|
77
|
+
Notes
|
|
78
|
+
-----
|
|
79
|
+
* Authentication: If `username`/`password` are not provided, the constructor
|
|
80
|
+
will prompt interactively (stdin).
|
|
81
|
+
* All methods pass through to a single `adss.ADSSClient` instance.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, username=None, password=None, SERVER_IP=f"https://splus.cloud", auto_renew=False, verbose=0):
|
|
85
|
+
"""Initialize a Core client.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
username : str, optional
|
|
90
|
+
S-PLUS account username. If None, asked interactively.
|
|
91
|
+
password : str, optional
|
|
92
|
+
S-PLUS account password. If None, prompted via getpass.
|
|
93
|
+
SERVER_IP : str, optional
|
|
94
|
+
Base URL of the S-PLUS service (default: "https://splus.cloud").
|
|
95
|
+
auto_renew : bool, optional
|
|
96
|
+
Placeholder for future token auto-renew behavior (unused here).
|
|
97
|
+
verbose : int, optional
|
|
98
|
+
Verbosity level. Defaults to 0.
|
|
99
|
+
|
|
100
|
+
Attributes
|
|
101
|
+
----------
|
|
102
|
+
client : adss.ADSSClient
|
|
103
|
+
Underlying authenticated ADSS client.
|
|
104
|
+
collections : list[dict]
|
|
105
|
+
Cached list of collections after `_load_collections()`.
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
Exception
|
|
110
|
+
Propagates any authentication/connection exceptions raised by ADSSClient.
|
|
111
|
+
"""
|
|
112
|
+
if not username:
|
|
113
|
+
username = input("splus.cloud username: ")
|
|
114
|
+
if not password:
|
|
115
|
+
password = getpass.getpass("splus.cloud password: ")
|
|
116
|
+
|
|
117
|
+
self.client = adss.ADSSClient(
|
|
118
|
+
SERVER_IP,
|
|
119
|
+
username=username,
|
|
120
|
+
password=password
|
|
121
|
+
)
|
|
122
|
+
self.collections = []
|
|
123
|
+
self.verbose = verbose
|
|
124
|
+
|
|
125
|
+
def _load_collections(self):
|
|
126
|
+
"""Fetch and cache image collections from the server.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
None
|
|
131
|
+
|
|
132
|
+
Side Effects
|
|
133
|
+
------------
|
|
134
|
+
Populates `self.collections` with a list of collection dicts, as returned by
|
|
135
|
+
`ADSSClient.get_image_collections()`.
|
|
136
|
+
"""
|
|
137
|
+
collections = self.client.get_image_collections()
|
|
138
|
+
self.collections = collections
|
|
139
|
+
|
|
140
|
+
def check_available_images_releases(self):
|
|
141
|
+
"""List available image collection names (data releases).
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
list[str]
|
|
146
|
+
Collection names, e.g., ["dr4", "dr5", "dr6", ...].
|
|
147
|
+
"""
|
|
148
|
+
collections = self.client.get_image_collections()
|
|
149
|
+
names = [col['name'] for col in collections]
|
|
150
|
+
return names
|
|
151
|
+
|
|
152
|
+
def get_collection_id_by_pattern(self, pattern):
|
|
153
|
+
"""Return the first collection whose `name` contains `pattern`.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
pattern : str
|
|
158
|
+
Substring to search for inside the collection name.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
dict
|
|
163
|
+
The first matching collection dictionary.
|
|
164
|
+
|
|
165
|
+
Raises
|
|
166
|
+
------
|
|
167
|
+
SplusdataError
|
|
168
|
+
If no collection name contains the given pattern.
|
|
169
|
+
"""
|
|
170
|
+
self._load_collections()
|
|
171
|
+
for col in self.collections:
|
|
172
|
+
if pattern in col['name']:
|
|
173
|
+
return col
|
|
174
|
+
raise SplusdataError("Collection not found")
|
|
175
|
+
|
|
176
|
+
def get_file_metadata(self, field, band, pattern = "", data_release = "dr4"):
|
|
177
|
+
"""Resolve a single file metadata entry matching field/band/pattern.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
field : str
|
|
182
|
+
Field identifier (e.g., "SPLUS-n01s10"). If not found, tries swapping
|
|
183
|
+
'-' and '_' once to be tolerant to naming variants.
|
|
184
|
+
band : str
|
|
185
|
+
Filter/band name (e.g., "R", "I", "F660", "U", ...).
|
|
186
|
+
pattern : str, optional
|
|
187
|
+
Key into the collection's `patterns` dict. Empty string selects the
|
|
188
|
+
default pattern list for full science images. "weight" commonly selects
|
|
189
|
+
weight maps.
|
|
190
|
+
data_release : str, optional
|
|
191
|
+
Collection pattern (substring) to select the DR (defaults to "dr4").
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
dict
|
|
196
|
+
A file entry suitable for `download_image()`, containing at least
|
|
197
|
+
`id`, `filename`, and `file_type`.
|
|
198
|
+
|
|
199
|
+
Selection Logic
|
|
200
|
+
---------------
|
|
201
|
+
1. Lists candidates via `list_image_files(collection_id, filter_str=field, filter_name=band)`.
|
|
202
|
+
2. If none, swaps '-' and '_' in `field` and retries once.
|
|
203
|
+
3. Reads the collection's `patterns[pattern]`, splits by commas, and filters:
|
|
204
|
+
- Tokens starting with '!' mean "exclude those containing token".
|
|
205
|
+
- Otherwise, "include if contains token".
|
|
206
|
+
4. If multiple remain, prefer those with `file_type == "fz"`.
|
|
207
|
+
|
|
208
|
+
Raises
|
|
209
|
+
------
|
|
210
|
+
SplusdataError
|
|
211
|
+
If no candidate files are found for the given (field, band).
|
|
212
|
+
KeyError
|
|
213
|
+
If `pattern` is not a key in the collection's `patterns` dict.
|
|
214
|
+
"""
|
|
215
|
+
collection = self.get_collection_id_by_pattern(data_release)
|
|
216
|
+
collection_id = collection['id']
|
|
217
|
+
|
|
218
|
+
candidates = self.client.list_image_files(collection_id, filter_str=field, filter_name=band)
|
|
219
|
+
|
|
220
|
+
if len(candidates) == 0 and ("-" in field or "_" in field):
|
|
221
|
+
field = field.replace("-", "_") if "-" in field else field.replace("_", "-")
|
|
222
|
+
candidates = self.client.list_image_files(collection_id, filter_str=field, filter_name=band)
|
|
223
|
+
if len(candidates) == 0:
|
|
224
|
+
raise SplusdataError(f"Field {field} not found in band {band}")
|
|
225
|
+
|
|
226
|
+
patterns = collection['patterns']
|
|
227
|
+
|
|
228
|
+
final_candidate = None
|
|
229
|
+
f_candidates = []
|
|
230
|
+
|
|
231
|
+
pattern = patterns[pattern]
|
|
232
|
+
|
|
233
|
+
pattern = pattern.split(",")
|
|
234
|
+
for c in candidates:
|
|
235
|
+
for p in pattern:
|
|
236
|
+
if p.startswith("!"):
|
|
237
|
+
if p not in c['filename']:
|
|
238
|
+
f_candidates.append(c)
|
|
239
|
+
else:
|
|
240
|
+
if p in c['filename']:
|
|
241
|
+
f_candidates.append(c)
|
|
242
|
+
|
|
243
|
+
if len(f_candidates) == 0:
|
|
244
|
+
final_candidate = candidates[0]
|
|
245
|
+
elif len(f_candidates) == 1:
|
|
246
|
+
final_candidate = f_candidates[0]
|
|
247
|
+
else:
|
|
248
|
+
fz_candidates = [c for c in f_candidates if c['file_type'] == "fz"]
|
|
249
|
+
final_candidate = fz_candidates[0] if fz_candidates else f_candidates
|
|
250
|
+
|
|
251
|
+
return final_candidate
|
|
252
|
+
|
|
253
|
+
def field_frame(self, field, band, weight=False, outfile=None, data_release="dr4"):
|
|
254
|
+
"""Download and open a full field FITS image.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
field : str
|
|
259
|
+
Field identifier, e.g., "SPLUS-n01s10".
|
|
260
|
+
band : str
|
|
261
|
+
Filter name, e.g., "R", "I", "F660", "U".
|
|
262
|
+
weight : bool, optional
|
|
263
|
+
If True, selects the "weight" pattern (commonly a weight map).
|
|
264
|
+
outfile : str or pathlib.Path, optional
|
|
265
|
+
If provided, ADSS will also write the downloaded file to this path.
|
|
266
|
+
data_release : str, optional
|
|
267
|
+
Target data release (pattern matched in collection name). Default "dr4".
|
|
268
|
+
|
|
269
|
+
Returns
|
|
270
|
+
-------
|
|
271
|
+
astropy.io.fits.HDUList
|
|
272
|
+
Opened FITS file as an HDUList.
|
|
273
|
+
|
|
274
|
+
Raises
|
|
275
|
+
------
|
|
276
|
+
SplusdataError
|
|
277
|
+
If the file cannot be resolved.
|
|
278
|
+
"""
|
|
279
|
+
if weight:
|
|
280
|
+
pattern = "weight"
|
|
281
|
+
else:
|
|
282
|
+
pattern = ""
|
|
283
|
+
|
|
284
|
+
final_candidate = self.get_file_metadata(field, band, pattern, data_release)
|
|
285
|
+
image_bytes = self.client.download_image(
|
|
286
|
+
final_candidate['id'],
|
|
287
|
+
output_path=outfile
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return fits.open(io.BytesIO(image_bytes))
|
|
291
|
+
|
|
292
|
+
def stamp(self, ra, dec, size, band, weight=False, field_name=None, size_unit="pixels", outfile=None, data_release="dr4"):
|
|
293
|
+
"""Create and open a FITS stamp (cutout) by coordinates or by object name.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
ra : float
|
|
298
|
+
Right ascension in degrees.
|
|
299
|
+
dec : float
|
|
300
|
+
Declination in degrees.
|
|
301
|
+
size : int or float
|
|
302
|
+
Stamp size in `size_unit`.
|
|
303
|
+
band : str
|
|
304
|
+
Filter name (e.g., "R", "I", "F660").
|
|
305
|
+
weight : bool, optional
|
|
306
|
+
If True, selects weight images (pattern "weight").
|
|
307
|
+
field_name : str, optional
|
|
308
|
+
If provided, creates a stamp using object/field name context instead
|
|
309
|
+
of pure coordinates (server may use FIELD metadata).
|
|
310
|
+
size_unit : {"pixels", "arcsec"}, optional
|
|
311
|
+
Unit for the size argument. Default "pixels".
|
|
312
|
+
outfile : str or pathlib.Path, optional
|
|
313
|
+
If provided, ADSS may also write the cutout to disk.
|
|
314
|
+
data_release : str, optional
|
|
315
|
+
Collection selector (substring). Default "dr4".
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
astropy.io.fits.HDUList
|
|
320
|
+
Opened FITS cutout.
|
|
321
|
+
|
|
322
|
+
Raises
|
|
323
|
+
------
|
|
324
|
+
SplusdataError
|
|
325
|
+
If the collection cannot be resolved.
|
|
326
|
+
"""
|
|
327
|
+
collection = self.get_collection_id_by_pattern(data_release)
|
|
328
|
+
collection_id = collection['id']
|
|
329
|
+
|
|
330
|
+
if weight:
|
|
331
|
+
weight = "weight"
|
|
332
|
+
if not field_name:
|
|
333
|
+
stamp_bytes = self.client.create_stamp_by_coordinates(
|
|
334
|
+
collection_id=collection_id,
|
|
335
|
+
filter=band,
|
|
336
|
+
ra=ra,
|
|
337
|
+
dec=dec,
|
|
338
|
+
size=size,
|
|
339
|
+
size_unit=size_unit,
|
|
340
|
+
pattern=weight if weight else "",
|
|
341
|
+
output_path=outfile
|
|
342
|
+
)
|
|
343
|
+
else:
|
|
344
|
+
stamp_bytes = self.client.stamp_images.create_stamp_by_object(
|
|
345
|
+
collection_id=collection_id,
|
|
346
|
+
object_name=field_name,
|
|
347
|
+
filter_name=band,
|
|
348
|
+
ra=ra,
|
|
349
|
+
dec=dec,
|
|
350
|
+
size=size,
|
|
351
|
+
size_unit=size_unit,
|
|
352
|
+
pattern=weight if weight else "",
|
|
353
|
+
output_path=outfile
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return fits.open(io.BytesIO(stamp_bytes))
|
|
357
|
+
|
|
358
|
+
def lupton_rgb(self, ra, dec, size, R="I", G="R", B="G", Q=8, stretch=3, field_name=None, size_unit="pixels", outfile=None, data_release="dr4"):
|
|
359
|
+
"""Create a Lupton RGB composite and return a PIL image.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
ra, dec : float
|
|
364
|
+
Coordinates in degrees.
|
|
365
|
+
size : int or float
|
|
366
|
+
Output image size in `size_unit`.
|
|
367
|
+
R, G, B : str, optional
|
|
368
|
+
Filter names for the RGB channels (defaults: I/R/G).
|
|
369
|
+
Q : float, optional
|
|
370
|
+
Lupton Q parameter (contrast). Default 8.
|
|
371
|
+
stretch : float, optional
|
|
372
|
+
Lupton stretch parameter. Default 3.
|
|
373
|
+
field_name : str, optional
|
|
374
|
+
If provided, generate by object/field context.
|
|
375
|
+
size_unit : {"pixels", "arcsec"}, optional
|
|
376
|
+
Unit for `size`. Default "pixels".
|
|
377
|
+
outfile : str or pathlib.Path, optional
|
|
378
|
+
If provided, ADSS may also write PNG/JPEG to disk.
|
|
379
|
+
data_release : str, optional
|
|
380
|
+
Collection selector (substring). Default "dr4".
|
|
381
|
+
|
|
382
|
+
Returns
|
|
383
|
+
-------
|
|
384
|
+
PIL.Image.Image
|
|
385
|
+
Composite RGB image.
|
|
386
|
+
"""
|
|
387
|
+
collection = self.get_collection_id_by_pattern(data_release)
|
|
388
|
+
collection_id = collection['id']
|
|
389
|
+
|
|
390
|
+
if not field_name:
|
|
391
|
+
stamp_bytes = self.client.create_rgb_image_by_coordinates(
|
|
392
|
+
collection_id=collection_id,
|
|
393
|
+
ra=ra,
|
|
394
|
+
dec=dec,
|
|
395
|
+
size=size,
|
|
396
|
+
r_filter=R,
|
|
397
|
+
g_filter=G,
|
|
398
|
+
b_filter=B,
|
|
399
|
+
Q=Q,
|
|
400
|
+
size_unit=size_unit,
|
|
401
|
+
stretch=stretch,
|
|
402
|
+
output_path=outfile
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
stamp_bytes = self.client.lupton_images.create_rgb_by_object(
|
|
406
|
+
collection_id=collection_id,
|
|
407
|
+
object_name=field_name,
|
|
408
|
+
ra=ra,
|
|
409
|
+
dec=dec,
|
|
410
|
+
size=size,
|
|
411
|
+
r_filter=R,
|
|
412
|
+
g_filter=G,
|
|
413
|
+
b_filter=B,
|
|
414
|
+
Q=Q,
|
|
415
|
+
size_unit=size_unit,
|
|
416
|
+
stretch=stretch,
|
|
417
|
+
output_path=outfile
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return Image.open(io.BytesIO(stamp_bytes))
|
|
421
|
+
|
|
422
|
+
def trilogy_image(self, ra, dec, size, R=["R", "I", "F861", "Z"], G=["G", "F515", "F660"], B=["U", "F378", "F395", "F410", "F430"], noiselum=0.15, satpercent=0.15, colorsatfac=2, size_unit="pixels", field_name=None, outfile=None, data_release="dr4"):
|
|
423
|
+
"""Create a Trilogy RGB composite (multi-filter blend) and return a PIL image.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
ra, dec : float
|
|
428
|
+
Coordinates in degrees.
|
|
429
|
+
size : int or float
|
|
430
|
+
Output size in `size_unit`.
|
|
431
|
+
R, G, B : list[str], optional
|
|
432
|
+
Lists of filters contributing to each RGB channel.
|
|
433
|
+
noiselum : float, optional
|
|
434
|
+
Controls noise luminance suppression.
|
|
435
|
+
satpercent : float, optional
|
|
436
|
+
Percentile value for saturation clipping.
|
|
437
|
+
colorsatfac : float, optional
|
|
438
|
+
Factor for color saturation.
|
|
439
|
+
size_unit : {"pixels", "arcsec"}, optional
|
|
440
|
+
Size unit. Default "pixels".
|
|
441
|
+
field_name : str, optional
|
|
442
|
+
If provided, generate by object/field context.
|
|
443
|
+
outfile : str or pathlib.Path, optional
|
|
444
|
+
If provided, ADSS may also write the composite to disk.
|
|
445
|
+
data_release : str, optional
|
|
446
|
+
Collection selector (substring). Default "dr4".
|
|
447
|
+
|
|
448
|
+
Returns
|
|
449
|
+
-------
|
|
450
|
+
PIL.Image.Image
|
|
451
|
+
Composite RGB image (Trilogy method).
|
|
452
|
+
"""
|
|
453
|
+
collection = self.get_collection_id_by_pattern(data_release)
|
|
454
|
+
collection_id = collection['id']
|
|
455
|
+
|
|
456
|
+
if not field_name:
|
|
457
|
+
stamp_bytes = self.client.trilogy_images.create_trilogy_rgb_by_coordinates(
|
|
458
|
+
collection_id=collection_id,
|
|
459
|
+
ra=ra,
|
|
460
|
+
dec=dec,
|
|
461
|
+
size=size,
|
|
462
|
+
r_filters=R,
|
|
463
|
+
g_filters=G,
|
|
464
|
+
b_filters=B,
|
|
465
|
+
size_unit=size_unit,
|
|
466
|
+
noiselum=noiselum,
|
|
467
|
+
satpercent=satpercent,
|
|
468
|
+
colorsatfac=colorsatfac,
|
|
469
|
+
output_path=outfile
|
|
470
|
+
)
|
|
471
|
+
else:
|
|
472
|
+
stamp_bytes = self.client.trilogy_images.create_trilogy_rgb_by_object(
|
|
473
|
+
collection_id=collection_id,
|
|
474
|
+
object_name=field_name,
|
|
475
|
+
ra=ra,
|
|
476
|
+
dec=dec,
|
|
477
|
+
size=size,
|
|
478
|
+
r_filters=R,
|
|
479
|
+
g_filters=G,
|
|
480
|
+
b_filters=B,
|
|
481
|
+
noiselum=noiselum,
|
|
482
|
+
size_unit=size_unit,
|
|
483
|
+
satpercent=satpercent,
|
|
484
|
+
colorsatfac=colorsatfac,
|
|
485
|
+
output_path=outfile
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
return Image.open(io.BytesIO(stamp_bytes))
|
|
489
|
+
|
|
490
|
+
def query(self, query, table_upload=None, table_name=None):
|
|
491
|
+
"""Execute a server-side query; optionally upload a small table first.
|
|
492
|
+
|
|
493
|
+
Parameters
|
|
494
|
+
----------
|
|
495
|
+
query : str
|
|
496
|
+
SQL/ADQL text to execute on the server.
|
|
497
|
+
table_upload : pandas.DataFrame or astropy.table.Table, optional
|
|
498
|
+
In-memory table to upload as a temporary (CSV) file for the query.
|
|
499
|
+
table_name : str, optional
|
|
500
|
+
Name to assign to the uploaded table on the server.
|
|
501
|
+
|
|
502
|
+
Returns
|
|
503
|
+
-------
|
|
504
|
+
Any
|
|
505
|
+
The `response.data` returned by `ADSSClient.query_and_wait`. Depends
|
|
506
|
+
on the query and server configuration (often JSON-like dict/list).
|
|
507
|
+
|
|
508
|
+
Raises
|
|
509
|
+
------
|
|
510
|
+
ValueError
|
|
511
|
+
If `table_upload` is provided but is neither a DataFrame nor an
|
|
512
|
+
Astropy Table.
|
|
513
|
+
Exception
|
|
514
|
+
Propagates server or network errors from the ADSS client.
|
|
515
|
+
"""
|
|
516
|
+
table_upload_bytes = None
|
|
517
|
+
if table_upload is not None and table_name is not None:
|
|
518
|
+
import pandas as pd
|
|
519
|
+
from astropy.table import Table
|
|
520
|
+
|
|
521
|
+
table_upload_bytes = None
|
|
522
|
+
if isinstance(table_upload, pd.DataFrame):
|
|
523
|
+
table_upload_bytes = table_upload.to_csv(index=False).encode()
|
|
524
|
+
elif isinstance(table_upload, Table):
|
|
525
|
+
table_upload_bytes = table_upload.to_pandas().to_csv(index=False).encode()
|
|
526
|
+
else:
|
|
527
|
+
raise ValueError("table_upload must be a pandas DataFrame or an astropy Table")
|
|
528
|
+
|
|
529
|
+
response = self.client.query_and_wait(
|
|
530
|
+
query_text=query,
|
|
531
|
+
table_name=table_name,
|
|
532
|
+
file=table_upload_bytes
|
|
533
|
+
)
|
|
534
|
+
return response.data
|
|
535
|
+
|
|
536
|
+
def get_zp_file(self, field, band, data_release = "dr6"):
|
|
537
|
+
"""Download and parse the per-field zero-point model (DR6).
|
|
538
|
+
|
|
539
|
+
Parameters
|
|
540
|
+
----------
|
|
541
|
+
field : str
|
|
542
|
+
Field name used in the DR6 collection.
|
|
543
|
+
band : str
|
|
544
|
+
Filter/band name.
|
|
545
|
+
data_release : str, optional
|
|
546
|
+
Collection selector, defaults to "dr6" (where zp models are expected).
|
|
547
|
+
|
|
548
|
+
Returns
|
|
549
|
+
-------
|
|
550
|
+
dict
|
|
551
|
+
Parsed JSON zero-point model.
|
|
552
|
+
|
|
553
|
+
Raises
|
|
554
|
+
------
|
|
555
|
+
SplusdataError
|
|
556
|
+
If no zero-point model file is found for the field/band.
|
|
557
|
+
JSONDecodeError
|
|
558
|
+
If the downloaded bytes are not valid JSON.
|
|
559
|
+
"""
|
|
560
|
+
import json
|
|
561
|
+
collection = self.get_collection_id_by_pattern(data_release)
|
|
562
|
+
collection_id = collection['id']
|
|
563
|
+
|
|
564
|
+
files = self.client.list_image_files(
|
|
565
|
+
collection_id,
|
|
566
|
+
filter_str=f"{field}_{band}_zp",
|
|
567
|
+
)
|
|
568
|
+
if len(files) == 0:
|
|
569
|
+
raise SplusdataError(f"No zp model found for field {field} in band {band} in {data_release}")
|
|
570
|
+
file = files[0]
|
|
571
|
+
|
|
572
|
+
print_level(f"Downloading zp_model {file['filename']}", 1, self.verbose)
|
|
573
|
+
json_bytes = self.client.download_image(file["id"])
|
|
574
|
+
json_data = json.loads(json_bytes)
|
|
575
|
+
return json_data
|
|
576
|
+
|
|
577
|
+
def get_zp(self, field, band, ra, dec):
|
|
578
|
+
"""Evaluate the local zero point at a sky position using the field model.
|
|
579
|
+
|
|
580
|
+
Parameters
|
|
581
|
+
----------
|
|
582
|
+
field : str
|
|
583
|
+
Field identifier for the zero-point model to use.
|
|
584
|
+
band : str
|
|
585
|
+
Filter name matching the zp model.
|
|
586
|
+
ra, dec : float
|
|
587
|
+
Coordinates (deg) where the zp should be evaluated.
|
|
588
|
+
|
|
589
|
+
Returns
|
|
590
|
+
-------
|
|
591
|
+
float
|
|
592
|
+
Zero point value at (ra, dec), in magnitudes.
|
|
593
|
+
|
|
594
|
+
Raises
|
|
595
|
+
------
|
|
596
|
+
SplusdataError
|
|
597
|
+
If the model file cannot be found/downloaded.
|
|
598
|
+
Exception
|
|
599
|
+
Any error propagated from `zp_at_coord` evaluation.
|
|
600
|
+
"""
|
|
601
|
+
model = self.get_zp_file(field, band)
|
|
602
|
+
|
|
603
|
+
from splusdata.features.zeropoints.zp_map import zp_at_coord
|
|
604
|
+
return zp_at_coord(model, ra, dec)
|
|
605
|
+
|
|
606
|
+
def calibrated_stamp(self, ra, dec, size, band, weight=False, field_name=None, size_unit="pixels", outfile=None, data_release="dr6"):
|
|
607
|
+
"""Create a stamp and return a photometrically calibrated PrimaryHDU.
|
|
608
|
+
|
|
609
|
+
This computes a cutout via `stamp(...)`, then loads the appropriate DR6+
|
|
610
|
+
per-field zero-point model and applies spatially varying calibration.
|
|
611
|
+
|
|
612
|
+
Parameters
|
|
613
|
+
----------
|
|
614
|
+
ra, dec : float
|
|
615
|
+
Coordinates in degrees.
|
|
616
|
+
size : int or float
|
|
617
|
+
Cutout size in `size_unit`.
|
|
618
|
+
band : str
|
|
619
|
+
Filter name.
|
|
620
|
+
weight : bool, optional
|
|
621
|
+
If True, returns weight cutouts (note: calibration typically applies
|
|
622
|
+
to science images, not weights).
|
|
623
|
+
field_name : str, optional
|
|
624
|
+
Use object/field context for the stamp creation.
|
|
625
|
+
size_unit : {"pixels", "arcsec"}, optional
|
|
626
|
+
Size unit (default "pixels").
|
|
627
|
+
outfile : str or pathlib.Path, optional
|
|
628
|
+
If provided, writes the calibrated HDU to disk (FITS).
|
|
629
|
+
data_release : str, optional
|
|
630
|
+
DR to use for both the stamp and the zp model (default "dr6").
|
|
631
|
+
|
|
632
|
+
Returns
|
|
633
|
+
-------
|
|
634
|
+
astropy.io.fits.PrimaryHDU
|
|
635
|
+
The calibrated science HDU (new object unless `in_place=True` were used).
|
|
636
|
+
|
|
637
|
+
Raises
|
|
638
|
+
------
|
|
639
|
+
SplusdataError
|
|
640
|
+
If the zp model cannot be found.
|
|
641
|
+
KeyError
|
|
642
|
+
If expected header keys (e.g., FIELD, FILTER) are missing in the stamp.
|
|
643
|
+
Exception
|
|
644
|
+
Propagates any calibration errors from `calibrate_hdu_with_zpmodel`.
|
|
645
|
+
"""
|
|
646
|
+
stamp = self.stamp(ra, dec, size, band, weight=weight, field_name=field_name, size_unit=size_unit, data_release=data_release)
|
|
647
|
+
|
|
648
|
+
from splusdata.features.zeropoints.zp_image import calibrate_hdu_with_zpmodel
|
|
649
|
+
zp_model = self.get_zp_file(stamp[1].header["FIELD"], stamp[1].header["FILTER"], data_release=data_release)
|
|
650
|
+
|
|
651
|
+
calibrated_hdu, factor_map = calibrate_hdu_with_zpmodel(
|
|
652
|
+
stamp[1], zp_model, in_place=False, return_factor=True
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
if outfile:
|
|
656
|
+
calibrated_hdu.writeto(outfile, overwrite=True)
|
|
657
|
+
return calibrated_hdu
|