Skip to content

Commit db80653

Browse files
committed
feat: add query_hierarchy to get children, parents, or siblings of an object
1 parent 94dbe5c commit db80653

File tree

6 files changed

+228
-2
lines changed

6 files changed

+228
-2
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ simbad
176176
- fixed ``query_objects`` that would not work in combination with the additional field
177177
``ident`` [#3149]
178178

179+
- added ``query_hierarchy``: a new method that allows to get the parents, children, or
180+
siblings of an object [#3175]
181+
179182
skyview
180183
^^^^^^^
181184

astroquery/simbad/core.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ def columns_in_output(self):
176176
- `query_objects`,
177177
- `query_region`,
178178
- `query_catalog`,
179+
- `query_hierarchy`,
179180
- `query_bibobj`,
180181
- `query_criteria`.
181182
@@ -359,6 +360,7 @@ def add_votable_fields(self, *args):
359360
- `query_objects`,
360361
- `query_region`,
361362
- `query_catalog`,
363+
- `query_hierarchy`,
362364
- `query_bibobj`,
363365
- `query_criteria`.
364366
@@ -487,6 +489,7 @@ def reset_votable_fields(self):
487489
- `query_objects`,
488490
- `query_region`,
489491
- `query_catalog`,
492+
- `query_hierarchy`,
490493
- `query_bibobj`,
491494
- `query_criteria`.
492495
@@ -855,6 +858,86 @@ def query_catalog(self, catalog, *, criteria=None, get_query_payload=False,
855858
return self._query(top, columns, joins, instance_criteria,
856859
get_query_payload=get_query_payload)
857860

861+
def query_hierarchy(self, name, hierarchy, *,
862+
detailed_hierarchy=False,
863+
criteria=None, get_query_payload=False):
864+
"""Query either the parents or the children of the object.
865+
866+
Parameters
867+
----------
868+
name : str
869+
name of the object
870+
hierarchy : str
871+
Can take the values "parents" to return the parents of the object (ex: a
872+
galaxy cluster is a parent of a galaxy), the value "children" to return
873+
the children of an object (ex: stars can be children of a globular cluster),
874+
or the value "siblings" to return the object that share a parent with the
875+
given one (ex: the stars of an open cluster are all siblings).
876+
detailed_hierarchy : bool
877+
Whether to add the two extra columns 'hierarchy_bibcode' that gives the
878+
article in which the hierarchy link is mentioned, and
879+
'membership_certainty'. membership_certainty is an integer that reflects the
880+
certainty of the hierarchy link according to the authors. Ranges between 0
881+
and 100 where 100 means that the authors were certain of the classification.
882+
Defaults to False.
883+
criteria : str
884+
Criteria to be applied to the query. These should be written in the ADQL
885+
syntax in a single string. See example.
886+
get_query_payload : bool, optional
887+
When set to `True` the method returns the HTTP request parameters without
888+
querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
889+
Defaults to `False`.
890+
891+
Returns
892+
-------
893+
table : `~astropy.table.Table`
894+
Query results table
895+
896+
Examples
897+
--------
898+
>>> from astroquery.simbad import Simbad
899+
>>> parent = Simbad.query_hierarchy("2MASS J18511048-0615470",
900+
... hierarchy="parents") # doctest: +REMOTE_DATA
901+
>>> parent[["main_id", "ra", "dec"]] # doctest: +REMOTE_DATA
902+
<Table length=1>
903+
main_id ra dec
904+
deg deg
905+
object float64 float64
906+
--------- ------- -------
907+
NGC 6705 282.766 -6.272
908+
"""
909+
top, columns, joins, instance_criteria = self._get_query_parameters()
910+
911+
sub_query = ("(SELECT oidref FROM ident "
912+
f"WHERE id = '{name}') AS name")
913+
914+
if detailed_hierarchy:
915+
columns.append(_Column("h_link", "link_bibcode", "hierarchy_bibcode"))
916+
columns.append(_Column("h_link", "membership", "membership_certainty"))
917+
918+
if hierarchy == "parents":
919+
joins += [_Join("h_link", _Column("basic", "oid"), _Column("h_link", "parent"))]
920+
instance_criteria.append("h_link.child = name.oidref")
921+
elif hierarchy == "children":
922+
joins += [_Join("h_link", _Column("basic", "oid"), _Column("h_link", "child"))]
923+
instance_criteria.append("h_link.parent = name.oidref")
924+
elif hierarchy == "siblings":
925+
sub_query = ("(SELECT DISTINCT basic.oid FROM "
926+
f"{sub_query}, basic JOIN h_link ON basic.oid = h_link.parent "
927+
"WHERE h_link.child = name.oidref) AS parents")
928+
joins += [_Join("h_link", _Column("basic", "oid"), _Column("h_link", "child"))]
929+
instance_criteria.append("h_link.parent = parents.oid")
930+
else:
931+
raise ValueError("'hierarchy' can only take the values 'parents', "
932+
f"'siblings', or 'children'. Got '{hierarchy}'.")
933+
934+
if criteria:
935+
instance_criteria.append(f"({criteria})")
936+
937+
return self._query(top, columns, joins, instance_criteria,
938+
from_table=f"{sub_query}, basic", distinct=True,
939+
get_query_payload=get_query_payload)
940+
858941
@deprecated_renamed_argument(["verbose"], new_name=[None],
859942
since=['0.4.8'], relax=True)
860943
def query_bibobj(self, bibcode, *, criteria=None,
@@ -1369,7 +1452,7 @@ def _get_query_parameters(self):
13691452
"""Get the current building blocks of an ADQL query."""
13701453
return tuple(map(copy.deepcopy, (self.ROW_LIMIT, self.columns_in_output, self.joins, self.criteria)))
13711454

1372-
def _query(self, top, columns, joins, criteria, from_table="basic",
1455+
def _query(self, top, columns, joins, criteria, from_table="basic", distinct=False,
13731456
get_query_payload=False, **uploads):
13741457
"""Generate an ADQL string from the given query parameters and executes the query.
13751458
@@ -1386,6 +1469,8 @@ def _query(self, top, columns, joins, criteria, from_table="basic",
13861469
with an AND clause.
13871470
from_table : str, optional
13881471
The table after 'FROM' in the ADQL string. Defaults to "basic".
1472+
distinct : bool, optional
1473+
Whether to add the DISTINCT instruction to the query.
13891474
get_query_payload : bool, optional
13901475
When set to `True` the method returns the HTTP request parameters without
13911476
querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
@@ -1400,6 +1485,7 @@ def _query(self, top, columns, joins, criteria, from_table="basic",
14001485
`~astropy.table.Table`
14011486
The result of the query to SIMBAD.
14021487
"""
1488+
distinct_results = " DISTINCT" if distinct else ""
14031489
top_part = f" TOP {top}" if top != -1 else ""
14041490

14051491
# columns
@@ -1433,7 +1519,7 @@ def _query(self, top, columns, joins, criteria, from_table="basic",
14331519
else:
14341520
criteria = ""
14351521

1436-
query = f"SELECT{top_part}{columns} FROM {from_table}{join}{criteria}"
1522+
query = f"SELECT{distinct_results}{top_part}{columns} FROM {from_table}{join}{criteria}"
14371523

14381524
response = self.query_tap(query, get_query_payload=get_query_payload,
14391525
maxrec=self.hardlimit,

astroquery/simbad/tests/test_simbad.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,37 @@ def test_query_catalog():
345345
assert adql.endswith(where_clause)
346346

347347

348+
@pytest.mark.usefixtures("_mock_simbad_class")
349+
def test_query_hierarchy():
350+
simbad_instance = simbad.Simbad()
351+
detailed = ('h_link."link_bibcode" AS "hierarchy_bibcode", h_link."membership"'
352+
' AS "membership_certainty"')
353+
# the three possible cases
354+
adql = simbad_instance.query_hierarchy("test", hierarchy="parents",
355+
detailed_hierarchy=True,
356+
get_query_payload=True)["QUERY"]
357+
assert "h_link.child = name.oidref" in adql
358+
assert detailed in adql
359+
adql = simbad_instance.query_hierarchy("test", hierarchy="children",
360+
criteria="test=test",
361+
get_query_payload=True)["QUERY"]
362+
assert "h_link.parent = name.oidref" in adql
363+
assert "test=test" in adql
364+
assert detailed not in adql
365+
adql = simbad_instance.query_hierarchy("test", hierarchy="siblings",
366+
get_query_payload=True)["QUERY"]
367+
assert "h_link.parent = parents.oid" in adql
368+
# if the keyword does not correspond
369+
with pytest.raises(ValueError, match="'hierarchy' can only take the values "
370+
"'parents', 'siblings', or 'children'. Got 'test'."):
371+
simbad_instance.query_hierarchy("object", hierarchy="test",
372+
get_query_payload=True)
373+
# if the people were used to the old votable_fields
374+
with pytest.raises(ValueError, match="The hierarchy information is no longer an "
375+
"additional field. *"):
376+
simbad_instance.add_votable_fields("membership")
377+
378+
348379
@pytest.mark.parametrize(('coordinates', 'radius', 'where'),
349380
[(ICRS_COORDS, 2*u.arcmin,
350381
r"WHERE CONTAINS\(POINT\('ICRS', basic\.ra, basic\.dec\), "

astroquery/simbad/tests/test_simbad_remote.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ def test_query_catalog(self):
5555
result = self.simbad.query_catalog('M')
5656
assert len(result) == 110
5757

58+
def test_query_hierarchy(self):
59+
self.simbad.ROW_LIMIT = -1
60+
obj = "NGC 4038"
61+
parents = self.simbad.query_hierarchy(obj, hierarchy="parents")
62+
assert len(parents) == 4
63+
children = self.simbad.query_hierarchy(obj, hierarchy="children")
64+
assert len(children) >= 45 # as of 2025, but more could be added
65+
siblings = self.simbad.query_hierarchy(obj, hierarchy="siblings",
66+
criteria="otype='G..'")
67+
assert len(siblings) >= 29
68+
5869
def test_query_region(self):
5970
self.simbad.ROW_LIMIT = 10
6071
result = self.simbad.query_region(ICRS_COORDS_M42, radius="1d")

astroquery/simbad/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def _catch_deprecated_fields_with_arguments(votable_field):
3737
if votable_field.startswith("bibcodelist("):
3838
raise ValueError("Selecting a range of years for bibcode is removed. You can still use "
3939
"bibcodelist without parenthesis and get the full list of bibliographic references.")
40+
if votable_field in ["membership", "link_bibcode"]:
41+
raise ValueError("The hierarchy information is no longer an additional field. "
42+
"It has been replaced by the 'query_hierarchy' method.")
4043

4144
# ----------------------------
4245
# Support wildcard argument

docs/simbad/simbad.rst

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,96 @@ associated with an object.
155155
NAME North Star
156156
WEB 2438
157157

158+
Query to get all parents (or children, or siblings) of an object
159+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
160+
161+
Let's find the galaxies composing the galaxy pair ``Mrk 116``:
162+
163+
.. doctest-remote-data::
164+
165+
>>> from astroquery.simbad import Simbad
166+
>>> galaxies = Simbad.query_hierarchy("Mrk 116",
167+
... hierarchy="children", criteria="otype='G..'")
168+
>>> galaxies[["main_id", "ra", "dec"]]
169+
<Table length=2>
170+
main_id ra dec
171+
deg deg
172+
object float64 float64
173+
--------- --------------- --------------
174+
Mrk 116A 143.50821525019 55.24105273196
175+
Mrk 116B 143.509956 55.239762
176+
177+
Alternatively, if we know one member of a group, we can find the others by asking for
178+
``siblings``:
179+
180+
.. doctest-remote-data::
181+
182+
>>> from astroquery.simbad import Simbad
183+
>>> galaxies = Simbad.query_hierarchy("Mrk 116A",
184+
... hierarchy="siblings", criteria="otype='G..'")
185+
>>> galaxies[["main_id", "ra", "dec"]]
186+
<Table length=2>
187+
main_id ra dec
188+
deg deg
189+
object float64 float64
190+
--------- --------------- --------------
191+
Mrk 116A 143.50821525019 55.24105273196
192+
Mrk 116B 143.509956 55.239762
193+
194+
Note that if we had not added the criteria on the object type, we would also get
195+
some stars that are part of these galaxies in the result.
196+
197+
And the other way around, let's find which cluster of stars contains
198+
``2MASS J18511048-0615470``:
199+
200+
.. doctest-remote-data::
201+
202+
>>> from astroquery.simbad import Simbad
203+
>>> cluster = Simbad.query_hierarchy("2MASS J18511048-0615470", hierarchy="parents")
204+
>>> cluster[["main_id", "ra", "dec"]]
205+
<Table length=1>
206+
main_id ra dec
207+
deg deg
208+
object float64 float64
209+
--------- ------- -------
210+
NGC 6705 282.766 -6.272
211+
212+
If needed, we can get a more detailed report with the two extra columns:
213+
- ``hierarchy_bibcode`` : the paper in which the hierarchy is established,
214+
- ``membership_certainty``: if present in the paper, a certainty index (100 meaning
215+
100% sure).
216+
217+
.. doctest-remote-data::
218+
219+
>>> from astroquery.simbad import Simbad
220+
>>> cluster = Simbad.query_hierarchy("2MASS J18511048-0615470",
221+
... hierarchy="parents",
222+
... detailed_hierarchy=True)
223+
>>> cluster[["main_id", "ra", "dec", "hierarchy_bibcode", "membership_certainty"]]
224+
<Table length=13>
225+
main_id ra dec hierarchy_bibcode membership_certainty
226+
deg deg percent
227+
object float64 float64 object int16
228+
--------- ------- ------- ------------------- --------------------
229+
NGC 6705 282.766 -6.272 2014A&A...563A..44M 100
230+
NGC 6705 282.766 -6.272 2015A&A...573A..55T 100
231+
NGC 6705 282.766 -6.272 2016A&A...591A..37J 100
232+
NGC 6705 282.766 -6.272 2018A&A...618A..93C 100
233+
NGC 6705 282.766 -6.272 2020A&A...633A..99C 100
234+
NGC 6705 282.766 -6.272 2020A&A...640A...1C 100
235+
NGC 6705 282.766 -6.272 2020A&A...643A..71G 100
236+
NGC 6705 282.766 -6.272 2020ApJ...903...55P 100
237+
NGC 6705 282.766 -6.272 2020MNRAS.496.4701J 100
238+
NGC 6705 282.766 -6.272 2021A&A...647A..19T 100
239+
NGC 6705 282.766 -6.272 2021A&A...651A..84M 100
240+
NGC 6705 282.766 -6.272 2021MNRAS.503.3279S 99
241+
NGC 6705 282.766 -6.272 2022MNRAS.509.1664J 100
242+
243+
Here, we see that the Simbad team found 13 papers mentioning the fact that
244+
``2MASS J18511048-0615470`` is a member of ``NGC 6705`` and that the authors of these
245+
articles gave high confidence indices for this membership (``membership_certainty`` is
246+
close to 100 for all bibcodes).
247+
158248

159249
Query a region
160250
^^^^^^^^^^^^^^
@@ -421,6 +511,7 @@ Some query methods outputs can be customized. This is the case for:
421511
- `~astroquery.simbad.SimbadClass.query_objects`
422512
- `~astroquery.simbad.SimbadClass.query_region`
423513
- `~astroquery.simbad.SimbadClass.query_catalog`
514+
- `~astroquery.simbad.SimbadClass.query_hierarchy`
424515
- `~astroquery.simbad.SimbadClass.query_bibobj`
425516

426517
For these methods, the default columns in the output are:
@@ -523,6 +614,7 @@ Most query methods take a ``criteria`` argument. They are listed here:
523614
- `~astroquery.simbad.SimbadClass.query_objects`
524615
- `~astroquery.simbad.SimbadClass.query_region`
525616
- `~astroquery.simbad.SimbadClass.query_catalog`
617+
- `~astroquery.simbad.SimbadClass.query_hierarchy`
526618
- `~astroquery.simbad.SimbadClass.query_bibobj`
527619
- `~astroquery.simbad.SimbadClass.query_bibcode`
528620
- `~astroquery.simbad.SimbadClass.query_objectids`

0 commit comments

Comments
 (0)