Skip to content

omnilab/vizier

Omnilab VizieR: the CDS's library of published astronomical catalogues (Strasbourg) - its tables whose description holds a text, as datasets (id, catalogue, title, authors, rows), and the rows of a table around a point of the sky or an object, each column with its unit and description - through TAPVizieR in ADQL, no key needed.

Floor
Provider · Omnilab
Version
1.x
Conformance suite
not wired
Verified for real
Its README does not say
Licence
MIT
composer require omnilab/vizier:1.x-dev

VizieR, the CDS's library of published astronomical catalogues, through TAPVizieR (https://tapvizier.cds.unistra.fr/TAPVizieR/tap/, ADQL) for omnisense/omnilab: the catalogues' tables by text as datasets; outside the contract, the rows of a table around a point of the sky or an object, with their units. No key.

Installation

composer require omnilab/vizier
use Omnilab\Vizier\VizierSourceFactory;

$vizier = (new VizierSourceFactory($httpClient))->create([
    'mailto' => 'support@glitchr.io',  // optional, in the User-Agent
    'throttle' => 0,                   // optional: seconds between two calls
]);

With the Symfony bundle, the factory is registered as vizier when the bundle knows it; otherwise register VizierSourceFactory as a service (it is autoconfigured as a SourceFactoryInterface).

Credentials

None: TAPVizieR is open, no key, no account.

Options

The factory's options, with the defaults it applies (VizierSourceFactory):

Option Type Default Required Effect
mailto ?string null no a contact address, sent in the User-Agent
user_agent ?string null no the whole User-Agent, in place of the one made from mailto
throttle float 0 no seconds left between two calls: the CDS publishes no rate for TAP (each call took 1 to 6 s)
base_uri string https://tapvizier.cds.unistra.fr/TAPVizieR/tap/ no the TAP service (sync is joined to it): TAPVizieR, or a mirror

Capabilities

capabilities() answers datasets; an operation it does not answer throws NotSupportedException before any call.

Capability Supported
entity no a row of a catalogue is no entity: rows() and rowsOf(), outside the contract
search no
series no
products no a table is a dataset (datasets()), not a product over a place on the Earth
occurrences no
datasets yes VizieR's tables whose description holds every word of the text (each word in four cases), the largest first, 1,000 at most, one page (no OFFSET in its ADQL)
jobs no
open no
structure no
cross_references no
forecast no
historical no
area no the core's places are on the Earth: a cone on the sky is rows()'s
station no
cloud_cover no

Outside the contract, on Omnilab\Vizier\VizierSource: rows(string $table, float $ra, float $dec, float $radius = 5 / 3600, int $limit = 50): list<Row> (the rows of a table around a point of the sky, in degrees, the nearest first; 10° and 1,000 rows at most) and rowsOf(string $table, Entity $object, float $radius = 5 / 3600, int $limit = 50): list<Row> (around an entity with ra and dec properties in degrees: omnilab/simbad's, ned's, gaia's). A Row holds table, separation (arcseconds) and values (a Property by column), and value($column) gives one value.

Which API

TAPVizieR (https://tapvizier.cds.unistra.fr/TAPVizieR/tap/sync, ADQL, FORMAT=json): every VizieR table is a TAP table named by its VizieR name in double quotes ("I/239/hip_main"), and TAP_SCHEMA.tables / TAP_SCHEMA.columns describe them (description with the authors, number of rows; each column's unit, UCD and description). VizieR's ADQL has no OFFSET, no ILIKE, no LOWER() (each a 400, "Encountered ..."), which shapes what follows.

Method Reads ADQL
datasets() Query::$text, each word in the description (as written, lower case, capitalised, upper case), limit (1,000 at most) SELECT TOP n+1 table_name, description, nrows FROM TAP_SCHEMA.tables WHERE (description LIKE '%w%' OR ...) AND ... ORDER BY nrows DESC, table_name ASC
rows() outside the contract: a table's name, a right ascension and declination (degrees), a radius (degrees, 5" by default, 10° at most), limit (50; 1,000 at most) its main position columns from TAP_SCHEMA.columns (UCD pos.eq.ra;meta.main, pos.eq.dec;meta.main), then SELECT TOP n *, DISTANCE(...) AS "_separation" FROM "table" WHERE 1 = CONTAINS(...) ORDER BY "_separation" ASC
rowsOf() outside the contract: a table and an Entity with ra and dec properties in degrees (omnilab/simbad's, ned's, gaia's) as rows()

Datasets: a Product per table - id the table (I/239/hip_main), collection its catalogue (I/239), properties title, authors (the description's bracketed end) and rows, url its VizieR page; no footprint, period nor asset. VizieR ranks nothing: the largest tables first. One page: next is null, total known when fewer than the limit matched.

Rows (Row): table, separation from the point (arcsec), values by column, each a Property (key the column's name, label VizieR's description, value a Quantity when the column has a unit and the value is a number; a blank cell is no value). Why outside the contract: the core's places are on the Earth, and a row of a catalogue is no entity with an identifier of its own. A table without main position columns (or unknown) is a ProviderException saying so. A table's name is checked before any call (letters, digits, +_./-).

Epochs: a table's positions are at its own epoch. Barnard's star moves 10.4" a year: SIMBAD's J2000 position is 166" from Gaia DR3's (epoch J2016.0) - with the default 5", rowsOf() finds other stars; ask a radius that covers the motion.

What VizieR does not give here

The catalogues' ReadMe and full metadata, the ASU and cone-search interfaces, cross-matching (X-Match), the TAP tables of other services, the asynchronous queries, and pages past the first (no OFFSET).

Example

<?php

require __DIR__.'/vendor/autoload.php';

use Omnilab\Source\Query;
use Omnilab\Vizier\VizierSourceFactory;
use Symfony\Component\HttpClient\HttpClient;

$vizier = (new VizierSourceFactory(HttpClient::create()))->create(['mailto' => 'you@example.org']);

foreach ($vizier->datasets(new Query('hipparcos', limit: 3)) as $table) {
    printf("%s  %s rows  %s\n", $table->id, $table->properties['rows'] ?? '?', $table->properties['title'] ?? '');
}

// Sirius, at its J2000 position (degrees), within 30 arcseconds.
foreach ($vizier->rows('I/239/hip_main', 101.287155, -16.716116, 30 / 3600, 3) as $row) {
    printf("HIP %s at %.2f\": Vmag %s\n", $row->values['HIP']->value ?? '?', $row->separation, $row->values['Vmag']->value ?? '?');
}
I/337/tgas  2057050 rows  TGAS: Subset of GaiaSource comprising those stars in the Hipparcos and Tycho-2 Catalogues for which a full 5-parameter astrometric solution has been possible in Gaia Data Release (TgasSource)
J/A+A/657/A7/tablea2  175155 rows  CPM candidate companions for Hipparcos stars
I/239/hip_main  118218 rows  The Hipparcos Main Catalogue
HIP 32349 at 11.72": Vmag -1.44 mag

(as answered on 2026-10-27; Vmag is a Quantity, its unit mag)

# config/packages/omnilab.yaml
omnilab:
    sources:
        vizier: { factory: vizier, options: { mailto: '%env(OMNILAB_MAILTO)%' } }

The bundle injects it as SourceInterface $vizier; rows() and rowsOf() are methods of VizierSource, not of the interface: check the instance before calling them.

Errors

Every call goes through HttpSource: a 404 or a 410 (and a 204) is null - an empty page for a list -; a 429 is a RateLimitedException (retryAfter from Retry-After); a 5xx, a time-out or a host that does not answer an UnavailableException; any other 4xx a ProviderException with its status and what the service said; an answer that is not JSON a ProviderException, a web page answered for JSON an UnavailableException.

TAP's errors (a VOTable whose QUERY_STATUS is ERROR, with a 400) are read into the ProviderException's message (Encountered ... for ADQL VizieR does not take). Before any call: a table's name that is not VizieR's form (letters, digits, +_./-), a position outside the sky, a radius not above 0 or beyond 10°, an entity without ra and dec (\InvalidArgumentException). A table without main position columns, or unknown, is a ProviderException that says so.

Verified, and not

Against the real service on 2026-10-16: the tables naming Hipparcos (TGAS first, I/239/hip_main third, 5.1 s), "Gaia DR3 source" (I/350/gaiaedr3, I/355/gaiadr3...), a text no table holds; I/239/hip_main around Sirius (HIP 32349, V -1.44, 11.72" from the J2000 position, 0.8 s); I/355/gaiadr3 around SIMBAD's Barnard's star (within 30": three other sources; within 200": Gaia DR3 4472832130942575872 at 166.3"); ILIKE, LOWER() and OFFSET refused (400). The fixtures in Tests/Fixtures/ are its answers.

Again on 2026-10-27: the tables naming Hipparcos (the same three first) and Sirius in I/239/hip_main at 11.72" (the example above).

Not verified: a 429 or a 5xx (none met); a table whose main positions are not in degrees; a mirror (base_uri).

Responsibility

This package is an independent, unofficial client: it is not affiliated with, endorsed by or sponsored by the CDS (Université de Strasbourg / CNRS). The CDS asks that work using VizieR acknowledge it ("This research has made use of the VizieR catalogue access tool, CDS, Strasbourg, France (DOI: 10.26093/cds/vizier)"), and each catalogue is its authors' work, to be cited from its own reference, as the README's Terms of use says. Whoever runs the package is responsible for complying with them and for any use made of what it returns: see the README's Responsibility section.

Propose a change docs/index.md