"""Download every external dataset used by the study into the raw directory.
Sources (all public, plain HTTPS):
* SETI Institute Technosearch (radio / optical / archival lists) -> parsed to data/technosearch_*.csv
* VizieR TAP: Wlodarczyk-Sroka+ 2020 (J/MNRAS/498/5720), Harp+ 2016 (J/AJ/152/181),
Isaacson+ 2017 (J/PASP/129/E4501), Gaia Catalogue of Nearby Stars (J/A+A/649/A6),
Baumgardt & Hilker 2018 globular-cluster masses (J/MNRAS/478/1520)
* CDS XMatch: Harp+ 2016 targets x Gaia DR3 (I/355/gaiadr3) for parallaxes
* Gaia archive TAP: binned M_G - luminosity relation from DR3 FLAME (for the Dyson-swarm sensitivity)
Files are cached: an existing non-empty file is not downloaded again (delete it to refresh).
"""
import csv
import html
import io
import os
import re
import sys
import time
import urllib.parse
import urllib.request
import uuid
from common import RAW, DATA
VIZ = os.path.join(RAW, "vizier")
TAP = "https://tapvizier.cds.unistra.fr/TAPVizieR/tap/sync"
XMATCH = "http://cdsxmatch.u-strasbg.fr/xmatch/api/v1/sync"
# technosearch.seti.org answers 403 to the default Python user agent
UA = "Mozilla/5.0 (X11; Linux x86_64) seti_meta-study/1.0 (research script; urllib)"
def _get(url, data=None, headers=None, timeout=600, tries=4, pause=5):
last = None
for i in range(tries):
try:
hdr = {"User-Agent": UA}
hdr.update(headers or {})
req = urllib.request.Request(url, data=data, headers=hdr)
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.read()
except Exception as e: # noqa
last = e
time.sleep(pause * (i + 1))
raise RuntimeError(f"download failed: {url}: {last}")
def cached(path):
return os.path.exists(path) and os.path.getsize(path) > 200
def tap(query, out, maxrec=500000):
if cached(out):
return
params = urllib.parse.urlencode({"REQUEST": "doQuery", "LANG": "ADQL", "FORMAT": "csv",
"MAXREC": str(maxrec), "QUERY": query})
print(" TAP ->", os.path.basename(out)); sys.stdout.flush()
b = _get(TAP + "?" + params)
if b.lstrip().startswith(b"", name)
with open(out, "wb") as f:
f.write(_get(f"https://technosearch.seti.org/{name}/", timeout=90))
s = open(out, encoding="utf8").read()
rows = []
for t in re.findall(r"
", s, flags=re.S):
for r in re.findall(r"]*>(.*?)
", t, flags=re.S):
cells = re.findall(r"]*>(.*?)", r, flags=re.S)
links = [l for l in re.findall(r'href="([^"]+)"', r)]
clean = []
for c in cells:
c = re.sub(r"(.*?)", r"^\1", c, flags=re.S)
c = re.sub(r"<[^>]+>", "", c)
clean.append(re.sub(r"\s+", " ", html.unescape(c)).strip())
links = ["https://technosearch.seti.org" + l if l.startswith("/") else l for l in links]
rows.append(clean + [" | ".join(links)])
if rows:
rows[0][-1] = "links"
with open(os.path.join(DATA, f"technosearch_{name.replace('-list','')}.csv"), "w", newline="", encoding="utf8") as f:
csv.writer(f).writerows(rows)
print(f" technosearch {name}: {len(rows)-1} rows")
def fetch_vizier():
tap('SELECT recno,RA_ICRS,DE_ICRS,Plx,RPlx,Gmag,Teff,Target,RATdeg,DETdeg,PlxT,rest,InstT,InstS,FWHM,'
'"Offset-arcmin",TelResGaus,"min-detec-EIRP","Price-target?" FROM "J/MNRAS/498/5720/catalog"',
os.path.join(VIZ, "ws2020_catalog.csv"))
tap('SELECT recno,S,Name,Percent,BW,SimbadName,"_RA","_DE" FROM "J/AJ/152/181/table7"',
os.path.join(VIZ, "harp2016_table7.csv"))
tap('SELECT * FROM "J/AJ/152/181/table6"', os.path.join(VIZ, "harp2016_table6.csv"))
tap('SELECT * FROM "J/PASP/129/E4501/table1"', os.path.join(VIZ, "isaacson2017_table1.csv"))
tap('SELECT GaiaEDR3,RA_ICRS,DE_ICRS,Plx,"Gmag",BPmag,RPmag,GCNSprob,WDprob,Dist50,W3mag,W4mag '
'FROM "J/A+A/649/A6/table1c"', os.path.join(VIZ, "gcns_table1c.csv"))
tap('SELECT Cluster,Dist,Mass,e_Mass FROM "J/MNRAS/478/1520/table2"', os.path.join(VIZ, "baumgardt2018_gc.csv"))
def xmatch_harp():
out = os.path.join(VIZ, "harp2016_x_gaiadr3.csv")
if cached(out):
return
rows = []
for fn, tag in (("harp2016_table7.csv", "t7"), ("harp2016_table6.csv", "t6")):
for r in csv.DictReader(open(os.path.join(VIZ, fn), encoding="utf8")):
if r["_RA"] and r["_DE"]:
rows.append((f"{tag}_{r['recno']}", r["_RA"], r["_DE"]))
buf = "id,ra,dec\n" + "\n".join(",".join(x) for x in rows) + "\n"
boundary = uuid.uuid4().hex
fields = {"request": "xmatch", "distMaxArcsec": "20", "RESPONSEFORMAT": "csv", "colRA1": "ra",
"colDec1": "dec", "cat2": "vizier:I/355/gaiadr3", "cols2": "Source,Plx,e_Plx,Gmag,PM"}
body = b""
for k, v in fields.items():
body += (f"--{boundary}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n{v}\r\n").encode()
body += (f"--{boundary}\r\nContent-Disposition: form-data; name=\"cat1\"; filename=\"cat1.csv\"\r\n"
f"Content-Type: text/csv\r\n\r\n").encode() + buf.encode() + f"\r\n--{boundary}--\r\n".encode()
print(" CDS XMatch -> harp2016_x_gaiadr3.csv"); sys.stdout.flush()
b = _get(XMATCH, data=body, headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
if not b.startswith(b"angDist"):
raise RuntimeError("xmatch error: " + b[:400].decode(errors="replace"))
with open(out, "wb") as f:
f.write(b)
def fetch_gaia_lum_relation():
"""Server-side binned M_G -> log10(L/Lsun) relation (Gaia DR3 FLAME luminosities, stars within 100 pc)."""
out = os.path.join(RAW, "gaia_mg_loglum_relation.csv")
if cached(out):
return
q = ("SELECT FLOOR((s.phot_g_mean_mag+5*LOG10(s.parallax)-10)*2)/2 AS mg_bin, COUNT(*) AS n, "
"AVG(LOG10(a.lum_flame)) AS mean_loglum, STDDEV(LOG10(a.lum_flame)) AS sd_loglum "
"FROM gaiadr3.gaia_source AS s JOIN gaiadr3.astrophysical_parameters AS a ON s.source_id=a.source_id "
"WHERE s.parallax>10 AND s.parallax_over_error>20 AND a.lum_flame IS NOT NULL AND s.ruwe<1.4 "
"GROUP BY mg_bin ORDER BY mg_bin")
params = urllib.parse.urlencode({"REQUEST": "doQuery", "LANG": "ADQL", "FORMAT": "csv", "QUERY": q})
print(" Gaia TAP -> gaia_mg_loglum_relation.csv"); sys.stdout.flush()
b = _get("https://gea.esac.esa.int/tap-server/tap/sync?" + params)
if not b.startswith(b"mg_bin"):
raise RuntimeError("Gaia TAP error: " + b[:400].decode(errors="replace"))
with open(out, "wb") as f:
f.write(b)
def verify_arxiv(offline=False):
"""Resolve every arXiv identifier cited in data/surveys_compilation.csv through the arXiv API and write
data/arxiv_verification.csv (first author, year, title and journal reference as recorded by arXiv), so
that the citations in the hand-typed compilation can be checked mechanically."""
import json
comp = os.path.join(DATA, "surveys_compilation.csv")
cache = os.path.join(RAW, "arxiv_verify.json")
rows = list(csv.DictReader(open(comp, encoding="utf8")))
ids = sorted({m.group(1) for r in rows for m in [re.search(r"arxiv.org/abs/([\d.]+)", r["url"])] if m})
meta = json.load(open(cache)) if os.path.exists(cache) else {}
todo = [i for i in ids if i not in meta]
if todo and not offline:
for k in range(0, len(todo), 20):
chunk = todo[k:k + 20]
print(f" arXiv API -> {len(chunk)} records"); sys.stdout.flush()
try:
xml = _get("https://export.arxiv.org/api/query?" + urllib.parse.urlencode(
{"id_list": ",".join(chunk), "max_results": 50}), timeout=90, tries=5, pause=20).decode()
except RuntimeError as err: # the arXiv API rate-limits (HTTP 429); this check is ancillary
print(" WARNING: arXiv API unavailable, citation check incomplete; rerun later:", str(err)[-60:])
break
for e in re.findall(r"(.*?)", xml, flags=re.S):
g = lambda t: re.sub(r"\s+", " ", html.unescape(re.search(r"<%s[^>]*>(.*?)%s>" % (t, t), e, flags=re.S).group(1))).strip()
au = re.findall(r"(.*?)", e)
jr = re.search(r"]*>(.*?)", e, flags=re.S)
meta[re.search(r"abs/([\d.]+)", g("id")).group(1)] = dict(
title=g("title"), first=au[0] if au else "", n_auth=len(au), year=g("published")[:4],
jr=html.unescape(jr.group(1)).strip() if jr else "")
json.dump(meta, open(cache, "w"), indent=1)
time.sleep(8)
with open(os.path.join(DATA, "arxiv_verification.csv"), "w", newline="", encoding="utf8") as f:
w = csv.writer(f)
w.writerow(["compilation_id", "arxiv_id", "reference_as_compiled", "arxiv_resolves", "arxiv_first_author",
"arxiv_year_submitted", "arxiv_title", "arxiv_journal_ref"])
n_ok = 0
for r in rows:
m = re.search(r"arxiv.org/abs/([\d.]+)", r["url"])
if not m:
continue
o = meta.get(m.group(1))
n_ok += o is not None
w.writerow([r["id"], m.group(1), r["reference"], True if o is not None else "not checked"] +
([o["first"], o["year"], o["title"], o["jr"]] if o else ["", "", "", ""]))
print(f" arXiv verification: {n_ok} of {len(ids)} cited arXiv identifiers resolve (the rest could not be checked)"
if n_ok < len(ids) else f" arXiv verification: {n_ok} of {len(ids)} cited arXiv identifiers resolve")
def main():
print("[fetch] Technosearch"); fetch_technosearch()
print("[fetch] Gaia"); fetch_gaia_lum_relation()
print("[fetch] VizieR"); fetch_vizier()
print("[fetch] XMatch"); xmatch_harp()
print("[fetch] done; raw dir:", RAW)
if __name__ == "__main__":
main()