Formats

Six ways to get the same numbers. Every format carries the served values exactly, so a CSV file and a binary download agree to the last bit.

API 3.1 · stars release 55c97bc0c218a6c5 · SS release ef7caf53a399bc1f

Choosing a format#

Set format= on a light-curve request. The default is the binary sp3c, which the website uses.

formatBest forContent-Type
csvSpreadsheets, pandas, a quick looktext/csv
ecsvastropy tables with units and typestext/plain
jsonScripts in any languageapplication/json
parquetLarge analyses, pandas and pyarrow, archivingapplication/vnd.apache.parquet
arrowZero-copy loading into Arrow toolsapplication/vnd.apache.arrow.stream
sp3cBrowsers and the fastest transfer; read it with 20 lines of codeapplication/vnd.asassn.sp3c

The batch stream sp3f is Coming soon.

Exact values#

The server decodes each light curve once and writes the same arrays into every format:

The magnitudes are not rounded for display. A value such as 14.036707 is the float32 the decoder produced, not a measurement good to a micromagnitude; use mag_err for the uncertainty.

CSV#

A CSV file starts with # key: value comment lines that describe the light curve, followed by a header row and one row per measurement:

Comment keyContent
formatSP3-CSV 1
kind, datasetstar_lc and stars, or ss_lc and ss
release_id, serve_idThe release and serving identity (versioned URLs)
nNumber of rows
objectThe star or object record (JSON)
selectionEvery parameter that shaped the curve, defaults included (JSON)
curvesStars: the curves (field, band, aperture) with their row ranges (JSON)
summaryRow counts (JSON). Stars: decoded, returned, excluded and detected. Solar system: rows, detections and recommended rows, in total and per band.
provenanceDecoder identity, quality-flag version, error model, sidecar checksum and server version (JSON)
citationWhat to cite
unitsUnit of each column (JSON)

A star CSV also expands the curve column into field, band and aperture columns, so each row stands alone.

Python
import pandas as pd

lc = pd.read_csv("https://asassn-v3.ifa.hawaii.edu/v3/stars/11010359/lc?format=csv", comment="#")
print(lc.dtypes)

Spreadsheet programs show the comment lines as text rows above the table. To read the values as float32 exactly, pass dtype={"mag": "float32", ...} to pandas.

ECSV#

ECSV 1.0 adds a YAML header with each column's data type, unit and description, and the light-curve header as table metadata. astropy reads it directly:

Python
from astropy.table import Table

t = Table.read("https://asassn-v3.ifa.hawaii.edu/v3/stars/11010359/lc?format=ecsv", format="ascii.ecsv")
print(t["mag"].dtype, t["mag"].unit, t.meta["release_id"])

JSON#

JSON holds the header and one array per column:

JSON
{"header": {"format": "SP3-JSON", "version": 1, "kind": "star_lc", "release_id": "…", "serve_id": "…",
            "object": {…}, "selection": {…}, "curves": […], "n": 3,
            "columns": [{"name": "bjd", "type": "f8", "unit": "d", "desc": "BJD_TDB at mid-exposure"}, …]},
 "columns": {"bjd": [2456024.1328295474, …], "mag": [14.036707, …], "camera": ["bA", …],
             "field": ["F0845+18_0428", …], …}}

Coded columns are already strings, and a star's curve is expanded into field, band and aperture, as in CSV. Column-wise arrays are much smaller than a list of row objects. JSON.parse returns float64 numbers; Math.fround(x) (or numpy.float32(x)) recovers the exact value of a 32-bit column.

Parquet and Arrow#

Parquet files have one row group, zstd level 1 compression and dictionary-encoded strings. Arrow responses are an IPC stream with uncompressed buffers. Both keep the binary types of the column tables, store each column's unit and description in its field metadata, and carry the full light-curve header as JSON in the schema metadata key sp3:

Python
import io, json, urllib.request
import pyarrow.parquet as pq

url = "https://asassn-v3.ifa.hawaii.edu/v3/stars/11010359/lc?format=parquet"
table = pq.read_table(io.BytesIO(urllib.request.urlopen(url).read()))
header = json.loads(table.schema.metadata[b"sp3"])
df = table.to_pandas()
print(header["release_id"], len(df))

SP3C: the binary light-curve format#

SP3C v1 holds one light curve as a JSON header followed by one binary array per column. A browser or NumPy can use most columns in place, without copying or parsing. A typical star light curve takes about 18 bytes per row after brotli compression; the same row in CSV is about 100 characters before compression.

Offsets are from the start of the payload. Columns begin at byte 8 + H, which is a multiple of 8.

OffsetSizeContent
04Magic bytes SP3C
44H, the header length, unsigned 32-bit little-endian
8HHeader: UTF-8 JSON, padded with spaces so that (8 + H) % 8 == 0
8 + H…Columns. Column k starts at 8 + H + columns[k].offset (a multiple of 8) and is zero-padded to a multiple of 8 bytes.

Header#

KeyContent
format, version"SP3C" and 1
kind, datasetstar_lc / stars or ss_lc / ss
release_id, serve_idRelease and serving identity
nNumber of rows
objectStar or object record
selectionEvery parameter that shaped the curve, defaults included
columnsColumn descriptors, in order (below)
dictionariesString tables for coded columns, e.g. {"camera": ["bA", "bC", …]}
provenancedecoder, quality_version, sidecars_sha256, server_version
citationWhat to cite
curvesStars only: [{entry_id, field, band, aperture, start, count}], the row range of each curve
summaryRow counts. Stars: {n_decoded, n_returned, n_excluded, n_detected}. Solar system: {n_rows, n_detected, n_recommended, bands}.

Readers must ignore header keys they do not know; new keys can appear without a version change.

Columns#

Each descriptor is {name, type, offset, nbytes, shuffled, dict?, unit?, desc?}. The types are:

typeValuesNumPy / JavaScript
f8, f464- and 32-bit IEEE floats<f8, <f4 / Float64Array, Float32Array
i4, u432-bit integers<i4, <u4 / Int32Array, Uint32Array
i2, u216-bit integers<i2, <u2 / Int16Array, Uint16Array
u1bytesu1 / Uint8Array
strUTF-8 stringsu4 offsets[n+1], then the bytes

Reader rules#

Readers#

These readers are complete and tested against the golden files in the server's test suite. Save one and use it as it stands.

"""Minimal SP3C v1 reader (numpy only). Sky Patrol V3 docs: /docs/formats.html"""
import json
import struct

import numpy as np

TYPES = {"f8": "<f8", "f4": "<f4", "i4": "<i4", "u4": "<u4", "i2": "<i2", "u2": "<u2", "u1": "u1"}


def read_sp3c(buf):
    """Return (header, columns) from one SP3C payload; str columns become lists of str."""
    buf = memoryview(buf).cast("B")
    if bytes(buf[:4]) != b"SP3C":
        raise ValueError("not an SP3C payload")
    (hlen,) = struct.unpack_from("<I", buf, 4)
    header = json.loads(bytes(buf[8:8 + hlen]))
    if int(str(header["version"]).split(".")[0]) != 1:
        raise ValueError("unsupported SP3C version %r" % header["version"])
    base, n, cols = 8 + hlen, header["n"], {}
    for c in header["columns"]:
        if c["offset"] % 8:
            raise ValueError("misaligned column %r" % c["name"])
        at = base + c["offset"]
        if c["type"] == "str":  # u4 offsets[n+1], then UTF-8 bytes
            offs = np.frombuffer(buf, "<u4", n + 1, at).tolist()
            blob = bytes(buf[at + 4 * (n + 1):at + c["nbytes"]])
            cols[c["name"]] = [blob[offs[i]:offs[i + 1]].decode("utf-8") for i in range(n)]
            continue
        if c["type"] not in TYPES:
            raise ValueError("unknown column type %r" % c["type"])
        dt = np.dtype(TYPES[c["type"]])
        if c.get("shuffled") and dt.itemsize > 1:  # itemsize planes of n bytes each
            planes = np.frombuffer(buf, np.uint8, n * dt.itemsize, at).reshape(dt.itemsize, n)
            cols[c["name"]] = np.ascontiguousarray(planes.T).view(dt).reshape(n)
        else:
            cols[c["name"]] = np.frombuffer(buf, dt, n, at)
    return header, cols


def expand(header, cols, name):
    """Strings for a dictionary-coded column such as 'camera' or 'band'."""
    spec = next(c for c in header["columns"] if c["name"] == name)
    return np.asarray(header["dictionaries"][spec["dict"]])[cols[name]]
// Minimal SP3C v1 reader for browsers and Node 18+. Sky Patrol V3 docs: /docs/formats.html
const TYPES = {f8: Float64Array, f4: Float32Array, i4: Int32Array, u4: Uint32Array,
               i2: Int16Array, u2: Uint16Array, u1: Uint8Array};

export function readSP3C(buf) {           // buf: ArrayBuffer, e.g. await (await fetch(url)).arrayBuffer()
  const dv = new DataView(buf);
  if (dv.getUint32(0, true) !== 0x43335053) throw new Error('not an SP3C payload');   // "SP3C"
  const hlen = dv.getUint32(4, true), base = 8 + hlen;
  const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 8, hlen)));
  if (parseInt(header.version, 10) !== 1) throw new Error('unsupported SP3C version ' + header.version);
  const n = header.n, cols = {};
  for (const c of header.columns) {
    if (c.offset % 8) throw new Error('misaligned column ' + c.name);
    const at = base + c.offset;
    if (c.type === 'str') {                // u4 offsets[n+1], then UTF-8 bytes
      const off = new Uint32Array(buf, at, n + 1), bytes = new Uint8Array(buf, at + 4 * (n + 1)), td = new TextDecoder();
      cols[c.name] = Array.from({length: n}, (_, i) => td.decode(bytes.subarray(off[i], off[i + 1])));
      continue;
    }
    const T = TYPES[c.type];
    if (!T) throw new Error('unknown column type ' + c.type);
    if (!c.shuffled || T.BYTES_PER_ELEMENT === 1) { cols[c.name] = new T(buf, at, n); continue; }  // zero-copy
    const w = T.BYTES_PER_ELEMENT, src = new Uint8Array(buf, at, n * w), out = new Uint8Array(n * w);
    for (let b = 0; b < w; b++) for (let i = 0; i < n; i++) out[i * w + b] = src[b * n + i];
    cols[c.name] = new T(out.buffer);
  }
  return {header, cols};
}

export function expand(header, cols, name) {   // strings of a dictionary column ('camera', 'band', ...)
  const dict = header.dictionaries[header.columns.find(c => c.name === name).dict];
  return Array.from(cols[name], k => dict[k]);
}
Python
import urllib.request
from sp3c_reader import read_sp3c, expand      # the file above

req = urllib.request.Request("https://asassn-v3.ifa.hawaii.edu/v3/stars/11010359/lc",
                             headers={"Accept-Encoding": "identity"})
header, cols = read_sp3c(urllib.request.urlopen(req).read())
camera = expand(header, cols, "camera")
print(header["n"], cols["mag"].dtype, camera[:3])

urllib does not decompress, so the Python example asks for an uncompressed response; with requests or a browser, compression is handled for you.

SP3F: the batch stream Coming soon#

The batch endpoints return many light curves as one SP3F v1 stream, so a client can process each curve as it arrives.

One stream header, then frames until the trailer.

Frame kindPayload
1One light curve as SP3C. Its header adds item: {index, id}, the position and id in your request.
2An error for one item, as JSON (error shape) plus item
3The trailer, as JSON: {n_items, n_ok, n_error, n_rows, request_id}

Items come back in request order. A stream without a trailer has failed, even if it ended cleanly: retry the items you did not receive.

"""Minimal SP3F v1 stream reader. Sky Patrol V3 docs: /docs/formats.html"""
import json
import struct

from sp3c_reader import read_sp3c  # the SP3C reader on the same page


def _exact(stream, n):
    data = b""
    while len(data) < n:
        chunk = stream.read(n - len(data))
        if not chunk:
            raise EOFError("SP3F stream ended early: no trailer, treat the batch as failed")
        data += chunk
    return data


def read_sp3f(stream):
    """Yield ('item', header, cols), ('error', dict) and finally ('trailer', dict)."""
    head = _exact(stream, 8)
    if head[:4] != b"SP3F" or head[4] != 1:
        raise ValueError("not an SP3F v1 stream")
    while True:
        frame = _exact(stream, 8)
        kind, (length,) = frame[0], struct.unpack_from("<I", frame, 4)
        payload = _exact(stream, length + (-length) % 8)[:length]
        if kind == 1:
            header, cols = read_sp3c(payload)
            yield "item", header, cols
        elif kind == 2:
            yield "error", json.loads(payload)
        elif kind == 3:
            yield "trailer", json.loads(payload)
            return
        else:
            raise ValueError("unknown SP3F frame kind %d" % kind)