Added stars at L1
This commit is contained in:
1 parent
e0381ccdd7
commit
970551b4e1
26 files changed
+2118
-55
No files matched your search
Executable
+399
@@ -0,0 +1,399 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Downloads the Hipparcos, Tycho-2 and Gaia DR3 star catalogs and writes the renderer's
|
||||
star file.
|
||||
|
||||
Sources:
|
||||
- Hipparcos main catalog, ESA 1997 (CDS I/239, hip_main.dat): 118,218 entries,
|
||||
complete to about V 7.3, with Johnson V and B-V. Positions are ICRS at epoch
|
||||
J1991.25. The 2007 new reduction (I/311) improves the astrometry by about a
|
||||
milliarcsecond, far below any pixel here, but carries no V, so the original is used.
|
||||
- Tycho-2, Hog et al. 2000 (CDS I/259, tyc2.dat.00-19): 2.54 million stars to about
|
||||
VT 11.5 (99 % complete to V 11.0), with Tycho BT and VT magnitudes. Mean positions
|
||||
are ICRS at epoch J2000.
|
||||
- Gaia DR3, Gaia Collaboration 2023 (gaiadr3.gaia_source_lite, from the ESA Gaia
|
||||
archive's TAP service, https://gea.esac.esa.int/archive/): every star brighter than
|
||||
G 15 by default (--gaia), 36.9 million, with its G magnitude and BP - RP color.
|
||||
Positions are ICRS at epoch J2016.0. Gaia's photometry saturates for the brightest
|
||||
stars, which Hipparcos and Tycho-2 cover.
|
||||
|
||||
Every star is taken from one catalog: Hipparcos first, then Tycho-2 for the stars it
|
||||
does not have (by Hipparcos number), then Gaia for the stars neither has (by Gaia's own
|
||||
cross-matches to them, gaiadr3.hipparcos2_best_neighbour and
|
||||
gaiadr3.tycho2tdsc_merge_best_neighbour). Magnitudes are converted to Johnson V and
|
||||
B - V:
|
||||
- Tycho: V = VT - 0.090 (BT - VT), B - V = 0.850 (BT - VT) (ESA 1997, vol. 1, 1.3,
|
||||
appendix 4). A star with no BT keeps V = VT and no color.
|
||||
- Gaia: V from G and BP - RP by Riello et al. 2021 (A&A 649, A3, table C.2),
|
||||
G - V = -0.02704 + 0.01424 c - 0.2156 c^2 + 0.01426 c^3 for c = BP - RP in -0.5 to
|
||||
2.75, 0.03 mag scatter. B - V from BP - RP by a cubic fitted here to the Hipparcos
|
||||
stars Gaia matched (printed when the script runs). A star with no BP - RP gets
|
||||
c = 0.82, the median of stars near G 15, and no B - V.
|
||||
Positions are carried to J2000 by their proper motion. The few Tycho-2 stars with no
|
||||
mean position (pflag X) use their observed position, at about epoch 1991.5, with no
|
||||
proper motion. Parallax is not kept: it moves every star by under 1 arcsecond.
|
||||
|
||||
Hipparcos and Tycho-2 come from the CDS archive (https://cdsarc.cds.unistra.fr) and are
|
||||
checked by sha256. Gaia comes from about 2,000 queries of the archive, each a range of
|
||||
source_id (a patch of sky), 4 at a time, split in half when a reply reaches the
|
||||
service's 50,000-row limit; a query's results are kept, so a run can be resumed, but
|
||||
they are not pinned by checksum. At G 15 that is about 2.3 GB of text and takes about
|
||||
an hour. Everything goes to data/stars/source/; the output is data/stars/stars.bin,
|
||||
about 600 MB at G 15, both gitignored. Needs numpy and pandas.
|
||||
|
||||
The output's format, little-endian:
|
||||
|
||||
8 bytes b"RSTARS02"
|
||||
u32 number of stars
|
||||
u32 0
|
||||
then per star, brightest first, 16 bytes:
|
||||
u32 right ascension, ICRS, epoch J2000, in units of 2 pi / 2^32
|
||||
i32 declination, ICRS, epoch J2000, in units of pi / 2^32
|
||||
i16 proper motion in right ascension times cos(declination), mas/yr
|
||||
i16 proper motion in declination, mas/yr
|
||||
u16 Johnson V magnitude plus 5, in thousandths
|
||||
i16 Johnson B - V in thousandths, or -32768 if unknown
|
||||
|
||||
Usage: scripts/fetch-stars.py [--gaia G] [--faint V]
|
||||
--gaia G take Gaia stars brighter than G (default 15; 0 for none)
|
||||
--faint V keep stars to V (default: all those the catalogs give)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
CDS = "https://cdsarc.cds.unistra.fr/ftp/I"
|
||||
GAIA_TAP = "https://gea.esac.esa.int/tap-server/tap/sync"
|
||||
# The service's limit on rows per synchronous query.
|
||||
GAIA_ROWS = 50_000
|
||||
# Gaia source_id carries the HEALPix level-12 cell in its high bits.
|
||||
CELLS = 12 * 4**12
|
||||
CELL = 2**35
|
||||
|
||||
FILES = {
|
||||
"hip_main.dat": ("239/hip_main.dat", "58ceabb104d647160d9437ce6e513a02a036bb4ad9f8879a5a22fd52943616e0"),
|
||||
"tyc2.dat.00.gz": ("259/tyc2.dat.00.gz", "1b224570fc6eb151984ce106fdf797728649d1ea77aaf3effd2d3444cfac6df6"),
|
||||
"tyc2.dat.01.gz": ("259/tyc2.dat.01.gz", "36221f4a5cdd9c5009d64299d2e6409f8d4cdf924321768780165bd3e2a2a99a"),
|
||||
"tyc2.dat.02.gz": ("259/tyc2.dat.02.gz", "e2ea4eeb7d204dde70ac54d172b1c429540c0d23d62ec0d7500561adc44cc57b"),
|
||||
"tyc2.dat.03.gz": ("259/tyc2.dat.03.gz", "29e0cb57cba7e1651455efcb528276b0c28e1fe83f47f8ebe963e48d8b180afb"),
|
||||
"tyc2.dat.04.gz": ("259/tyc2.dat.04.gz", "d04868329b470aa320f0fe5e9c525be619a7a228b364433e5f114f2178946d9b"),
|
||||
"tyc2.dat.05.gz": ("259/tyc2.dat.05.gz", "51f08772197dcf0cd26ff098c866036340b797f71ee04be22712517c10c73c4e"),
|
||||
"tyc2.dat.06.gz": ("259/tyc2.dat.06.gz", "f55503a66abce0e11d1a4ccbcde1f4c97a6e3e769de85e005f57ba963086ab03"),
|
||||
"tyc2.dat.07.gz": ("259/tyc2.dat.07.gz", "bc15b5b1f6308477360fbb93f6a98d9029601cab68973202be0f8764c127f524"),
|
||||
"tyc2.dat.08.gz": ("259/tyc2.dat.08.gz", "315ac34c678bc2e0f568b43b6b2d7c3d7c9cf9c089b51115d18b872864e50426"),
|
||||
"tyc2.dat.09.gz": ("259/tyc2.dat.09.gz", "d66c671fa29aad10bc5fd697f68c918bb774c1ffc3c7db9c20234895268390d5"),
|
||||
"tyc2.dat.10.gz": ("259/tyc2.dat.10.gz", "d86529df819ebdf3f4b8892510973ee10e1db06930814e014dd467193e740d52"),
|
||||
"tyc2.dat.11.gz": ("259/tyc2.dat.11.gz", "fc0c3203b3d2787da54c43d9963e363c8a3926576c383b101a15350a7ca23e9c"),
|
||||
"tyc2.dat.12.gz": ("259/tyc2.dat.12.gz", "68fd9d7e7353d52dea0de043cc62b45c04d234104eb05c063dc23ea7b6759576"),
|
||||
"tyc2.dat.13.gz": ("259/tyc2.dat.13.gz", "a08ecc092e6d134742c0f594c56c5c2e02b8fd5e76cffb1779877f2fcefac3fc"),
|
||||
"tyc2.dat.14.gz": ("259/tyc2.dat.14.gz", "2846e8cc489795a4d8160d2d5b64ff0e9a00598adba35a52a4f8ccf4c5e38b3e"),
|
||||
"tyc2.dat.15.gz": ("259/tyc2.dat.15.gz", "74784a3656382090a70d778820335f4a781509dbe3fcb9e92d18cf96b2c46c72"),
|
||||
"tyc2.dat.16.gz": ("259/tyc2.dat.16.gz", "320560e3d551cc40fa1f54ef7133709bf4bd45efa6688f808be7c09be5544a4d"),
|
||||
"tyc2.dat.17.gz": ("259/tyc2.dat.17.gz", "a8154e940aa0a0e8f31d91b4bd9fd56e6849da42cddb13c7dda772439b06991a"),
|
||||
"tyc2.dat.18.gz": ("259/tyc2.dat.18.gz", "5e951a1a51df7956f205b83cbfa5501d357c843640a253c1e6d3917bebe7d928"),
|
||||
"tyc2.dat.19.gz": ("259/tyc2.dat.19.gz", "f59605a38116f517a31a7dbdee3469c077658f2f40b8afe5da2aeb832eaee3dd"),
|
||||
}
|
||||
|
||||
# A star's fields as arrays: right ascension and declination (radians, J2000), proper
|
||||
# motion (mas/yr), V and B - V (NaN if unknown).
|
||||
FIELDS = ("ra", "dec", "pm_ra", "pm_dec", "v", "bv")
|
||||
|
||||
|
||||
def fetch(source):
|
||||
os.makedirs(source, exist_ok=True)
|
||||
for name, (path, digest) in FILES.items():
|
||||
target = os.path.join(source, name)
|
||||
if not os.path.exists(target):
|
||||
print(f"fetching {name}")
|
||||
urllib.request.urlretrieve(f"{CDS}/{path}", target + ".part")
|
||||
os.replace(target + ".part", target)
|
||||
h = hashlib.sha256()
|
||||
with open(target, "rb") as f:
|
||||
for block in iter(lambda: f.read(1 << 20), b""):
|
||||
h.update(block)
|
||||
if h.hexdigest() != digest:
|
||||
sys.exit(f"{target}: sha256 {h.hexdigest()}, expected {digest}")
|
||||
|
||||
|
||||
def number(line, first, last):
|
||||
"""The value in 1-based columns first to last, or None if blank."""
|
||||
text = line[first - 1:last].strip()
|
||||
return float(text) if text else None
|
||||
|
||||
|
||||
def propagate(ra, dec, pm_ra, pm_dec, years):
|
||||
"""Moves positions (radians) by their proper motion (mas/yr, right ascension times
|
||||
cos declination) over `years`, along the sphere."""
|
||||
scale = np.radians(1 / 3.6e6) * years
|
||||
ca, sa, cd, sd = np.cos(ra), np.sin(ra), np.cos(dec), np.sin(dec)
|
||||
x = cd * ca + scale * (pm_ra * -sa + pm_dec * -sd * ca)
|
||||
y = cd * sa + scale * (pm_ra * ca + pm_dec * -sd * sa)
|
||||
z = sd + scale * (pm_dec * cd)
|
||||
norm = np.sqrt(x * x + y * y + z * z)
|
||||
return np.mod(np.arctan2(y, x), 2 * np.pi), np.arcsin(z / norm)
|
||||
|
||||
|
||||
def table(rows):
|
||||
columns = list(zip(*rows)) if rows else [[]] * 6
|
||||
return {k: np.array(c, dtype=float) for k, c in zip(FIELDS, columns)}
|
||||
|
||||
|
||||
def hipparcos(path):
|
||||
rows, numbers = [], set()
|
||||
with open(path, encoding="ascii") as f:
|
||||
for line in f:
|
||||
v, ra, dec = number(line, 42, 46), number(line, 52, 63), number(line, 65, 76)
|
||||
if v is None or ra is None or dec is None:
|
||||
continue
|
||||
bv = number(line, 246, 251)
|
||||
rows.append((math.radians(ra), math.radians(dec), number(line, 88, 95) or 0.0,
|
||||
number(line, 97, 104) or 0.0, v, math.nan if bv is None else bv))
|
||||
numbers.add(int(line[8:14]))
|
||||
stars = table(rows)
|
||||
stars["ra"], stars["dec"] = propagate(
|
||||
stars["ra"], stars["dec"], stars["pm_ra"], stars["pm_dec"], 2000.0 - 1991.25)
|
||||
return stars, numbers
|
||||
|
||||
|
||||
def hipparcos_colors(path):
|
||||
"""B - V by Hipparcos number."""
|
||||
colors = {}
|
||||
with open(path, encoding="ascii") as f:
|
||||
for line in f:
|
||||
bv = number(line, 246, 251)
|
||||
if bv is not None:
|
||||
colors[int(line[8:14])] = bv
|
||||
return colors
|
||||
|
||||
|
||||
def tycho2(paths, hip_numbers):
|
||||
rows, skipped = [], {"hipparcos": 0, "no VT": 0}
|
||||
for path in paths:
|
||||
with gzip.open(path, "rt", encoding="ascii") as f:
|
||||
for line in f:
|
||||
hip = line[142:148].strip()
|
||||
if hip and int(hip) in hip_numbers:
|
||||
skipped["hipparcos"] += 1
|
||||
continue
|
||||
bt, vt = number(line, 111, 116), number(line, 124, 129)
|
||||
if vt is None:
|
||||
skipped["no VT"] += 1
|
||||
continue
|
||||
if line[13] == "X":
|
||||
ra, dec = number(line, 153, 164), number(line, 166, 177)
|
||||
pm_ra = pm_dec = 0.0
|
||||
else:
|
||||
ra, dec = number(line, 16, 27), number(line, 29, 40)
|
||||
pm_ra, pm_dec = number(line, 42, 48) or 0.0, number(line, 50, 56) or 0.0
|
||||
if bt is None:
|
||||
v, bv = vt, math.nan
|
||||
else:
|
||||
v, bv = vt - 0.090 * (bt - vt), 0.850 * (bt - vt)
|
||||
rows.append((math.radians(ra), math.radians(dec), pm_ra, pm_dec, v, bv))
|
||||
return table(rows), skipped
|
||||
|
||||
|
||||
def gaia_query(query):
|
||||
"""The rows of an ADQL query as a DataFrame, retrying a few times."""
|
||||
data = urllib.parse.urlencode({"REQUEST": "doQuery", "LANG": "ADQL", "FORMAT": "csv",
|
||||
"QUERY": query}).encode()
|
||||
for attempt in range(6):
|
||||
try:
|
||||
with urllib.request.urlopen(GAIA_TAP, data=data, timeout=600) as reply:
|
||||
return pd.read_csv(io.BytesIO(reply.read()))
|
||||
except Exception as e: # network or service errors: wait and retry
|
||||
if attempt == 5:
|
||||
raise
|
||||
print(f" retrying after {e}")
|
||||
time.sleep(10 * (attempt + 1))
|
||||
|
||||
|
||||
def gaia_range(directory, faint, lo, hi):
|
||||
"""Gaia stars brighter than G `faint` with source_id in [lo, hi), from the cache in
|
||||
`directory` or the archive, split until each query is within the row limit."""
|
||||
path = os.path.join(directory, f"{lo}-{hi}.csv")
|
||||
split = os.path.join(directory, f"{lo}-{hi}.split")
|
||||
mid = (lo + hi) // 2
|
||||
if os.path.exists(path):
|
||||
return [path]
|
||||
if os.path.exists(split):
|
||||
return gaia_range(directory, faint, lo, mid) + gaia_range(directory, faint, mid, hi)
|
||||
query = (
|
||||
"SELECT g.ra, g.dec, g.pmra, g.pmdec, g.phot_g_mean_mag, g.bp_rp, "
|
||||
"t.source_id AS tycho, h.source_id AS hip "
|
||||
"FROM gaiadr3.gaia_source_lite AS g "
|
||||
"LEFT OUTER JOIN gaiadr3.tycho2tdsc_merge_best_neighbour AS t ON g.source_id = t.source_id "
|
||||
"LEFT OUTER JOIN gaiadr3.hipparcos2_best_neighbour AS h ON g.source_id = h.source_id "
|
||||
f"WHERE g.phot_g_mean_mag < {faint} AND g.source_id >= {lo} AND g.source_id < {hi}"
|
||||
)
|
||||
rows = gaia_query(query)
|
||||
if len(rows) >= GAIA_ROWS:
|
||||
# Too many for one reply: split, and remember it, so a rerun does not ask again.
|
||||
open(split, "w").close()
|
||||
return gaia_range(directory, faint, lo, mid) + gaia_range(directory, faint, mid, hi)
|
||||
rows.to_csv(path + ".part", index=False)
|
||||
os.replace(path + ".part", path)
|
||||
return [path]
|
||||
|
||||
|
||||
def gaia(source, faint):
|
||||
directory = os.path.join(source, f"gaia-g{faint:g}")
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
chunks = 2048
|
||||
edges = [CELLS * i // chunks * CELL for i in range(chunks + 1)]
|
||||
paths = []
|
||||
with concurrent.futures.ThreadPoolExecutor(4) as pool:
|
||||
jobs = [pool.submit(gaia_range, directory, faint, lo, hi) for lo, hi in zip(edges, edges[1:])]
|
||||
for done, job in enumerate(concurrent.futures.as_completed(jobs), 1):
|
||||
paths += job.result()
|
||||
if done % 100 == 0:
|
||||
print(f" Gaia: {done} of {chunks} ranges")
|
||||
return pd.concat((pd.read_csv(p) for p in sorted(paths)), ignore_index=True)
|
||||
|
||||
|
||||
def gaia_color_fit(source, hip_main):
|
||||
"""A cubic giving B - V from BP - RP, fitted to the Hipparcos stars Gaia matched, and
|
||||
the scatter of the fit."""
|
||||
path = os.path.join(source, "gaia-hipparcos.csv")
|
||||
if not os.path.exists(path):
|
||||
parts = []
|
||||
edges = [CELLS * i // 8 * CELL for i in range(9)]
|
||||
for lo, hi in zip(edges, edges[1:]):
|
||||
parts.append(gaia_query(
|
||||
"SELECT h.original_ext_source_id AS hip, g.bp_rp "
|
||||
"FROM gaiadr3.hipparcos2_best_neighbour AS h "
|
||||
"JOIN gaiadr3.gaia_source_lite AS g ON h.source_id = g.source_id "
|
||||
f"WHERE g.source_id >= {lo} AND g.source_id < {hi}"
|
||||
))
|
||||
pd.concat(parts).to_csv(path, index=False)
|
||||
matched = pd.read_csv(path).dropna()
|
||||
colors = hipparcos_colors(hip_main)
|
||||
c = matched["bp_rp"].to_numpy()
|
||||
bv = np.array([colors.get(int(h), np.nan) for h in matched["hip"]])
|
||||
ok = np.isfinite(bv) & (c > -0.5) & (c < 3.5)
|
||||
coefficients = np.polyfit(c[ok], bv[ok], 3)
|
||||
residual = bv[ok] - np.polyval(coefficients, c[ok])
|
||||
return coefficients, np.median(np.abs(residual)) * 1.4826, ok.sum()
|
||||
|
||||
|
||||
def gaia_stars(rows, coefficients):
|
||||
"""Gaia rows that neither Hipparcos nor Tycho-2 has, as V and B - V at J2000."""
|
||||
rows = rows[rows["tycho"].isna() & rows["hip"].isna()]
|
||||
c = rows["bp_rp"].to_numpy()
|
||||
known = np.isfinite(c)
|
||||
c = np.where(known, c, 0.82)
|
||||
cc = np.clip(c, -0.5, 2.75)
|
||||
g_minus_v = -0.02704 + 0.01424 * cc - 0.2156 * cc**2 + 0.01426 * cc**3
|
||||
pm_ra = np.nan_to_num(rows["pmra"].to_numpy())
|
||||
pm_dec = np.nan_to_num(rows["pmdec"].to_numpy())
|
||||
ra, dec = propagate(np.radians(rows["ra"].to_numpy()), np.radians(rows["dec"].to_numpy()),
|
||||
pm_ra, pm_dec, 2000.0 - 2016.0)
|
||||
return {
|
||||
"ra": ra, "dec": dec, "pm_ra": pm_ra, "pm_dec": pm_dec,
|
||||
"v": rows["phot_g_mean_mag"].to_numpy() - g_minus_v,
|
||||
"bv": np.where(known, np.polyval(coefficients, np.clip(c, -0.5, 3.5)), np.nan),
|
||||
}
|
||||
|
||||
|
||||
def unit(stars):
|
||||
cd = np.cos(stars["dec"])
|
||||
return np.stack([cd * np.cos(stars["ra"]), cd * np.sin(stars["ra"]), np.sin(stars["dec"])], 1)
|
||||
|
||||
|
||||
def drop_near(stars, others, radius):
|
||||
"""`stars` without those within `radius` radians of any of `others`, and how many
|
||||
were dropped: cells of the radius on a side, searched with their neighbors."""
|
||||
key = lambda c: ((c[:, 0] + (1 << 20)) << 42) | ((c[:, 1] + (1 << 20)) << 21) | (c[:, 2] + (1 << 20))
|
||||
points = unit(stars)
|
||||
keys = key(np.floor(points / radius).astype(np.int64))
|
||||
order = np.argsort(keys)
|
||||
# A key no cell has ends the list, so a walk past the last star stops there.
|
||||
sorted_keys = np.append(keys[order], np.iinfo(np.int64).max)
|
||||
near = unit(others)
|
||||
cells = np.floor(near / radius).astype(np.int64)
|
||||
drop = np.zeros(len(points), dtype=bool)
|
||||
for offset in np.ndindex(3, 3, 3):
|
||||
k = key(cells + np.array(offset) - 1)
|
||||
i = np.searchsorted(sorted_keys, k)
|
||||
# Every star in the cell: walk forward while the key repeats.
|
||||
while True:
|
||||
hit = sorted_keys[i] == k
|
||||
if not hit.any():
|
||||
break
|
||||
j = order[i[hit]]
|
||||
close = np.linalg.norm(points[j] - near[hit], axis=1) < radius
|
||||
drop[j[close]] = True
|
||||
i = i + hit
|
||||
return {f: stars[f][~drop] for f in FIELDS}, int(drop.sum())
|
||||
|
||||
|
||||
def write(path, stars, faint):
|
||||
keep = stars["v"] <= faint
|
||||
order = np.argsort(stars["v"][keep], kind="stable")
|
||||
s = {k: stars[k][keep][order] for k in FIELDS}
|
||||
record = np.dtype([("ra", "<u4"), ("dec", "<i4"), ("pm_ra", "<i2"), ("pm_dec", "<i2"),
|
||||
("v", "<u2"), ("bv", "<i2")])
|
||||
out = np.zeros(len(s["v"]), dtype=record)
|
||||
out["ra"] = np.round(np.mod(s["ra"], 2 * np.pi) / (2 * np.pi) * 2**32).astype(np.uint64) % 2**32
|
||||
out["dec"] = np.clip(np.round(s["dec"] / np.pi * 2**32), -(2**31), 2**31 - 1)
|
||||
out["pm_ra"] = np.clip(np.round(s["pm_ra"]), -32767, 32767)
|
||||
out["pm_dec"] = np.clip(np.round(s["pm_dec"]), -32767, 32767)
|
||||
out["v"] = np.clip(np.round((s["v"] + 5.0) * 1000), 0, 65535)
|
||||
out["bv"] = np.where(np.isfinite(s["bv"]), np.clip(np.round(s["bv"] * 1000), -32767, 32767), -32768)
|
||||
with open(path + ".part", "wb") as f:
|
||||
f.write(b"RSTARS02" + np.array([len(out), 0], dtype="<u4").tobytes())
|
||||
f.write(out.tobytes())
|
||||
os.replace(path + ".part", path)
|
||||
return len(out), s["v"][-1] if len(out) else math.nan
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
||||
parser.add_argument("--gaia", type=float, default=15.0, help="Gaia stars brighter than G")
|
||||
parser.add_argument("--faint", type=float, default=math.inf, help="faintest V kept")
|
||||
args = parser.parse_args()
|
||||
root = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "stars")
|
||||
source = os.path.join(root, "source")
|
||||
fetch(source)
|
||||
hip_main = os.path.join(source, "hip_main.dat")
|
||||
hip, numbers = hipparcos(hip_main)
|
||||
tyc, skipped = tycho2(
|
||||
sorted(os.path.join(source, n) for n in FILES if n.startswith("tyc2")), numbers
|
||||
)
|
||||
print(f"Hipparcos: {len(hip['v'])} stars; Tycho-2: {len(tyc['v'])} more "
|
||||
f"({skipped['hipparcos']} in Hipparcos, {skipped['no VT']} without VT)")
|
||||
parts = [hip, tyc]
|
||||
if args.gaia > 0:
|
||||
coefficients, scatter, used = gaia_color_fit(source, hip_main)
|
||||
print(f"B - V from BP - RP: cubic {np.round(coefficients, 5).tolist()} "
|
||||
f"(highest power first), {scatter:.3f} mag scatter over {used} Hipparcos stars")
|
||||
rows = gaia(source, args.gaia)
|
||||
extra = gaia_stars(rows, coefficients)
|
||||
print(f"Gaia DR3 to G {args.gaia:g}: {len(rows)} stars, {len(extra['v'])} not in "
|
||||
f"Hipparcos or Tycho-2 by Gaia's cross-match")
|
||||
# Gaia's cross-match misses about 1 % of Tycho-2: drop Gaia stars within 1
|
||||
# arcsecond of one, which Tycho's and Hipparcos's photometry, unable to resolve
|
||||
# pairs that close, already includes.
|
||||
others = {k: np.concatenate([hip[k], tyc[k]]) for k in FIELDS}
|
||||
extra, dropped = drop_near(extra, others, np.radians(1 / 3600))
|
||||
print(f" {dropped} more within 1 arcsecond of a Hipparcos or Tycho-2 star")
|
||||
parts.append(extra)
|
||||
stars = {k: np.concatenate([p[k] for p in parts]) for k in FIELDS}
|
||||
out = os.path.join(root, "stars.bin")
|
||||
count, faintest = write(out, stars, args.faint)
|
||||
print(f"{count} stars to V {faintest:.2f} in {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Writes examples/scenes/<model>.toml for the NASA models fetch-models.sh downloads,
|
||||
and two scenes with Earth, the Moon and a spacecraft together (below):
|
||||
two scenes with Earth, the Moon and a spacecraft together (below):
|
||||
examples/scenes/earthrise.toml from lunar orbit and examples/scenes/moonrise.toml
|
||||
from geostationary height.
|
||||
from geostationary height, and scenes for the stars of camera tier C1: a star field,
|
||||
examples/scenes/stars-pleiades.toml, and a telescope on one geostationary satellite
|
||||
imaging another along the arc, examples/scenes/geo-telescope-*.toml.
|
||||
|
||||
Every scene has the same composition: the spacecraft in an isometric view (the
|
||||
camera looks along its body (+1, +1, +1) diagonal, body +Z toward image up) with
|
||||
@@ -447,6 +449,135 @@ attitude = {fmt(quaternion(attitude), 9)}
|
||||
"""
|
||||
|
||||
|
||||
# Earth's orbital speed, m/s, and the geostationary orbital speed.
|
||||
EARTH_SPEED = 29.78e3
|
||||
GEO_SPEED = 3074.7
|
||||
# The telescope's longitude on the geostationary arc, degrees: looking along the arc
|
||||
# from here points at right ascension 103 degrees, where the Milky Way crosses the
|
||||
# equator in Monoceros, the densest stretch of the equator (190 stars per square degree
|
||||
# to V 12, against 50 where it crosses in Aquila, behind the dust of the Aquila Rift).
|
||||
GEO_LONGITUDE = 13.0
|
||||
# The ecliptic's north pole in the inertial (equatorial J2000) frame.
|
||||
ECLIPTIC_POLE = [0.0, -math.sin(math.radians(23.4393)), math.cos(math.radians(23.4393))]
|
||||
|
||||
|
||||
def earth_velocity(sun):
|
||||
"""Earth's velocity around the Sun when the Sun is in direction `sun`: along the
|
||||
ecliptic, a quarter turn ahead of the Sun's direction from Earth."""
|
||||
return [EARTH_SPEED * x for x in norm(cross(ECLIPTIC_POLE, [-x for x in sun]))]
|
||||
|
||||
|
||||
def stars_pleiades():
|
||||
# The Pleiades, right ascension 56.75 and declination 24.12 degrees, from
|
||||
# geostationary orbit, with the Sun behind the camera.
|
||||
ra, dec = math.radians(56.75), math.radians(24.12)
|
||||
look = [math.cos(dec) * math.cos(ra), math.cos(dec) * math.sin(ra), math.sin(dec)]
|
||||
camera = [GEO, 0.0, 0.0]
|
||||
sun = [-x for x in look]
|
||||
fmt = lambda v, d=2: "[" + ", ".join(f"{x:.{d}f}" for x in v) + "]"
|
||||
return f"""# A star field: the Pleiades through the example camera (12 by 7 degrees), from
|
||||
# geostationary orbit, long-exposed at 5 s. Stars are drawn at lighting L1 with camera
|
||||
# C1, from the catalog scripts/fetch-stars.py writes. Alcyone, the brightest at V 2.9,
|
||||
# gives about 200,000 electrons, ten times the full well; a V 8 star about 2,000 and a
|
||||
# V 11 star about 130. Generated by scripts/make-example-scenes.py.
|
||||
#
|
||||
# resolved render --scene examples/scenes/stars-pleiades.toml --lighting L1 --camera C1 --pretty --preview pleiades.png
|
||||
|
||||
sun = {fmt([AU * x for x in sun], 0)}
|
||||
moon = [0, -384400000, 0]
|
||||
earth_velocity = {fmt(earth_velocity(sun))}
|
||||
|
||||
[camera]
|
||||
exposure = 5
|
||||
|
||||
[observer]
|
||||
position = {fmt(camera)}
|
||||
look_at = {fmt([camera[i] + 1e9 * look[i] for i in range(3)])}
|
||||
up = [0, 0, 1]
|
||||
velocity = [0.00, {GEO_SPEED:.2f}, 0.00]
|
||||
|
||||
# At lighting L1, Earth's surface shows July (from scripts/fetch-earth.sh).
|
||||
[appearance]
|
||||
earth_month = 7
|
||||
"""
|
||||
|
||||
|
||||
def geo_telescope(range_m, phase_deg, f_number, exposure, description):
|
||||
# The camera at GEO_LONGITUDE on the geostationary arc, the target ahead along it.
|
||||
file, scale, center = "ssl1300.glb", 0.1, MODELS[3][3]
|
||||
lon = math.radians(GEO_LONGITUDE)
|
||||
camera = [GEO * math.cos(lon), GEO * math.sin(lon), 0.0]
|
||||
angle = lon + range_m / GEO
|
||||
spot = [GEO * math.cos(angle), GEO * math.sin(angle), 0.0]
|
||||
velocity = [-GEO_SPEED * math.sin(lon), GEO_SPEED * math.cos(lon), 0.0]
|
||||
look = norm([spot[i] - camera[i] for i in range(3)])
|
||||
image_up = [0.0, 0.0, 1.0]
|
||||
attitude = triad([1, 1, 1], [0, 0, 1], [-x for x in look], image_up)
|
||||
offset = mul(attitude, center)
|
||||
position = [spot[i] - offset[i] for i in range(3)]
|
||||
# The Sun `phase_deg` from the camera's direction as the target sees it, turned
|
||||
# about the image's up, 10 degrees out of the equator so the target is not shadowed
|
||||
# by Earth at any phase.
|
||||
back = [-x for x in look]
|
||||
side = cross(image_up, back)
|
||||
p, t = math.radians(phase_deg), math.radians(10.0)
|
||||
sun = norm([
|
||||
math.cos(t) * (math.cos(p) * back[i] + math.sin(p) * side[i]) + math.sin(t) * image_up[i]
|
||||
for i in range(3)
|
||||
])
|
||||
fmt = lambda v, d=2: "[" + ", ".join(f"{x:.{d}f}" for x in v) + "]"
|
||||
heading = textwrap.fill(description, 86, initial_indent="# ", subsequent_indent="# ")
|
||||
return f"""{heading}
|
||||
# A 1 m, f/{f_number:.0f} telescope ({100 / f_number:g} cm aperture, 5.5 urad per pixel, 0.6 by 0.34
|
||||
# degrees) on one geostationary satellite, imaging an SSL-1300 {range_m / 1000:.0f} km ahead along
|
||||
# the arc, lit at a phase of {phase_deg} degrees, against the stars toward the Milky Way in
|
||||
# Monoceros. Stars are drawn at lighting L1 with camera C1 (scripts/fetch-stars.py).
|
||||
# Generated by scripts/make-example-scenes.py; the model comes from
|
||||
# scripts/fetch-models.sh.
|
||||
#
|
||||
# resolved render --scene <this file> --lighting L1 --camera C1 --pretty --preview geo.png
|
||||
|
||||
sun = {fmt([AU * x for x in sun], 0)}
|
||||
moon = [0, -384400000, 0]
|
||||
earth_velocity = {fmt(earth_velocity(sun))}
|
||||
|
||||
[camera]
|
||||
focal_length = 1.0
|
||||
f_number = {f_number:.1f}
|
||||
exposure = {exposure}
|
||||
|
||||
[observer]
|
||||
position = {fmt(camera)}
|
||||
look_at = {fmt(spot)}
|
||||
up = [0, 0, 1]
|
||||
velocity = {fmt(velocity)}
|
||||
|
||||
# At lighting L1, Earth's surface shows July (from scripts/fetch-earth.sh), and lights
|
||||
# the target's Earth-facing side.
|
||||
[appearance]
|
||||
earth_month = 7
|
||||
|
||||
[[target]]
|
||||
model = "../models/nasa/{file}"
|
||||
scale = {scale}
|
||||
position = {fmt(position, 4)}
|
||||
attitude = {fmt(quaternion(attitude), 9)}
|
||||
"""
|
||||
|
||||
|
||||
GEO_TELESCOPE = [
|
||||
("geo-telescope-100km", 100e3, 30, 8.0, 6e-4,
|
||||
"GEO to GEO at 100 km: the target about 55 px across, lit nearly face on, exposed "
|
||||
"short of saturating it, with only the brighter stars behind it."),
|
||||
("geo-telescope-100km-side", 100e3, 90, 8.0, 6e-4,
|
||||
"GEO to GEO at 100 km, lit from the side: half the target in shadow."),
|
||||
("geo-telescope-1000km", 1000e3, 30, 4.0, 0.2,
|
||||
"GEO to GEO at 1000 km, exposed 0.2 s at f/4 as a surveillance camera would: the "
|
||||
"target a few pixels across and far past saturation, in a field of stars to the "
|
||||
"catalog's limit, about V 15, which gives a few DN."),
|
||||
]
|
||||
|
||||
|
||||
out = pathlib.Path(__file__).resolve().parent.parent / "examples" / "scenes"
|
||||
for model in MODELS:
|
||||
path = out / f"{model[0]}.toml"
|
||||
@@ -458,3 +589,10 @@ print(f"wrote {path}")
|
||||
path = out / "moonrise.toml"
|
||||
path.write_text(moonrise())
|
||||
print(f"wrote {path}")
|
||||
path = out / "stars-pleiades.toml"
|
||||
path.write_text(stars_pleiades())
|
||||
print(f"wrote {path}")
|
||||
for name, range_m, phase, f_number, exposure, description in GEO_TELESCOPE:
|
||||
path = out / f"{name}.toml"
|
||||
path.write_text(geo_telescope(range_m, phase, f_number, exposure, description))
|
||||
print(f"wrote {path}")
|
||||
Reference in new issue
Block a user