scieee AI-readable full text Open interactive document viewer

Python resources for Virtual Observatory

Rizzo, J. Ricardo; Solano, Enrique; Murillo-Ojeda, Raquel

Abstract

This tutorial presents an introductory use of Python tools aimed to profit the Virtual Observatory (VO) capabilities. It is organised in a series of examples which demonstrate different functions and methods of PyVO, astroquery, and astroquery/Vizier to query, download and manipulate tables, spectra, and images. To run the tutorial, please use the link available at the end of this contribution (see Software, Repository URL below), which points to the last stable version in Google Colab. [Version 2]:Added description of IVOA protocols.Added search radius in example 1.Added a modification of plot 2 of example 4.Minor rewording. [Version 3]:Added description of TAP protocol.Added example 7.

Full text

Python resources for the Virtual Observatory. Some use cases of astroquery, Vizier and PyVO Authored by Ricardo Rizzo (ISDEFE, SVO collaborator) Revised/approved: Spanish Virtual Observatory Creation date: October 25, 2025 Updated: November 2025 This tutorial presents an introductory use of Python tools aimed at profiting from Virtual Observatory (VO) capabilities. The tutorial assumes that the user: •has sufficient knowledge of Python 3. •is able to manage python notebooks and Google colab platform. •has a general idea about Virtual Observatory (VO). •does not need previous knowledge about existing catalogs or other VO resources. 1 Overview: the tools The tutorial focuses on the use of the packages Vizier,astroquery, and PyVO. These packages are considered a bridge between the VO resources and the powerful and versatile Python language. It follows a “learning by doing” approach and is structured around a series of examples which make use of the necessary packages. Therefore, it is NOT a complete reference to all the capabilities of the packages, but a starting guide to use them. Some external references are included when appropriate. Why using VizieR •VizieR (https://vizier.cds.unistra.fr/) provides the most complete library of published astronomical catalogues (25800+ up to November 2025). •It is a “live” repository, with verified and enriched data from surveys, catalogs and tables. •Its content is accessible by multiple online interfaces and desktop applications. •There are a number of query tools that allow the user to select relevant data tables and to extract records matching a given criteria. •It has several mirrors worldwide. 1 •Most of its capabilities are integrated in astroquery.vizier, an affiliated package of astroquery. •Reference: https://astroquery.readthedocs.io/en/latest/vizier/vizier.html What about astroquery •astroquery is THE query tool in Python for astronomy. •It has an excellent management of tables in all their formats, particularly VOTables. •astroquery includes easy access to VizieR functionalities through its convenience functions, which are added to the astroquery.vizier methods and functions. •Reference: https://astroquery.readthedocs.io/en/latest/ When to go to PyVO •PyVO works as an “extension” of astropy (https://www.astropy.org), the most popular collection of software packages written in Python for use in astronomy. •PyVO includes several methods and functions to facilitate an easy access to the mostly commonly used VO protocols. •When used in combination with numpy, pandas, matplotlib, etc., other high functionalities are possible. •Reference: https://github.com/astropy/pyvo 2 Overview: the protocols The examples developed in this tutorial are based on a client-server structure. The communication between the tools (on the client side) and the servers (which provide access to data and metadata on the server side) is done through protocols. IVOA has defined a number of protocols which are necessary to successfully communicate servers and clients. In this tutorial we use some of the most popular VO protocols, briefly described below. Simple Cone Search protocol (SCS) This protocol is used to provide results from queries to one or more tables, based on a given position on the sky. The parameters needed are therefore very simple: position (RA, Dec) and a search radius (SR) in degrees around it. Some packages and utilities may include wrappers to use SCS with other parameters, like a search based on a source name. Reference: https://www.ivoa.net/documents/latest/ConeSearch.html Simple Image Access protocol (SIA) This protocol provides capabilities for the discovery, description, access, and retrieval of multidimensional image datasets, including 2D images as well as datacubes of three or more dimensions. 2 The queries using this protocols need, as a minimum, the same parameters as the SCS. In most cases, some of the fields in the metadata are also searchable, which adds flexibility (and complexity!) to the queries. The resulting VOTables are lists of metadata on images on the queried service/s. Reference: https://www.ivoa.net/documents/SIA/20151223/index.html Simple Spectral Access protocol (SSA) This protocol works very similar to SIA. Results from a query are VOTable whose metadata (some of them searchable) describe information about spectra which can be analised and downloaded. The resulting spectra from a SSA query are VOTables, which can later be converted to other formats. Reference: https://www.ivoa.net/documents/SSA/ Table Access Protocol (TAP) This protocol also allow the client to access tables and catalogs, but with more powerful capabilities in comparison with the precedent ones. Its robustness relies in highly flexible specifications to the query, which are derived from the implementation of ADQL (Astronomical Data Query Language). ADQL is a tailored query language based on SQL. The client can consult to the server adding more conditions to the query. In this way, the queries are faster than using the previous protocols because filtering is made on the server side. Reference: https://www.ivoa.net/documents/TAP/ 3 Install the necessary packages The packages needed for this tutorial are indicated in the file requirements.txt. [ ]: # Download the list of needed packages from the Zenodo tutorial !wget -qO requirements.txt https://zenodo.org/records/17533386/files/ ,→requirements.txt?download=1 # Load requirements.txt from owner's drive !pip install -r requirements.txt Requirement already satisfied: astropy in /usr/local/lib/python3.12/distpackages (from -r requirements.txt (line 1)) (7.1.1) Requirement already satisfied: astroquery in /usr/local/lib/python3.12/distpackages (from -r requirements.txt (line 2)) (0.4.11) Requirement already satisfied: pyvo in /usr/local/lib/python3.12/dist-packages (from -r requirements.txt (line 3)) (1.7.1) ... 3 Requirement already satisfied: pycparser in /usr/local/lib/python3.12/distpackages (from cffi>=1.12->cryptography>=2.0->SecretStorage>=3.2->keyring>=15.0->astroquery->-r requirements.txt (line 2)) (2.23) The following packages are necessary: astropy astroquery pyvo numpy pandas matplotlib rich requests Most likely, the colab session will only install pyvo and astroquery. The other packages are included here in order to guarantee a correct run in case of exporting the notebook to another environment. 4 Using VizieR in Python: a cone search Below we develop two examples which perform a Cone Search query to VizieR using astroquery. Packages, definitions, setups Before doing the queries, we import the necessary packages, define the target source and create the query instance. [ ]: # Import necessary modules from astroquery.vizier import Vizier from astropy.coordinates import SkyCoord import astropy.units as u from astropy.table import Table from rich import print [ ]: # Define the target (play and change to your favorite source) TARGET_NAME ="NGC2359" # a Wolf Rayet Nebula [ ]: # Clear the local cache Vizier.clear_cache() # Create a Vizier instance vizier =Vizier(columns=['*']) # Retrieve all columns [ ]: # Optionally, it is possible to select a mirror us_vizier =Vizier(vizier_server="vizier.cfa.harvard.edu/viz-bin/VizieR-2") # [[See help(Vizier) for other parameters]] 4 Warning! As it is specified, the instance vizier will do the query in all the catalogs (more than 20k!). This may take several seconds, or even minutes, to finish. The user may want to specify the catalogs to be searched by specifying them explicitly or by taking advantage of the CDS categories (for example, adding , catalog="II/*" to the instance in case of looking for photometry surveys). Example 1: query the available catalogs around the target Key function/method Description vizier.query_object Queries the service by internally resolving coordinates of the input source name. Returns a table object. In this example we query all the catalogs available in VizieR around a source, with a search radius of 5 arcsec. The search radius is an optional parameter, whose default is 2 arcmin. Note that radius should be an astropy Quantity. [ ]: # Do the query SEARCH_RADIUS =5*u.arcsec catalogs =vizier.query_object(TARGET_NAME, radius=SEARCH_RADIUS) print(f"Number of catalogs found around {TARGET_NAME}:{len(catalogs)}") Number of catalogs found around NGC2359: 10 [ ]: # Print the catalog codes and number of entries for catalog in catalogs: print(f"Catalog: {catalog.meta['name']}, Number of entries: {len(catalog)}") # [[Note the difference w.r.t. pyvo cases]] Catalog: I/353/gsc242, Number of entries: 1 Catalog: II/316/gps6, Number of entries: 1 Catalog: II/341/vphasp, Number of entries: 2 ... Catalog: VII/239A/icpos, Number of entries: 1 Catalog: J/MNRAS/497/672/tablea1, Number of entries: 1 Example 2: perform a Cone Search around the target coordinates 5 Key function/method Description vizier.query_region Queries the service in a region centered at some coordinates and with a given radius. Returns a table object. [ ]: # Do the query target_coords =SkyCoord.from_name(TARGET_NAME) SEARCH_RADIUS =5*u.arcsec catalogs =vizier.query_region(target_coords, radius=SEARCH_RADIUS) if catalogs != None: print(f"Number of catalogs found in Cone Search around {TARGET_NAME}:␣ ,→{len(catalogs)}") else: print(f"No catalogs found in Cone Search around {TARGET_NAME}.") Number of catalogs found in Cone Search around NGC2359: 10 [ ]: # Print the catalog codes and number of entries if catalogs != None: for catalog in catalogs: print(f"Catalog: {catalog.meta['name']}, Number of entries:␣ ,→{len(catalog)}") Catalog: I/353/gsc242, Number of entries: 1 Catalog: II/316/gps6, Number of entries: 1 Catalog: II/341/vphasp, Number of entries: 2 ... Catalog: J/MNRAS/497/672/tablea1, Number of entries: 1 5 More VizieR: Query keywords, table downloading and writing In this example we do a query by specifying descriptors which will match the keywords provided for the catalogs. Additionally, we select, download and inspect one of the catalogs. Finally the catalog is written as a VOTable. Example 3: work with catalogs containing data related to pulsar rotation Key function/method Description vizier.find_catalogs Queries all catalogs matching some keywords. 6 Key function/method Description vizier.get_catalogs Retrieves a list of catalogs. table.write Writes a table in several formats. Find all the catalogues matching the keywords. [ ]: # Clear the local cache Vizier.clear_cache() # Create a Vizier instance vizier =Vizier(columns=['*']) # Retrieve all columns # Search for catalogs related to pulsar rotation SEARCH_TERM ='pulsars rotation'# Play and change it by your favourite topic! pulsar_list =vizier.find_catalogs(SEARCH_TERM) # Number of catalogs found print("-------------------------------------------------------------") print(f"Searching for catalogs with term: '{SEARCH_TERM}'") print(f"Number of catalogs found: {len(pulsar_list)}") ------------------------------------------------------------- Searching for catalogs with term: 'pulsars rotation' Number of catalogs found: 9 [ ]: # Print catalog codes and their descriptions print("-------------------------------------------------------------") for k,v in pulsar_list.items(): print(k, ":", v.description) ------------------------------------------------------------- J/ApJ/642/868 : Rotation measures for 223 pulsars (Han+, 2006) J/ApJS/234/11 : Pulsar rotation measures (Han+, 2018) J/A+A/684/A208 : Unassociated Fermi-LAT sources seen with eROSITA (Mayer+, 2024) J/MNRAS/386/1881 : New pulsar rotation measures (Noutsos+, 2008) J/MNRAS/414/1679 :315 glitches in the rotation of 102 pulsars (Espinoza+, 2011) J/MNRAS/484/3646 : LOFAR PSR low-frequency Faraday rotation measures (Sobey+ 2019) 7 J/MNRAS/496/2836 : Faraday rotation of Northern hemisphere pulsars (Ng+, 2020) J/MNRAS/511/4669 : Study of galactic PSRs mainly from ATNF (Kutukcu+, 2022) J/AZh/88/22 : Angles rotation/magnetic moment in pulsars (Malov+, 2011) Select one of the catalogs and inspect their tables. [ ]: # Get a specific catalog by its code CATALOG_CODE ='J/A+A/684/A208' # Limit the number of rows to retrieve (optional) vizier.ROW_LIMIT =1# Set to the minimum for faster retrieval # Retrieve the catalog catalog =vizier.get_catalogs(CATALOG_CODE) # Retrieving may take some time print("-------------------------------------------------------------") print(f"Retrieving catalog: {CATALOG_CODE}") print(f"Number of tables in catalog: {len(catalog)}") print(catalog) ------------------------------------------------------------- Retrieving catalog: J/A+A/684/A208 Number of tables in catalog: 3 TableList with 3tables: '0:J/A+A/684/A208/catalog'with 15 column(s) and 1row(s) '1:J/A+A/684/A208/cand'with 14 column(s) and 1row(s) '2:J/A+A/684/A208/prior'with 26 column(s) and 1row(s) Work with the first table of the catalog. [ ]: # Access to the first table (still with just one row) table =catalog[0] print("-------------------------------------------------------------") print(f"First table in catalog '{CATALOG_CODE}':") print(table) First table in catalog 'J/A+A/684/A208': plots Seq 4eRASS ... logFluxratio Pi ... ----- --- ----------------------- ... ------------------- -------------------- plots 14eRASS J154415.4-255531 ... 2.4337511062622070 0.75450614218880074 8 [ ]: # Access to the columns print("-------------------------------------------------------------") print("Columns in the table:") print(table.colnames) # Select a specific column and print its description print("-------------------------------------------------------------") print(f"Description of column 'FeRASS':\n{table['FeRASS'].description}") Columns in the table: [ 'plots', 'Seq', '4eRASS', 'RA_ICRS', 'DE_ICRS', 'CR', 'FeRASS', 'Name4FGL', 'Class2-4FGL', 'Assoc2-4FGL', 'RA4deg', 'DE4deg', 'F4FGL', 'logFluxratio', 'Pi' ] ------------------------------------------------------------- Description of column 'FeRASS': X-ray source flux in 0.2-2.3keV, determined via an energy conversion factor from␣ ,→the count rate Access to the data in the table (download and write as VOTable). [ ]: # Access to data (all rows) vizier.ROW_LIMIT =-1# All available rows #OPTION vizier.TIMEOUT = 500 # Increase timeout to 500 sec catalog =vizier.get_catalogs(CATALOG_CODE) table =catalog[0] print("-------------------------------------------------------------") print(f"Number of rows: {len(table)}") print(f"Data in selected table, from catalog {CATALOG_CODE}:") print(table) 9 Field/Param: has_xp_continuous Field/Param: has_xp_sampled Field/Param: has_rvs Field/Param: has_epoch_photometry Field/Param: has_epoch_rv Field/Param: has_mcmc_gspphot Field/Param: has_mcmc_msc Field/Param: in_andromeda_survey Field/Param: classprob_dsc_combmod_quasar Field/Param: classprob_dsc_combmod_galaxy Field/Param: classprob_dsc_combmod_star Field/Param: teff_gspphot Field/Param: teff_gspphot_lower Field/Param: teff_gspphot_upper Field/Param: logg_gspphot Field/Param: logg_gspphot_lower Field/Param: logg_gspphot_upper Field/Param: mh_gspphot Field/Param: mh_gspphot_lower Field/Param: mh_gspphot_upper Field/Param: distance_gspphot Field/Param: distance_gspphot_lower Field/Param: distance_gspphot_upper Field/Param: azero_gspphot Field/Param: azero_gspphot_lower Field/Param: azero_gspphot_upper Field/Param: ag_gspphot Field/Param: ag_gspphot_lower Field/Param: ag_gspphot_upper Field/Param: ebpminrp_gspphot Field/Param: ebpminrp_gspphot_lower Field/Param: ebpminrp_gspphot_upper Field/Param: libname_gspphot Field/Param: standardID Field/Param: accessURL Field/Param: contentType Save results as a VOTable. [ ]: # Save results to a local VOTable file results_vot.to_xml('pleiades_gaia_dr3.vot') Create a color-magnitude diagram. [ ]: # Convert to pandas DataFrame df =results.to_table().to_pandas() print(df.head()) 16 # Plot 1: G magnitude vs. BP-RP color plt.figure(figsize=(8,6)) plt.scatter(df['bp_rp'], df['phot_g_mean_mag'], s=1, color='blue') plt.gca().invert_yaxis() plt.xlabel('BP - RP [mag]') plt.ylabel('G mag [mag]') plt.title('Color-Magnitude Diagram of Pleiades (Gaia DR3). N={}'.format(len(df))) plt.savefig('pleiades_cmd.pdf', dpi=300) plt.show() dist solution_id designation \ 0 0.002947 1636148068921376768 Gaia DR3 65227221648967296 1 0.008958 1636148068921376768 Gaia DR3 65227260303936512 2 0.011807 1636148068921376768 Gaia DR3 65227157224754432 3 0.013289 1636148068921376768 Gaia DR3 65230215242484864 4 0.015192 1636148068921376768 Gaia DR3 65226847988213888 source_id random_index ref_epoch ra ra_error dec \ 0 65227221648967296 87069915 2016.0 56.656459 0.268152 24.180456 1 65227260303936512 152453615 2016.0 56.668142 1.000549 24.178471 2 65227157224754432 145211448 2016.0 56.666441 1.146572 24.168853 3 65230215242484864 678434333 2016.0 56.659998 0.024227 24.191257 4 65226847988213888 384685837 2016.0 56.649300 0.032684 24.165293 dec_error ... azero_gspphot azero_gspphot_lower azero_gspphot_upper \ 0 0.199597 ... 0.0567 0.0160 0.1224 1 0.794606 ... NaN NaN NaN 2 0.970956 ... NaN NaN NaN 3 0.018089 ... 0.3902 0.3662 0.4337 4 0.024651 ... 0.8923 0.8678 0.9150 ag_gspphot ag_gspphot_lower ag_gspphot_upper ebpminrp_gspphot \ 0 0.0373 0.0105 0.0808 0.0225 1NaN NaN NaN NaN 2NaN NaN NaN NaN 3 0.3225 0.3023 0.3591 0.1756 4 0.7036 0.6835 0.7219 0.3785 ebpminrp_gspphot_lower ebpminrp_gspphot_upper libname_gspphot 0 0.0063 0.0485 PHOENIX 1NaN NaN 2NaN NaN 3 0.1647 0.1955 MARCS 4 0.3677 0.3883 MARCS [5rows x 153 columns] 17 Plot proper motion vectors in the sky. [ ]: # Plot 2: Proper motion vector field (using quiver with scaling) plt.figure(figsize=(8,8)) scale_factor = 3e-4 # Adjust this factor to scale the arrows plt.quiver(df['ra'], df['dec'], df['pmra']*scale_factor,␣ ,→df['pmdec']*scale_factor, angles='xy', scale_units='xy', scale=1, color='red',␣ ,→width=0.002) plt.xlabel('RA [deg]') plt.ylabel('Dec [deg]') plt.gca().invert_xaxis() plt.title('Proper Motion Vector Field of Pleiades (Gaia DR3). N={}'. ,→format(len(df))) plt.axis('equal') plt.show() 18 Now we do a zoom around the center of Pleiades to better distinguish the vectors. [ ]: # Plot 2 repeated: a zoom around the center of Pleiades fig, ax =plt.subplots(figsize=(8,8)) scale_factor = 3e-4 # Scale factor for proper motion arrows # Conversion: 1 arcmin = 1/60 deg half_size_deg =(10 / 60)/ 2 # 10 arcmin total → ±5 arcmin around the center # Plot the proper motion vectors ax.quiver(df['ra'], df['dec'], df['pmra']*scale_factor, df['pmdec']*scale_factor, angles='xy', scale_units='xy', scale=1, color='red', width=0.002) # Labels and title ax.set_xlabel('RA [deg]') ax.set_ylabel('Dec [deg]') 19 ax.set_title(f'Proper Motion Vector Field of Pleiades (10×10 arcmin).␣ ,→N={len(df)}') # Define the field center ra_center =pleiades_coord.ra.degree dec_center =pleiades_coord.dec.degree # Set axis limits (RA decreases to the right, Dec increases upward) ax.set_xlim(ra_center +half_size_deg, ra_center -half_size_deg) # RA inverted ax.set_ylim(dec_center -half_size_deg, dec_center +half_size_deg) # Keep the aspect ratio square (so the field is not distorted) ax.set_aspect('equal') plt.show() Apparently, there is a number of stars with higher proper motions towards the southeast. Let’s see a diagram of proper motion values in RA, Dec. 20 Note the group of points around (20, -45) mas/yr. [ ]: # Plot 3: Proper motions in RA vs. Dec plt.figure(figsize=(8,6)) plt.scatter(df['pmra'], df['pmdec'], s=1, color='blue') plt.xlabel('pmra [mas/yr]') plt.ylabel('pmdec [mas/yr]') plt.xlim(-50,70) plt.ylim(-80,40) plt.title('Proper Motions of Pleiades (Gaia DR3). N={}'.format(len(df))) plt.grid() ax =plt.gca() ax.set_aspect('equal', adjustable='box') plt.savefig('pleiades_pm.pdf', dpi=300) plt.show() 21 Create a group of comoving sources around (20, -45) mas/yr. [ ]: # Select comoving sources pm_mask =( (df['pmra']> 15)& (df['pmra']< 25)& (df['pmdec']> -50)& (df['pmdec']< -40) ) comoving =df[pm_mask] print(f"Number of comoving sources: {len(comoving)}") print(comoving[['source_id','ra','dec','pmra','pmdec','phot_g_mean_mag']]) Number of comoving sources: 518 source_id ra dec pmra pmdec \ 54 65229734206151680 56.605873 24.159919 16.920656 -48.014607 106 65226401312036864 56.630027 24.117141 18.379559 -45.251956 108 66728265476081408 56.730558 24.187798 20.392591 -44.020638 139 65225611037551360 56.663990 24.102995 20.529235 -46.941873 143 65226092073881856 56.724916 24.132371 19.309048 -44.323806 ... ... ... ... ... ... 24138 64960628735279232 56.710656 23.239000 18.082485 -40.430134 24140 65089336018173440 55.907301 23.535788 19.527245 -45.292407 24242 69846274292973056 56.525940 25.112610 16.415834 -40.418413 24261 68317746971945600 55.790707 24.692314 23.192645 -48.135359 24514 66847321969262464 56.813762 25.115240 20.304699 -41.129036 phot_g_mean_mag 54 14.781489 106 13.536427 108 17.192360 139 9.199545 143 17.303078 ... ... 24138 9.784797 24140 10.576324 24242 15.486157 24261 15.114011 24514 15.495056 [518 rows x 6columns] Repeat the color-magnitude diagram for the whole sample and the comoving stars. 22 [ ]: # Repeat plot 1 (a CMD) for df and comoving sources plt.figure(figsize=(8,6)) plt.scatter(df['bp_rp'], df['phot_g_mean_mag'], s=1, color='blue', label="All␣ ,→sources") plt.scatter(comoving['bp_rp'], comoving['phot_g_mean_mag'], s=1, color='red',␣ ,→label="Comoving sources") plt.gca().invert_yaxis() plt.xlabel('BP - RP [mag]') plt.ylabel('G mag [mag]') plt.title('Color-Magnitude Diagram of Comoving Sources in Pleiades (Gaia DR3).␣ ,→N={}'.format(len(comoving))) plt.savefig('pleiades_comoving_cmd.pdf', dpi=300) plt.show() 7 More PyVO: access to images In this example we use pyVO to perform a Simple Image Access (SIA) query. Example 5: search and download an image 23 Key function/method Description pyvo.regsearch Do a query consulting the VO Registry. pyvo.dat.SIAService Definition of a Simple Image Access query Import the necessary packages [ ]: # Import packages import pyvo from pyvo.dal.sia import SIAResults import requests import pandas as pd import matplotlib.pyplot as plt import numpy as np from astropy.table import Table from astropy import units as u from astropy.coordinates import SkyCoord, get_icrs_coordinates from astropy.io import fits from astropy.wcs import WCS from astropy.io.votable import from_table import subprocess Define a function to plot an image from a 2D FITS file. [ ]: # Function to plot a 2D FITS image def plot_FITS(filename): from pathlib import Path from astropy.io import fits from astropy.wcs import WCS import matplotlib.pyplot as plt from astropy.visualization import ZScaleInterval, ImageNormalize # Open FITS file hdu =fits.open(filename)[0] data =hdu.data header =hdu.header # Get WCS information wcs =WCS(header) # Compute zscale normalization (automatic intensity range) interval =ZScaleInterval() vmin, vmax =interval.get_limits(data) norm =ImageNormalize(vmin=vmin, vmax=vmax) 24 # Plot the FITS image with WCS projection fig =plt.figure() ax =fig.add_subplot(111, projection=wcs) im =ax.imshow(data, origin='lower', cmap='viridis', norm=norm) # Add coordinate grid and labels #ax.coords.grid(color='cyan', ls='--', lw=0.7, alpha=0.7) ax.set_xlabel('Right Ascension') ax.set_ylabel('Declination') # Custom formatter for the bottom-right status bar def format_coord(x, y): try: ra, dec =wcs.wcs_pix2world([[x, y]], 0)[0] return f"RA={ra:.6f}°Dec={dec:.6f}°Pixel=({x:.1f},{y:.1f})" except Exception: return f"Pixel=({x:.1f},{y:.1f})" ax.format_coord =format_coord plt.colorbar(im, ax=ax, orientation='vertical', label='Intensity') plt.title(Path(filename).name) plt.savefig(filename.replace('fits','pdf')) print(f"Image saved to {filename.replace('fits','pdf')}") plt.show() Look for available services in the VO Registry. [ ]: # It is possible to look for all the available (i.e. registered) SIA services services =pyvo.regsearch(servicetype='sia', keywords=['image']) print(f"Number of SIA services found: {len(services)}") Number of SIA services found: 240 [ ]: # Too many results. Let's refine the search to the collections hosted by NOIRLab␣ ,→Data Lab services =pyvo.regsearch(servicetype='sia', keywords=['noirlab']) print(f"Number of SIA services found for NOIRLab: {len(services)}") Number of SIA services found for NOIRLab: 64 [ ]: for pos, row in enumerate(services.to_table()): print(f"\nService {pos}:{row['res_title']}") if 'access_urls'in row.colnames: print(f"URL: {row['access_urls']}") 25 print(f"Access URLs: {s['access_urls']}") except KeyError: print("No access_urls found.") pos += 1 Service 0: LAMOST DR1 survey spectra Access URLs: ['http://vos2.asu.cas.cz/lamost_dr1/q/ssa/ssap.xml?'] Service 1: LAMOST DR2 survey spectra Access URLs: ['http://vos2.asu.cas.cz/lamost_dr2/q/ssa/ssap.xml?'] Service 2: LAMOST DR3 survey spectra Access URLs: ['http://vos2.asu.cas.cz/lamost_dr3/q/ssa/ssap.xml?'] Service 3: LAMOST DR1 SPECTRUM CATALOG SERVICE Access URLs: ['http://dr1.lamost.org/voservice/ssap?'] Service 4: LAMOST DR5 survey spectra Access URLs: ['http://dc.g-vo.org/lamost5/q/ssa/ssap.xml?'] Service 5: LAMOST DR6 Low Resolution Spectra Access URLs: ['http://dc.g-vo.org/lamost6/q/svc_lrs/ssap.xml?'] Service 6: LAMOST DR6 Medium Resolution Spectra Access URLs: ['http://dc.g-vo.org/lamost6/q/svc_mrs/ssap.xml?'] Select the last service (LAMOST DR6) [ ]: # Select the last service (LAMOST DR6) lamost_dr6 =ssa_lamost_services[-1] print(f"Selected LAMOST DR6 MRS service title: {lamost_dr6['res_title']}") for col,val in lamost_dr6.items(): print(f"{col}:{val}") Selected LAMOST DR6 MRS service title: LAMOST DR6 Medium Resolution Spectra ivoid: ivo://org.gavo.dc/lamost6/q/svc_mrs res_type: vs:catalogservice 32 short_name: LM6 MRS SSAP res_title: LAMOST DR6 Medium Resolution Spectra content_level: research res_description: A machine-readable interface to the LAMOST6 medium resolution␣ ,→spectra based on SSAP and datalink. reference_url: http://dc.g-vo.org/lamost6/q/svc_mrs/info creator_seq: National Astronomical Observatories of the Chinese Academy of Sciences created: 2020-10-16T10:16:41 updated: 2024-12-27T08:31:05 rights: Guoshoujing Telescope (the Large Sky Area Multi-Object Fiber Spectroscopic Telescope LAMOST) is a National Major Scientific Project built by the Chinese Academy of Sciences. Funding for the project has been provided by the National Development and Reform Commission. LAMOST is operated and managed by the National Astronomical Observatories, Chinese Academy of Sciences. Please also see the `LAMOST Data Policy`_. .. _LAMOST Data Policy: http://dr.lamost.org/ucenter/doc/lssdp content_type: survey source_format: source_value: http://dr6.lamost.org/v2/ region_of_regard: nan waveband: optical access_urls: ['http://dc.g-vo.org/lamost6/q/svc_mrs/ssap.xml?'] standard_ids: ['ivo://ivoa.net/std/ssa'] intf_types: ['vs:paramhttp'] intf_roles: ['std'] cap_descriptions: [] 33 Build the service and do the query [ ]: # Get the URL from the previous request # query_url = lamost_dr6['access_urls'] query_url =lamost_dr6['access_urls'][0]# Extract the first URL from the list print(f"Query URL: {query_url}") # Build the service object lamost_service =pyvo.dal.SSAService(query_url) print(f"SSAService object created: {lamost_service}") # Performing the query spectra =lamost_service.search(pos=(180.0,0.0), diameter=3.) # Alternatively, try with a source name #pos = SkyCoord.from_name('VY CMa') #spectra = lamost_service.search(pos=pos, diameter=3.) print(f"Number of results from LAMOST DR6 MRS query: {len(spectra)}") Query URL: http://dc.g-vo.org/lamost6/q/svc_mrs/ssap.xml? SSAService object created: SSAService(baseurl : 'http://dc.g-vo.org/lamost6/q/ ,→svc_mrs/ssap.xml?', description : 'None') Number of results from LAMOST DR6 MRS query: 1522 Manipulate the results and save the table [ ]: # Convert results to an Astropy Table spectra_table =spectra.to_table() print(spectra_table) # Convert to VOTable and write to file spectra_vot =spectra.votable spectra_vot.to_xml('lamost_dr6_spectra.vot') print("\n-------------------------------------------------------------") print("Results saved to lamost_dr6_spectra.vot") ssa_score ... preview ... --------- ... ----------------------------------------------------------------- 0.0 ... http://dc.g-vo.org/getproduct/lamost6/mrs/638701196B?preview=True 0.0 ... http://dc.g-vo.org/getproduct/lamost6/mrs/638701196R?preview=True 0.0 ... http://dc.g-vo.org/getproduct/lamost6/mrs/638701193B?preview=True 34 ... ... ... 0.0 ... http://dc.g-vo.org/getproduct/lamost6/mrs/653107150R?preview=True 0.0 ... http://dc.g-vo.org/getproduct/lamost6/mrs/626207139B?preview=True 0.0 ... http://dc.g-vo.org/getproduct/lamost6/mrs/626207139R?preview=True Length = 1522 rows ------------------------------------------------------------- Results saved to lamost_dr6_spectra.vot Select and save one spectrum from the results. [ ]: # Select one spectrum and download data k= 690 # Index of the spectrum to download spectrum =spectra[k] spectrum_url =spectrum.getdataurl() print(f"Spectrum data URL: {spectrum_url}") response =requests.get(spectrum_url) # response is natively a VOTable file with open('lamost_spectrum.vot','wb')as f: f.write(response.content) print("\n-------------------------------------------------------------") print("Spectrum saved to lamost_spectrum.vot") Spectrum data URL: http://dc.g-vo.org/getproduct/lamost6/mrs/626205246B Spectrum saved to lamost_spectrum.vot Load the spectrum, plot it and save the figure. [ ]: # Load the spectrum and plot it spectrum_table =Table.read('lamost_spectrum.vot') plt.plot(spectrum_table['spectral'], spectrum_table['flux']) plt.xlabel('Wavelength (Angstrom)') plt.ylabel('Flux (counts)') plt.title('LAMOST DR6 MRS Spectrum') plt.savefig('lamost_spectrum.pdf', dpi=300) print("\n-------------------------------------------------------------") print("Spectrum saved to lamost_spectrum.pdf") plt.show() ------------------------------------------------------------- Spectrum saved to lamost_spectrum.pdf 35 9 A sophisticated query using TAP In this example we query tha Gaia DR3 dataset to download those records matching several conditions, instead of filtering a large table after downloading it. Example 7: Tailored query of the Gaia DR3 archive. Key function/method Description pyvo.dal.TAPService Class to instantiate a TAP service and include its methods and functions. Reference: this example is adapted from the Mas-Buitrago’s et al. (2024) tutorial in zenodo. Import the necessary packages [ ]: import pyvo import matplotlib.pyplot as plt 36 from rich import print from astropy import units as u from astropy.coordinates import SkyCoord Create the instance to the Gaia TAP service [ ]: tap_gaia =pyvo.dal.TAPService("https://gea.esac.esa.int/tap-server/tap") Start with a generic query [ ]: # Define a query to the Gaia endpoint to get information about the available␣ ,→tables (schemas, types) query =''' SELECT DISTINCT schema_name, table_type FROM tap_schema.tables ''' # Execute the query and get the results as an Astropy Table gaia_info =tap_gaia.search(query).to_table() print(gaia_info) schema_name table_type ----------- ---------- external table gaiadr1 table gaiadr2 table gaiadr3 table gaiaedr3 table gaiafpr table public table tap_config table tap_schema table Define a query to the selected Gaia endpoint to get information about tables in the ‘gaiadr3’ schema related to ‘source’ [ ]: query =''' SELECT table_name, schema_name, size, table_type FROM tap_schema.tables WHERE schema_name = 'gaiadr3' AND table_name LIKE '%source%' AND table_type = 'table' ''' Execute the query and get the results as an Astropy Table 37 [ ]: gaia_info =tap_gaia.search(query).to_table() print(gaia_info) table_name schema_name size table_type -------------------------------- ----------- ---------- ---------- gaiadr3.gaia_source_simulation gaiadr3 1644237695 table gaiadr3.frame_rotator_source gaiadr3 429249 table gaiadr3.gaia_source_lite gaiadr3 1811709771 table gaiadr3.alerts_mixedin_sourceids gaiadr3 94 table gaiadr3.sso_source gaiadr3 158152 table gaiadr3.gaia_source gaiadr3 1811709771 table Define and execute a query to get data from the gaiadr3.gaia_source table. Coordinates selected correspond to the open cluster M67. Note the conditions applied to the query. [ ]: # query for M67 OBJECT_NAME ='M67' SEARCH_RADIUS = 0.5*u.deg coords =SkyCoord.from_name(OBJECT_NAME) print(f"\nCoordinates of {OBJECT_NAME}: RA={coords.ra.deg}deg, Dec={coords.dec. ,→deg}deg") query =''' SELECT TOP 5000 * FROM gaiadr3.gaia_source WHERE parallax_over_error > 10 AND ABS(pmra_error/pmra) < 0.1 AND ABS(pmdec_error/pmdec) < 0.1 AND pmra < -10.44 AND pmra > -11.44 AND pmdec < -2.39 AND pmdec > -3.39 AND 1=CONTAINS( POINT(ra,dec), CIRCLE(132.848,11.815,0.5)) ''' # Execute the query and get the results as an Astropy Table gaia_data =tap_gaia.search(query).to_table() print(gaia_data) Coordinates of M67: RA=132.848 deg, Dec=11.815 deg solution_id designation ... libname_gspphot ... ------------------- --------------------------- ... --------------- 1636148068921376768 Gaia DR3 598695763635752448 ... MARCS 38 1636148068921376768 Gaia DR3 598696077168308992 ... MARCS 1636148068921376768 Gaia DR3 598697142320464384 ... MARCS ... ... ... ... 1636148068921376768 Gaia DR3 605005418486272896 ... MARCS 1636148068921376768 Gaia DR3 605005620348847488 ... PHOENIX 1636148068921376768 Gaia DR3 605013767903055616 ... MARCS 1636148068921376768 Gaia DR3 605015309794935552 ... Length = 824 rows Plot a color-magnitude diagram and save data [ ]: # Plot a CMD using matplotlib plt.figure(figsize=(8,6)) plt.scatter(gaia_data['bp_rp'], gaia_data['phot_g_mean_mag'], s=1, color='blue') plt.gca().invert_yaxis() # Invert y-axis for magnitude plt.xlabel('BP - RP [mag]') plt.ylabel('G mean magnitude [mag]') plt.title(f"CMD of {OBJECT_NAME}from Gaia DR3. Search radius: {SEARCH_RADIUS. ,→to(u.arcmin)}") plt.savefig('cmd_gaia_m67.pdf', dpi=300) plt.show() # Save data to a VOTable file gaia_data.write('gaia_m67.vot',format='votable', overwrite=True) print("\n-------------------------------------") print("Gaia data saved to gaia_m67.vot") 39 ------------------------------------- Gaia data saved to gaia_m67.vot 40