Add the seti-tools technosignature toolkit, its guide, ledger, results, papers and coverage store
aa1ffa8 Download seti.py from phanerozoic/seti-tools: direct link, hf CLI and curl.
- Browser
- Download file 22.3 kB
-
https://huggingface.co/phanerozoic/seti-tools/resolve/main/seti.py
- Command line
-
hf download hf://phanerozoic/seti-tools/seti.py
-
curl -L -o seti.py https://huggingface.co/phanerozoic/seti-tools/resolve/main/seti.py
22.3 kB
| """Command line of seti-tools: search families, the coverage store and the self-test.""" | |
| import math | |
| import os | |
| import sys | |
| import tempfile | |
| import time | |
| import traceback | |
| # one math-library thread per process, so the process cap bounds CPU load | |
| for _v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"): | |
| os.environ.setdefault(_v, "1") | |
| import numpy as np # noqa: E402 | |
| for _p in (os.environ.get("SETITOOLS", ""), os.path.dirname(os.path.abspath(__file__))): | |
| if _p and os.path.isdir(os.path.join(_p, "setitools")): | |
| sys.path.insert(0, _p) | |
| break | |
| from setitools import science # noqa: E402 | |
| CHECKS = [] | |
| def check(name): | |
| def wrap(fn): | |
| def run(): | |
| t0 = time.time() | |
| try: | |
| msg = fn() or "" | |
| CHECKS.append((name, True, msg, time.time() - t0)) | |
| except Exception as e: | |
| CHECKS.append((name, False, "%s: %s" % (type(e).__name__, e), time.time() - t0)) | |
| traceback.print_exc() | |
| run.__name__ = fn.__name__ | |
| return run | |
| return wrap | |
| def synthetic_star(n=6000, nv=6, seed=0, line_at=3000, line_amp=0.0, line_visits=(1, 4), topo_at=None, topo_amp=0.0, | |
| sigma=1.5): | |
| """Synthetic visits of one star: absorption features, noise, and optional rest-frame and observatory-frame lines.""" | |
| rng = np.random.default_rng(seed) | |
| dlog = 6e-6 | |
| lam = 10 ** (np.log10(15200.0) + dlog * np.arange(n)) | |
| base = 1.0 - 0.3 * np.exp(-0.5 * ((np.arange(n)[:, None] - rng.uniform(0, n, 60)[None, :]) / 3.0) ** 2).sum(1) | |
| vobs = np.linspace(-25.0, 25.0, nv) | |
| V, E = [], [] | |
| x = np.arange(n) | |
| for k in range(nv): | |
| f = base * (1.0 + (line_amp if k in line_visits else 0.0) * np.exp(-0.5 * ((x - line_at) / sigma) ** 2)) | |
| if topo_at is not None: | |
| # an observatory-frame line moves in the rest frame from visit to visit | |
| shift = -vobs[k] / science.C / (dlog * math.log(10.0)) | |
| f = f * (1.0 + topo_amp * np.exp(-0.5 * ((x - topo_at - shift) / sigma) ** 2)) | |
| e = np.full(n, 0.004) | |
| V.append(1000.0 * (f + rng.normal(0, 0.004, n))) | |
| E.append(1000.0 * e) | |
| return lam, np.array(V), np.array(E), np.zeros((nv, n), bool), vobs, dlog | |
| def t_matched(): | |
| rng = np.random.default_rng(1) | |
| n, sp = 4000, 2.0 | |
| res = rng.normal(0, 0.01, n) | |
| res += 0.06 * np.exp(-0.5 * ((np.arange(n) - 1700) / sp) ** 2) | |
| snr, amp = science.matched_filter(res, np.full(n, 0.01), sp) | |
| j = int(np.argmax(snr)) | |
| w = science.width_ratio(res, j, sp) | |
| assert abs(j - 1700) <= 1 and snr[j] > 8 and 0.7 < w < 1.4, (j, snr[j], w) | |
| return "peak %d, S/N %.1f, width ratio %.2f, amplitude %.3f" % (j, snr[j], w, amp[j]) | |
| def t_spiky(): | |
| f = np.ones(200) | |
| f[100] = 3.0 | |
| assert science.profile_is_spiky(f, 100, 2.0, base=1.0) | |
| g = 1.0 + np.exp(-0.5 * ((np.arange(200) - 100) / 2.0) ** 2) | |
| assert not science.profile_is_spiky(g, 100, 2.0, base=1.0) | |
| def t_lsf(): | |
| a = science.lsf_sigma_px(22500, dlog10=6e-6) | |
| b = science.lsf_sigma_px(115000, 5000.0, 0.01) | |
| assert 1.2 < a < 1.5 and 1.7 < b < 1.9, (a, b) | |
| return "APOGEE %.2f px, HARPS %.2f px" % (a, b) | |
| def t_recurrence(): | |
| sp = science.lsf_sigma_px(22500, dlog10=6e-6) | |
| lam, V, E, bad, vobs, _ = synthetic_star(line_at=2500, line_amp=0.06, topo_at=4200, topo_amp=0.06, sigma=sp) | |
| out = science.visit_recurrence(lam, V, E, bad, vobs, sp) | |
| pos = [g["lam"] for g in out["pos"]] | |
| assert any(abs(x - lam[2500]) < 0.5 for x in pos), pos | |
| assert not any(abs(x - lam[4200]) < 3.0 for x in pos), pos | |
| assert out["terrestrial"], out | |
| return "rest-frame groups %s, terrestrial %s" % (pos, out["terrestrial"]) | |
| def t_dropout(): | |
| sp = science.lsf_sigma_px(22500, dlog10=6e-6) | |
| lam, V, E, bad, vobs, _ = synthetic_star(nv=7, line_amp=0.0, sigma=sp) | |
| for k, s in ((0, 3000), (4, 3004), (5, 3001)): | |
| V[k, s:s + 8] *= 0.33 | |
| bad[1:3, 2990:3020] = True | |
| out = science.visit_recurrence(lam, V, E, bad, vobs, sp) | |
| assert not any(abs(g["lam"] - lam[3003]) < 3.0 for g in out["pos"]), out["pos"] | |
| return "emission groups near the dropouts: none" | |
| def t_twin(): | |
| rng = np.random.default_rng(2) | |
| n = 800 | |
| x = np.arange(n) | |
| feats = rng.uniform(0, n, 30) | |
| def star(s): | |
| return 1 - 0.2 * np.exp(-0.5 * ((x[:, None] - feats[None, :] - s) / 3.0) ** 2).sum(1) | |
| basis = np.array([star(0.0) + rng.normal(0, 0.002, n) for _ in range(5)]) | |
| target = star(0.0) + 0.05 * np.exp(-0.5 * ((x - 400) / 1.5) ** 2) + rng.normal(0, 0.002, n) | |
| model, _ = science.twin_model(target, basis, np.ones(n, bool)) | |
| res = target / model - 1 | |
| assert res[400] > 0.03, res[400] | |
| return "residual at the line %.3f" % res[400] | |
| def t_absorption(): | |
| rng = np.random.default_rng(5) | |
| grid = np.round(np.arange(4200.0, 4300.0, 0.01), 2) | |
| feats = rng.uniform(4200, 4300, 150) | |
| def star(depth_scale): | |
| f = np.ones(len(grid)) | |
| for c in feats: | |
| f *= 1 - depth_scale * 0.3 * np.exp(-0.5 * ((grid - c) / 0.05) ** 2) | |
| return f | |
| twins = [star(1.0 + 0.02 * k) + rng.normal(0, 0.003, len(grid)) for k in range(6)] | |
| lines = (4220.0, 4250.0, 4280.0) | |
| target = star(1.01) + rng.normal(0, 0.003, len(grid)) | |
| sp = 4250.0 / 115000.0 / 2.3548 | |
| for c in lines: | |
| target *= 1 - 0.03 * np.exp(-0.5 * ((grid - c) / sp) ** 2) | |
| z = [science.line_absorption(target, twins, grid, c, 115000.0)[0] for c in lines] | |
| centres = rng.uniform(4210, 4290, 300) | |
| null = np.array([science.line_absorption(target, twins, grid, c, 115000.0)[0] for c in centres | |
| if all(abs(c - x) > 1 for x in lines)]) | |
| zc, p = science.combined_line_score(z, [1.0, 1.0, 1.0], null[np.isfinite(null)]) | |
| assert all(v < -4 for v in z) and p < 0.01, (z, zc, p) | |
| return "line scores %s, combined %.1f, p %.4f" % ([round(v, 1) for v in z], zc, p) | |
| def t_frames(): | |
| bc = float(science.barycentric_correction(83.82, -5.39, 60000.0, "Paranal")) | |
| assert abs(bc) < 31.0, bc | |
| tr, half = science.line_tracks(1420.405751768, 83.82, -5.39, np.array([60000.0, 60000.01]), "GBT", | |
| star=dict(rv=20.0, rv_err=0.5, plx=50.0, pmra=10.0, pmdec=-5.0)) | |
| df = (tr["ssb"] - 1420.405751768) / 1420.405751768 * science.C | |
| assert np.all(np.abs(df) < 31.0) and set(tr) == {"cmb", "ssb", "star"}, (df, tr.keys()) | |
| return "Paranal correction %.3f km/s; GBT SSB offset %.3f km/s" % (bc, df[0]) | |
| def t_track(): | |
| rng = np.random.default_rng(3) | |
| nt, nch = 16, 40000 | |
| z = rng.normal(0, 1, (nt, nch)).astype(np.float32) | |
| path_ch = 20000 + 3.0 * np.arange(nt) | |
| z[np.arange(nt), path_ch.astype(int)] += 2.5 | |
| foff, f_first = 2.8e-6, 1420.0 | |
| deltas = np.arange(-15000, 15001, 500) | |
| out = science.track_search(z, f_first + foff * path_ch, f_first, foff, 3, deltas) | |
| p = science.gumbel_p(out["null"], out["s"].max()) | |
| assert out["s"].max() > 7 and p < 1e-3 and out["flat"].max() < out["s"].max(), (out["s"].max(), p) | |
| return "track %.1f sigma, zero-drift %.1f, p %.1e" % (out["s"].max(), out["flat"].max(), p) | |
| def t_trend(): | |
| rng = np.random.default_rng(4) | |
| t = np.sort(rng.uniform(1890, 1990, 400)) | |
| lab = np.where(t < 1940, "A", "B") | |
| def curve(s): | |
| return t, 10.0 + s * (t - 1940) / 100.0 + rng.normal(0, 0.15, len(t)), lab | |
| r = science.differential_trend(curve(0.30), [curve(0.0) for _ in range(6)]) | |
| assert abs(r["diff"] - 0.30) < 0.06 and r["sigma"] > 3, r | |
| return "difference %.3f mag/century, %.1f sigma, device %s" % (r["diff"], r["sigma"], science._device()) | |
| def t_ratio(): | |
| w = np.linspace(0.75, 5.0, 300) | |
| tgt = [dict(wave=x, flux=1.0 + (0.1 if x > 4 else 0.0), err=0.01) for x in w] | |
| ctl = [dict(wave=x, flux=1.0, err=0.01) for x in w] | |
| c, r, e = science.binned_ratio(tgt, [ctl, ctl], np.linspace(0.75, 5.0, 18)) | |
| assert abs(r[c > 4.2].mean() - 1.1) < 0.01 and abs(r[c < 3].mean() - 1.0) < 0.01, r | |
| return "ratio above 4.2 um %.3f" % r[c > 4.2].mean() | |
| def t_coverage(): | |
| from setitools import core | |
| path = os.path.join(tempfile.mkdtemp(), "coverage.db") | |
| db = core.connect(path) | |
| sel = core.ids_selection(["gaia_dr3:1", "gaia_dr3:2", "gaia_dr3:3"]) | |
| core.record_search("test_run", "Test approach", archive="test", selection=sel, code=__file__, db=db, | |
| n_targets=3, n_unit="stars") | |
| core.record_targets("test_run", [dict(target="gaia_dr3:1", stat=2.5, p=0.3), | |
| dict(target="gaia_dr3:2", vet="cosmic-ray"), | |
| dict(target="gaia_dr3:3", vet="X-ray source (eRASS1); binary (RUWE 2.1, NSS 0)", | |
| note="5 pc")], db=db) | |
| card = core.card("gaia_dr3:2", db) | |
| row = db.execute("SELECT vet, note FROM coverage WHERE target = 'gaia_dr3:3'").fetchone() | |
| assert core.covered("gaia_dr3:1", "Test approach", db) == ["test_run"] and card[0][7] == "cosmic-ray", card | |
| assert row == ("active-star; binary", "X-ray source (eRASS1); binary (RUWE 2.1, NSS 0); 5 pc"), row | |
| assert not core.covered("gaia_dr3:4", db=db) | |
| try: | |
| core.record_targets("test_run", [dict(target="gaia_dr3:4", vet="an explanation with no code")], db=db) | |
| raise AssertionError("an explanation without a code was recorded") | |
| except KeyError: | |
| pass | |
| db.close() | |
| return "three targets recorded and read back; a worded vet stored as its codes; an uncoded one refused" | |
| def synthetic_plate(nfib=40, npix=4000, seed=0, sigma=1.0): | |
| """Synthetic sky-fibre plate: Gaussian noise of known variance and a sky model with thirty airglow lines.""" | |
| rng = np.random.default_rng(seed) | |
| lam = 10 ** (3.5563 + 1e-4 * np.arange(npix)) | |
| cont = 1.0 + 0.3 * np.sin(np.arange(npix) / 700.0) | |
| sky = np.tile(cont, (nfib, 1)) | |
| glow = rng.choice(np.arange(60, npix - 60), 30, replace=False) | |
| for j in glow: | |
| sky[:, j - 1:j + 2] += 8.0 | |
| return dict(lam=lam, sky=np.arange(nfib), flux=rng.normal(0, sigma, (nfib, npix)), | |
| ivar=np.full((nfib, npix), 1.0 / sigma ** 2), skym=sky), glow | |
| def t_skyfibre(): | |
| from setitools import lasers as L | |
| k = np.arange(-4, 5) | |
| w = np.exp(-0.5 * (k / L.skyfibre_LSF_PX) ** 2) | |
| gain = math.sqrt(float(np.sum(w ** 2))) | |
| clean = lambda p: p[0] > 0 and not p[5] # noqa: E731 | |
| # uninjected plate: the false-alarm rate of clean peaks per fibre | |
| d0, glow = synthetic_plate(seed=11) | |
| false = sum(1 for p in L.skyfibre_detect(d0, common=False, shared_min=1)[0] if clean(p)) | |
| rng = np.random.default_rng(12) | |
| rec = {1: [0, 0], 2: [0, 0]} | |
| for trial in range(3): | |
| d, glow = synthetic_plate(seed=20 + trial) | |
| _, on_line, _ = L.skyfibre_detect(d, common=False) | |
| ok = np.nonzero(~on_line)[0] | |
| ok = ok[(ok > 10) & (ok < len(d["lam"]) - 10)] | |
| where = [(i, int(rng.choice(ok))) for i in range(len(d["sky"]))] | |
| for i, j in where: | |
| d["flux"][i][j - 4:j + 5] += 10.0 / gain * w # S/N 10 in the matched filter | |
| for rule in (1, 2): | |
| peaks = L.skyfibre_detect(d, common=False, shared_min=rule)[0] | |
| for i, j in where: | |
| rec[rule][1] += 1 | |
| rec[rule][0] += any(clean(p) and p[1] == i and abs(p[2] - j) <= 1 for p in peaks) | |
| # hard negatives: a line in six fibres, on an airglow line, on a masked pixel, and an absorption dip; a one-pixel spike passes the width test | |
| d, glow = synthetic_plate(seed=30) | |
| j_sh, j_sp, j_gl, j_m, j_dip = 900, 1700, int(glow[0]), 2600, 3300 | |
| for i in range(6): | |
| d["flux"][i][j_sh - 4:j_sh + 5] += 10.0 / gain * w | |
| d["flux"][10][j_sp] += 25.0 | |
| d["flux"][11][j_gl - 4:j_gl + 5] += 10.0 / gain * w | |
| d["flux"][12][j_m - 4:j_m + 5] += 10.0 / gain * w | |
| d["ivar"][12][j_m] = 0.0 | |
| d["flux"][13][j_dip - 4:j_dip + 5] -= 10.0 / gain * w | |
| peaks = L.skyfibre_detect(d, common=False, shared_min=1)[0] | |
| near = lambda i, j: [p for p in peaks if p[1] == i and abs(p[2] - j) <= 1] # noqa: E731 | |
| assert all(near(i, j_sh) and all("shared" in p[5] for p in near(i, j_sh)) for i in range(6)), "shared line kept" | |
| spike = [p for p in near(10, j_sp) if p[0] > 0] | |
| assert not any(clean(p) for p in near(11, j_gl)), near(11, j_gl) | |
| assert not any(clean(p) for p in near(12, j_m)), near(12, j_m) | |
| assert not any(clean(p) for p in near(13, j_dip)) and any(p[0] < 0 for p in near(13, j_dip)), near(13, j_dip) | |
| # two strong lines at one redshift mark a galaxy; a lone line or [S II] pair does not | |
| z = 0.3 | |
| gal = L.skyfibre_lines(sorted([6562.80 * (1 + z), 5006.84 * (1 + z)])) | |
| sii = L.skyfibre_lines(sorted([6716.44 * (1 + z), 6730.82 * (1 + z)])) | |
| assert gal and abs(gal[0] - z) < 1e-3 and L.skyfibre_lines([7000.0]) is None and sii is None, (gal, sii) | |
| r1, r2 = rec[1][0] / rec[1][1], rec[2][0] / rec[2][1] | |
| assert r1 > 0.7 and r2 > 0.9 and false <= 1, (r1, r2, false) | |
| return ("S/N 10 lines recovered %.2f (one-fibre rule) and %.2f (two-fibre); %d clean peaks in %d noise fibres; " | |
| "a one-pixel spike %s" % (r1, r2, false, len(d0["sky"]), | |
| "kept at S/N %.0f, width %.2f px" % (spike[0][3], spike[0][4]) if spike and | |
| clean(spike[0]) else "set aside")) | |
| def t_glints(): | |
| """Isolated-flash completeness is bounded by the quiet-neighbour rule: 0.954^8 = 0.69 in white noise.""" | |
| from setitools import flashes as F | |
| rng = np.random.default_rng(40) | |
| t = np.arange(0.0, 60.0, F.kepler_CAD) | |
| q = np.zeros(len(t), int) | |
| def curve(): | |
| return 1.0 + 0.002 * np.sin(2 * np.pi * t / 7.3) + rng.normal(0, 780e-6, len(t)) | |
| sig = 780e-6 | |
| P, t0 = 3.3, 0.71 | |
| f = curve() | |
| inj = [t0 + n * P for n in range(int((t[-1] - t0) / P) + 1)] | |
| for ti in inj: | |
| f[int(round((ti - t[0]) / F.kepler_CAD))] += 10 * sig | |
| found, good = F.kepler_flashes(t, f, q) | |
| got = [x for x in found if min(abs(x[0] - ti) for ti in inj) < 2 * F.kepler_CAD] | |
| n0, fap = F.kepler_phase_cluster([x[0] for x in got], good, P, t0, rng, ntrial=300) | |
| # the same number of flashes at random times | |
| g = curve() | |
| rand = rng.uniform(t[0], t[-1], len(inj)) | |
| for ti in rand: | |
| g[int(round((ti - t[0]) / F.kepler_CAD))] += 10 * sig | |
| found_r, good_r = F.kepler_flashes(t, g, q) | |
| n0_r, fap_r = F.kepler_phase_cluster([x[0] for x in found_r], good_r, P, t0, rng, ntrial=300) | |
| # a six-cadence flare and a flash beside a dip are rejected; pure noise gives none | |
| h = curve() | |
| h[20000:20006] += 10 * sig | |
| h[40000] += 10 * sig | |
| h[40002:40005] -= 5 * sig | |
| found_h, _ = F.kepler_flashes(t, h, q) | |
| bad = [x for x in found_h if abs(x[0] - t[20003]) < 0.01 or abs(x[0] - t[40000]) < 0.01] | |
| noise, _ = F.kepler_flashes(t, curve(), q) | |
| # completeness over isolated flashes in fresh noise, against the quiet-neighbour bound | |
| hits = n_inj = 0 | |
| for trial in range(4): | |
| c = curve() | |
| idx = rng.choice(np.arange(100, len(t) - 100, 40), 200, replace=False) | |
| c[idx] += 10 * sig | |
| fl, _ = F.kepler_flashes(t, c, q) | |
| ft = np.array([x[0] for x in fl]) | |
| hits += sum(1 for i in idx if len(ft) and np.min(np.abs(ft - t[i])) < 2 * F.kepler_CAD) | |
| n_inj += len(idx) | |
| comp = hits / n_inj | |
| assert 0.6 < comp < 0.8 and fap < 0.01 and fap_r > 0.05 and not bad and len(noise) <= 2, ( | |
| comp, fap, fap_r, bad, len(noise)) | |
| return ("%d of %d periodic flashes found, phase FAP %.3f; random flashes FAP %.2f; completeness for 10 sigma " | |
| "flashes %.2f against 0.69 from the quiet-neighbour rule; %d flashes in pure noise" % ( | |
| len(got), len(inj), fap, fap_r, comp, len(noise))) | |
| def t_meteors(): | |
| from setitools import solar_system as SS | |
| rng = np.random.default_rng(50) | |
| def iso(n): | |
| x = rng.normal(size=(n, 3)) | |
| return x / np.linalg.norm(x, axis=1)[:, None] | |
| def around(axis, n, deg): | |
| a = np.asarray(axis, float) / np.linalg.norm(axis) | |
| x = a[None, :] + np.radians(deg) * rng.normal(size=(n, 3)) | |
| return x / np.linalg.norm(x, axis=1)[:, None] | |
| nb = 20000 | |
| xyz_b, v_b = iso(nb), np.abs(rng.normal(10.0, 5.0, nb)) | |
| axis = (0.3, -0.8, 0.52) | |
| for label, v_s in (("coherent", rng.normal(12.0, 0.4, 16)), ("incoherent", rng.uniform(1.0, 40.0, 16))): | |
| xyz = np.vstack([xyz_b, around(axis, 16, 0.25)]) | |
| v = np.concatenate([v_b, v_s]) | |
| _, _, p = SS.meteors_pvalues(xyz, v) | |
| hit = int(np.sum(p[nb:] < 1e-4)) | |
| if label == "coherent": | |
| groups = SS.meteors_groups(xyz, v, p < 1e-4) | |
| big = max((len([k for k in g if k >= nb]) for g in groups), default=0) | |
| coherent = (hit, big) | |
| else: | |
| incoherent = hit | |
| _, _, p0 = SS.meteors_pvalues(xyz_b, v_b) | |
| false = int(np.sum(p0 < 1e-4)) | |
| assert coherent[0] >= 12 and coherent[1] >= 12 and incoherent <= 3 and false <= 10, (coherent, incoherent, false) | |
| return "coherent stream: %d of 16 members below p 1e-4, %d in one group; speed-incoherent group: %d; background: %d of %d" % ( | |
| coherent[0], coherent[1], incoherent, false, nb) | |
| def t_owner(): | |
| from setitools import ultraviolet as uv | |
| star = dict(ra=269.452, dec=4.693, pmra=-801.6, pmdec=10362.4) # Barnard's star | |
| comp = dict(star, ra=star["ra"] + 10 / 3600.0) # a companion 10 arcsec east | |
| mjd = 58700.0 | |
| yrs = (mjd - 51544.5) / 365.25 | |
| ra = star["ra"] + star["pmra"] / 3.6e6 * yrs / math.cos(math.radians(star["dec"])) | |
| dec = star["dec"] + star["pmdec"] / 3.6e6 * yrs | |
| d_now = float(uv.uv_track_dist(star, [ra], [dec], [mjd])[0]) | |
| d_cat = float(uv.uv_track_dist(star, [star["ra"]], [star["dec"]], [mjd])[0]) | |
| d_comp = float(uv.uv_track_dist(comp, [ra], [dec], [mjd])[0]) | |
| owner = uv.uv_owner({"star": d_now, "companion": d_comp}, 3.0) | |
| assert d_now < 0.01 and d_cat < 0.01 and 9.5 < d_comp < 10.5 and owner == "star", (d_now, d_cat, d_comp, owner) | |
| groups = uv.uv_groups([1.0, 1.01, 1.02, 2.0], [0.015] * 4) | |
| assert groups == [[0, 1, 2], [3]], groups | |
| return "epoch and catalogue positions %.3f and %.3f arcsec from the track, companion %.1f" % (d_now, d_cat, d_comp) | |
| def t_docstrings(): | |
| import ast | |
| import glob | |
| root = os.path.dirname(os.path.abspath(__file__)) | |
| files = (sorted(glob.glob(os.path.join(root, "setitools", "*.py"))) + [os.path.abspath(__file__)] + | |
| sorted(glob.glob(os.path.join(root, "papers", "*", "*.py")))) | |
| bad, n = [], 0 | |
| for f in files: | |
| for node in ast.walk(ast.parse(open(f, encoding="utf-8").read())): | |
| if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Module)): | |
| d = ast.get_docstring(node, clean=False) | |
| if d is not None: | |
| n += 1 | |
| if chr(10) in d.strip(): | |
| bad.append("%s:%d %s" % (os.path.relpath(f, root), node.body[0].lineno, getattr(node, "name", "module"))) | |
| assert not bad, bad[:10] | |
| return "%d docstrings in %d files" % (n, len(files)) | |
| def online(): | |
| from setitools import core | |
| def a(): | |
| return "%d row" % len(core.gaia("SELECT TOP 1 source_id FROM gaiadr3.gaia_source")) | |
| def b(): | |
| return "%d row" % len(core.simbad("SELECT TOP 1 main_id FROM basic")) | |
| def c(): | |
| return "%d spectra near HD 10700" % len(core.eso_spectra(26.017, -15.937, "HARPS")) | |
| def d(): | |
| return "%d row" % len(core.irsa("SELECT TOP 1 designation FROM allwise_p3as_psd")) | |
| def e(): | |
| from setitools import core | |
| return "%d row" % len(core.tap("vizier", 'SELECT TOP 1 * FROM "I/239/hip_main"')) | |
| for fn in (a, b, c, d, e): | |
| fn() | |
| def selftest(): | |
| for fn in (t_matched, t_spiky, t_lsf, t_recurrence, t_dropout, t_twin, t_absorption, t_frames, t_track, t_trend, | |
| t_ratio, t_coverage, t_owner, t_skyfibre, t_glints, t_meteors, t_docstrings): | |
| fn() | |
| if "--online" in sys.argv: | |
| online() | |
| print() | |
| for name, ok, msg, dt in CHECKS: | |
| print("%-4s %-80s %5.1fs %s" % ("ok" if ok else "FAIL", name, dt, msg)) | |
| sys.exit(0 if all(r[1] for r in CHECKS) else 1) | |
| def main(): | |
| argv = list(sys.argv) | |
| if len(argv) > 1 and argv[1] in ("selftest", "store"): | |
| from setitools.core import quiet_warnings | |
| quiet_warnings() | |
| sys.argv = [argv[0]] + argv[2:] | |
| if argv[1] == "selftest": | |
| return selftest() | |
| from setitools import core | |
| return core.main() | |
| from setitools import run | |
| return run() | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |