Skip to content

Commit 5ed0079

Browse files
authored
Merge pull request #3398 from snbianco/resolve_multi
2 parents 3c9388e + 2c588eb commit 5ed0079

File tree

8 files changed

+465
-125
lines changed

8 files changed

+465
-125
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ mast
133133

134134
- Fix bug where duplicate columns from server responses cause an error when converting to an `~astropy.table.Table`. [#3400]
135135

136+
- Support for resolving multiple object names at once with `~astroquery.mast.MastClass.resolve_object`, including automatic batching
137+
into groups of up to 30 names per request to the name translation service. [#3398]
136138

137139
Infrastructure, Utility and Other Changes and Additions
138140
-------------------------------------------------------

astroquery/mast/missions.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from astroquery import log
2222
from astroquery.utils import commons, async_to_sync
2323
from astroquery.utils.class_or_instance import class_or_instance
24-
from astropy.utils.console import ProgressBarOrSpinner
2524
from astroquery.exceptions import InvalidQueryError, MaxResultsWarning, NoResultsWarning
2625

2726
from astroquery.mast import utils
@@ -439,31 +438,18 @@ def get_product_list_async(self, datasets):
439438
# Filter out duplicates
440439
datasets = list(set(datasets))
441440

442-
# Batch API calls if number of datasets exceeds maximum
443-
max_batch = 1000
444-
num_datasets = len(datasets)
445-
if num_datasets > max_batch:
446-
# Split datasets into chunks
447-
dataset_chunks = list(utils.split_list_into_chunks(datasets, max_batch))
448-
449-
results = [] # list to store responses from each batch
450-
with ProgressBarOrSpinner(num_datasets, f'Fetching products for {num_datasets} unique datasets '
451-
f'in {len(dataset_chunks)} batches ...') as pb:
452-
datasets_fetched = 0
453-
pb.update(0)
454-
for chunk in dataset_chunks:
455-
# Send request for each chunk and add response to list
456-
params = {'dataset_ids': chunk}
457-
results.append(self._service_api_connection.missions_request_async(self.service, params))
458-
459-
# Update progress bar with the number of datasets that have had products fetched
460-
datasets_fetched += len(chunk)
461-
pb.update(datasets_fetched)
462-
return results
463-
else:
464-
# Single batch request
465-
params = {'dataset_ids': datasets}
466-
return self._service_api_connection.missions_request_async(self.service, params)
441+
results = utils._batched_request(
442+
datasets,
443+
params={},
444+
max_batch=1000,
445+
param_key="dataset_ids",
446+
request_func=lambda p: self._service_api_connection.missions_request_async(self.service, p),
447+
extract_func=lambda r: [r], # missions_request_async already returns one result
448+
desc=f"Fetching products for {len(datasets)} unique datasets"
449+
)
450+
451+
# Return a list of responses only if multiple requests were made
452+
return results[0] if len(results) == 1 else results
467453

468454
def get_unique_product_list(self, datasets):
469455
"""

astroquery/mast/tests/data/README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ To generate `~astroquery.mast.tests.data.resolver.json`, use the following:
5757
>>> import json
5858
>>> from astroquery.mast import utils
5959
...
60+
>>> objects = ["TIC 307210830", "Barnard's Star", "M1", "M101", "M103", "M8", "M10"]
6061
>>> resp = utils._simple_request('http://mastresolver.stsci.edu/Santa-war/query',
61-
... {'name': 'TIC 307210830', 'outputFormat': 'json', 'resolveAll': 'true'})
62+
... {'name': objects, 'outputFormat': 'json', 'resolveAll': 'true'})
6263
>>> with open('resolver.json', 'w') as file:
6364
... json.dump(resp.json(), file, indent=4) # doctest: +SKIP

astroquery/mast/tests/data/resolver.json

Lines changed: 164 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44
"searchString": "tic 307210830",
55
"resolver": "TIC",
66
"cached": false,
7-
"resolverTime": 3,
7+
"resolverTime": 2,
88
"searchRadius": 0.000333,
99
"canonicalName": "TIC 307210830",
1010
"ra": 124.531756290083,
1111
"decl": -68.3129998725044
1212
},
1313
{
1414
"searchString": "tic 307210830",
15-
"resolver": "SIMBADCFA",
15+
"resolver": "SIMBAD",
1616
"cached": true,
17-
"resolverTime": 289,
18-
"cacheDate": "Mar 19, 2025, 4:36:27 PM",
17+
"resolverTime": 294,
18+
"cacheDate": "Apr 17, 2025, 3:47:59 PM",
1919
"searchRadius": -1.0,
2020
"canonicalName": "L 98-59",
2121
"ra": 124.5317560026638,
@@ -24,15 +24,172 @@
2424
},
2525
{
2626
"searchString": "tic 307210830",
27-
"resolver": "SIMBAD",
27+
"resolver": "SIMBADCFA",
2828
"cached": true,
29-
"resolverTime": 299,
30-
"cacheDate": "Apr 17, 2025, 3:47:59 PM",
29+
"resolverTime": 296,
30+
"cacheDate": "Mar 19, 2025, 4:36:27 PM",
3131
"searchRadius": -1.0,
3232
"canonicalName": "L 98-59",
3333
"ra": 124.5317560026638,
3434
"decl": -68.3130014904408,
3535
"objectType": "HighPM*"
36+
},
37+
{
38+
"searchString": "barnard's star",
39+
"resolver": "SIMBAD",
40+
"cached": true,
41+
"resolverTime": 302,
42+
"cacheDate": "May 7, 2025, 2:29:06 PM",
43+
"searchRadius": -1.0,
44+
"canonicalName": "NAME Barnard's star",
45+
"ra": 269.4520769586187,
46+
"decl": 4.6933649665767,
47+
"objectType": "BYDraV*"
48+
},
49+
{
50+
"searchString": "m1",
51+
"resolver": "SIMBAD",
52+
"cached": true,
53+
"resolverTime": 292,
54+
"cacheDate": "Apr 29, 2025, 1:57:27 PM",
55+
"searchRadius": -1.0,
56+
"canonicalName": "M 1",
57+
"ra": 83.6324,
58+
"decl": 22.0174,
59+
"radius": 0.058333333333333334,
60+
"majorAxis": 0.11666666666666667,
61+
"minorAxis": 0.08333333333333333,
62+
"objectType": "SNRemnant"
63+
},
64+
{
65+
"searchString": "m1",
66+
"resolver": "NED",
67+
"cached": false,
68+
"resolverTime": 1417,
69+
"searchRadius": -1.0,
70+
"canonicalName": "MESSIER 001",
71+
"ra": 83.63311,
72+
"decl": 22.01449,
73+
"objectType": "SNR"
74+
},
75+
{
76+
"searchString": "m101",
77+
"resolver": "SIMBAD",
78+
"cached": true,
79+
"resolverTime": 290,
80+
"cacheDate": "Sep 2, 2025, 8:13:51 PM",
81+
"searchRadius": -1.0,
82+
"canonicalName": "M 101",
83+
"ra": 210.802429,
84+
"decl": 54.34875,
85+
"radius": 0.18233333333333332,
86+
"majorAxis": 0.36466666666666664,
87+
"minorAxis": 0.3481666666666667,
88+
"objectType": "GinPair"
89+
},
90+
{
91+
"searchString": "m101",
92+
"resolver": "SIMBADCFA",
93+
"cached": true,
94+
"resolverTime": 289,
95+
"cacheDate": "May 14, 2025, 8:04:05 PM",
96+
"searchRadius": -1.0,
97+
"canonicalName": "M 101",
98+
"ra": 210.802429,
99+
"decl": 54.34875,
100+
"radius": 0.18233333333333332,
101+
"majorAxis": 0.36466666666666664,
102+
"minorAxis": 0.3481666666666667,
103+
"objectType": "GinPair"
104+
},
105+
{
106+
"searchString": "m101",
107+
"resolver": "NED",
108+
"cached": false,
109+
"resolverTime": 1181,
110+
"searchRadius": -1.0,
111+
"canonicalName": "MESSIER 101",
112+
"ra": 210.80227,
113+
"decl": 54.34895,
114+
"radius": 0.24000000000000002,
115+
"objectType": "G"
116+
},
117+
{
118+
"searchString": "m103",
119+
"resolver": "SIMBAD",
120+
"cached": true,
121+
"resolverTime": 294,
122+
"cacheDate": "May 2, 2025, 4:33:25 AM",
123+
"searchRadius": -1.0,
124+
"canonicalName": "M 103",
125+
"ra": 23.339,
126+
"decl": 60.659,
127+
"radius": 0.06166666666666667,
128+
"majorAxis": 0.12333333333333334,
129+
"minorAxis": 0.12333333333333334,
130+
"positionAngle": 0.0,
131+
"objectType": "OpenCluster"
132+
},
133+
{
134+
"searchString": "m103",
135+
"resolver": "NED",
136+
"cached": false,
137+
"resolverTime": 1094,
138+
"searchRadius": -1.0,
139+
"canonicalName": "MESSIER 103",
140+
"ra": 23.34086,
141+
"decl": 60.658,
142+
"objectType": "*Cl"
143+
},
144+
{
145+
"searchString": "m8",
146+
"resolver": "SIMBAD",
147+
"cached": true,
148+
"resolverTime": 294,
149+
"cacheDate": "May 1, 2025, 1:11:47 PM",
150+
"searchRadius": -1.0,
151+
"canonicalName": "M 8",
152+
"ra": 270.904,
153+
"decl": -24.387,
154+
"objectType": "OpenCluster"
155+
},
156+
{
157+
"searchString": "m8",
158+
"resolver": "NED",
159+
"cached": false,
160+
"resolverTime": 1165,
161+
"searchRadius": -1.0,
162+
"canonicalName": "MESSIER 008",
163+
"ra": 270.92194,
164+
"decl": -24.38017,
165+
"objectType": "Neb"
166+
},
167+
{
168+
"searchString": "m10",
169+
"resolver": "SIMBAD",
170+
"cached": true,
171+
"resolverTime": 290,
172+
"cacheDate": "Apr 29, 2025, 2:00:16 PM",
173+
"searchRadius": -1.0,
174+
"canonicalName": "M 10",
175+
"ra": 254.28771,
176+
"decl": -4.10031,
177+
"radius": 0.12583333333333332,
178+
"majorAxis": 0.25166666666666665,
179+
"minorAxis": 0.25166666666666665,
180+
"positionAngle": 90.0,
181+
"objectType": "GlobCluster"
182+
},
183+
{
184+
"searchString": "m10",
185+
"resolver": "NED",
186+
"cached": false,
187+
"resolverTime": 998,
188+
"searchRadius": -1.0,
189+
"canonicalName": "MESSIER 010",
190+
"ra": 254.28771,
191+
"decl": -4.10031,
192+
"objectType": "*Cl"
36193
}
37194
],
38195
"status": ""

astroquery/mast/tests/test_mast.py

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import os
55
import re
6+
import warnings
67
from shutil import copyfile
78
from unittest.mock import patch
89

@@ -566,36 +567,91 @@ def test_mast_query(patch_post):
566567
assert "Please provide at least one filter." in str(invalid_query.value)
567568

568569

569-
def test_resolve_object(patch_post):
570+
def test_resolve_object_single(patch_post):
570571
obj = "TIC 307210830"
571572
tic_coord = SkyCoord(124.531756290083, -68.3129998725044, unit="deg")
572573
simbad_coord = SkyCoord(124.5317560026638, -68.3130014904408, unit="deg")
574+
575+
# Resolve without a specific resolver
573576
obj_loc = mast.Mast.resolve_object(obj)
577+
assert isinstance(obj_loc, SkyCoord)
574578
assert round(obj_loc.separation(tic_coord).value, 10) == 0
575579

576-
# resolve using a specific resolver and an object that belongs to a MAST catalog
580+
# Resolve using a specific resolver and an object that belongs to a MAST catalog
577581
obj_loc_simbad = mast.Mast.resolve_object(obj, resolver="SIMBAD")
578582
assert round(obj_loc_simbad.separation(simbad_coord).value, 10) == 0
579583

580-
# resolve using a specific resolver and an object that does not belong to a MAST catalog
581-
obj_loc_simbad = mast.Mast.resolve_object("M101", resolver="SIMBAD")
582-
assert round(obj_loc_simbad.separation(simbad_coord).value, 10) == 0
584+
# Resolve using a specific resolver and an object that does not belong to a MAST catalog
585+
m1_coord = SkyCoord(83.6324, 22.0174, unit="deg")
586+
obj_loc_simbad = mast.Mast.resolve_object("M1", resolver="SIMBAD")
587+
assert isinstance(obj_loc_simbad, SkyCoord)
588+
assert round(obj_loc_simbad.separation(m1_coord).value, 10) == 0
583589

584-
# resolve using all resolvers
590+
# Resolve using all resolvers
585591
obj_loc_dict = mast.Mast.resolve_object(obj, resolve_all=True)
586592
assert isinstance(obj_loc_dict, dict)
587593
assert round(obj_loc_dict["SIMBAD"].separation(simbad_coord).value, 10) == 0
594+
assert round(obj_loc_dict["TIC"].separation(tic_coord).value, 10) == 0
588595

589-
# error with invalid resolver
596+
# Error with invalid resolver
590597
with pytest.raises(ResolverError, match="Invalid resolver"):
591598
mast.Mast.resolve_object(obj, resolver="invalid")
592599

593-
# warn if specifying both resolver and resolve_all
600+
# Error if single object cannot be resolved
601+
with pytest.raises(ResolverError, match='Could not resolve "nonexisting" to a sky position.'):
602+
mast.Mast.resolve_object("nonexisting")
603+
604+
# Error if single object cannot be resolved with given resolver
605+
with pytest.raises(ResolverError, match='Could not resolve "Barnard\'s Star" to a sky position using '
606+
'resolver "NED".'):
607+
mast.Mast.resolve_object("Barnard's Star", resolver="NED")
608+
609+
# Warn if specifying both resolver and resolve_all
594610
with pytest.warns(InputWarning, match="The resolver parameter is ignored when resolve_all is True"):
595611
loc = mast.Mast.resolve_object(obj, resolver="NED", resolve_all=True)
596612
assert isinstance(loc, dict)
597613

598614

615+
def test_resolve_object_multi(patch_post):
616+
objects = ["TIC 307210830", "M1", "Barnard's Star"]
617+
618+
# No resolver specified
619+
coord_dict = mast.Mast.resolve_object(objects)
620+
assert isinstance(coord_dict, dict)
621+
for obj in objects:
622+
assert obj in coord_dict
623+
assert isinstance(coord_dict[obj], SkyCoord)
624+
625+
# Warn if one of the objects cannot be resolved
626+
with pytest.warns(InputWarning, match='Could not resolve "nonexisting" to a sky position.'):
627+
coord_dict = mast.Mast.resolve_object(["M1", "nonexisting"])
628+
629+
# Resolver specified
630+
coord_dict = mast.Mast.resolve_object(objects, resolver="SIMBAD")
631+
assert isinstance(coord_dict, dict)
632+
for obj in objects:
633+
assert obj in coord_dict
634+
assert isinstance(coord_dict[obj], SkyCoord)
635+
636+
# Warn if one of the objects can't be resolved with given resolver
637+
with pytest.warns(InputWarning, match='Could not resolve "TIC 307210830" to a sky position using resolver "NED"'):
638+
mast.Mast.resolve_object(objects[:2], resolver="NED")
639+
640+
# Resolve all
641+
coord_dict = mast.Mast.resolve_object(objects, resolve_all=True)
642+
assert isinstance(coord_dict, dict)
643+
for obj in objects:
644+
assert obj in coord_dict
645+
obj_dict = coord_dict[obj]
646+
assert isinstance(obj_dict, dict)
647+
assert isinstance(obj_dict["SIMBAD"], SkyCoord)
648+
649+
# Error if none of the objects can be resolved
650+
warnings.simplefilter("ignore", category=InputWarning) # ignore warnings
651+
with pytest.raises(ResolverError, match='Could not resolve any of the given object names to sky positions.'):
652+
mast.Mast.resolve_object(["nonexisting1", "nonexisting2"])
653+
654+
599655
def test_login_logout(patch_post):
600656
test_token = "56a9cf3df4c04052atest43feb87f282"
601657

0 commit comments

Comments
 (0)