Skip to content
Merged
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
Binary file added docs/images/k2-catalogue-brightness-maps.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions docs/stellar-brightness-maps-from-tess.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ given no verdict, and is not fetched.
| Kind of star and light curve | Method | State |
| --- | --- | --- |
| Between 3250 and 6250 K with log g over 4.2; the K2 mission's PDC-MAP light curve of a campaign, campaigns 0 to 18 without 9 | Reinhold & Hekker (2020, A&A 635, A43) | Wired |
| A star in the paper's catalogue of rotation periods; the K2 mission's PDC-MAP light curve of each campaign it lists | Reinhold & Hekker (2020, A&A 635, A43): the paper's own verdict, read from its catalogue (VizieR J/A+A/635/A43) | Wired, for a star no method and no TESS paper gives a rotation |
| A dwarf by the cuts of Ciardi et al. (2011); the TESS mission's 2-minute PDC-MAP light curve of a sector | Holcomb et al. (2022, ApJ 936, 138) | Wired |
| A target in the paper's catalogue of rotators; the TESS mission's 2-minute light curve of a sector, sectors 1 to 26 | Colman et al. (2024, AJ 167, 189): the paper's own verdict, read from its table | Wired, for a star Holcomb et al.'s method refuses |
| A TESS Object of Interest in the paper's table of unambiguous rotation periods; the TESS mission's 2-minute light curves of its sectors among 1 to 22 | Canto Martins et al. (2020, ApJS 250, 20): the paper's own verdict, read from its tables | Wired, for a star Holcomb et al.'s method refuses |
Expand Down Expand Up @@ -616,6 +617,26 @@ arcseconds of 359 of the 786 stars, and a KELT one of 129. What is missing for t
light, star by star: the rotation of a planet's host is mostly a sentence in the planet's own paper, on seasons the
public release does not always hold. That is a lead, not followed here.

### K2: the verdict Reinhold & Hekker (2020) published

Their method is wired and run here on the K2 light MAST serves now, and it can refuse a star their own run accepted: on
2026-10-19, 12 of the 38 stars with a page that their catalogue lists had no map, because the peak came out under 0.3
here, the three periods a little further apart, or the star's record put it outside their temperature and gravity
ranges, which they took from the EPIC catalogue. Their catalogue (VizieR J/A+A/635/A43, table2: one row a star and a
campaign, with the period, its error, the peak and the variability range) is their verdict, and
[`reinhold-hekker-table.mts`](../packages/telescope-cli/src/archives/tess/reinhold-hekker-table.mts) reads it as a table
entry, asked after the TESS papers. A star is found by its place, within K2's 4 arcseconds, from the EPIC positions the
table prints. Three things are this repository's reading: a star with rows of several campaigns takes the paper's own
rule for its method (the mean, and no verdict past 20% apart; K2-95, 23.85 and 11.38 d, gets none); every campaign the
table lists must be among the star's light curves at MAST; and campaigns 10 and 11 are read as their second, longer
parts. The swing is the table's, the mean of the campaigns' variability ranges. Of the 12, 11 have a map.

![The 11 stars' pages as they open on Color + brightness](images/k2-catalogue-brightness-maps.webp)

Each star's page as it opens in the dev server on 2026-10-23, at 800 px wide, cropped to the card and the star: HD 73344,
K2-105, K2-117, K2-152 (top row), K2-162, K2-174, K2-18, K2-31, K2-33, K2-90 and WASP-157. The darker patches are drawn far
stronger than the real contrast, on each map's own scale.

### K2: a one-star paper's own verdict

Some stars' rotation was measured only in a paper about that one star, which prints the period in its text and keeps no
Expand Down
4 changes: 4 additions & 0 deletions packages/telescope-cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ star passes behind the Sun, the one-turn limit, and the light's swing, which is
northern paper (Newton et al. 2016) is not wired: its one rotator among the stars with a page is flagged for a bright
contaminant.

Reinhold & Hekker's own K2 catalogue is a table entry of the same kind (`tess/reinhold-hekker-table.mts`): asked after
the TESS papers for a star no method gives a rotation, found by the EPIC place it prints, and judged by the paper's rule
for a star seen in several campaigns. Their method, run here, can refuse a star their run accepted.

A star whose rotation only a paper about that one star measured, and prints in its text, is the fourth kind of entry
(`tess/one-star.mts`, its entries in `tess/one-star.json`): the quoted sentence, the K2 target's number and place, the
campaign and the file the paper measured, which MAST serves (today the EVEREST light curve, read as lightkurve reads that
Expand Down
16 changes: 16 additions & 0 deletions packages/telescope-cli/src/archives/tess/reduce.mts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { apart, contaminationRatio, sectorLightCurvesAt } from './light-curves.m
import { GAIA_EPOCH_YEAR, pixelLight, PIXELS, type PixelLight } from './neighbours.mts';
import { catalogueSays, filled, methodFor, METHODS, missionLightCurve, SAME_STAR_ARCSEC, type PeriodMethod, type StarKind, type StarResult, type Transit } from './methods.mts';
import { PUBLISHED } from './papers.mts';
import { REINHOLD_HEKKER_2020_TABLE, rowAtPlace, windowOfCampaign } from './reinhold-hekker-table.mts';
import { lightSwing, oneStarFor, oneStarSays, oneStarVerdict, readOneStar, type OneStarPaper } from './one-star.mts';
import { keptAsPublished, publishedApart, publishedRows, ticOf, type Catalogued, type PublishedFinding, type PublishedPaper } from './published.mts';
import { besideCatalogued, blended, notTurning, withinBreakup, type Mission, type RotationVerdict } from './verdict.mts';
Expand Down Expand Up @@ -219,6 +220,21 @@ export async function reduceStar(id: string, light?: StarLight): Promise<Record<
for (const window of accepted) { const entry = tried.find(one => one.mission === last!.mission && one.window === window.window && one.method === last!.method.id);
tried.push({ mission: last.mission, window: window.window, method: paper.id, lightCurveFile: entry?.lightCurveFile, frames: entry?.frames, lightCurve: entry?.lightCurve, analysis: measures, says, verdict: said.verdict }); } }
if (!rotation.detected) rotation = { detected: false, reason: reasons.join(' ') }; } }
// A star no method and no TESS paper gives a rotation, whose K2 target Reinhold & Hekker (2020) list in their own
// catalogue (reinhold-hekker-table.mts): their verdict, on the K2 light they judged. The light is the method's
// prepared light where the method was run on it here, and otherwise the mission's file as published.
if (!claimed && !rotation.detected && !star.otherLight && await isStar(id)) { const paper = REINHOLD_HEKKER_2020_TABLE, row = rowAtPlace((await publishedRows(paper, keptTable(paper.id))).values(), targetPlaces(star, 'K2'));
if (row) { const k2 = campaigns.length ? campaigns : (await lightCurvesAt(targetPlaces(star, 'K2'))).map(one => ({ window: one.campaign, filename: one.filename, uri: one.uri })), judgement = paper.judge(row, k2.map(one => one.window)), said = judgement.verdict;
const kept = said.detected ? keptAsPublished(paper, said, checked(said), star.cataloguedPeriodDays, await cataloguedOf(id)) : said; claimed ||= said.detected;
published = { id: paper.id, citation: paper.citation, url: paper.url, table: paper.table, epic: row.epic, row, verdict: said };
if (!kept.detected) { reasons.push(kept.reason!); rotation = { detected: false, reason: reasons.join(' ') }; }
else { const ran = [...refused, ...(last ? [last] : [])].find(one => one.mission === 'K2'), read: { window: number; time: readonly number[]; flux: readonly number[] }[] = [], named: Listed[] = [];
for (const one of k2.filter(file => judgement.windows.includes(file.window))) { const judged = ran?.judged.windows.find(window => window.window === windowOfCampaign(one.window)), name = `${stem('K2', windowOfCampaign(one.window))}.curve.json`;
const [file] = await fetchLightCurves([one], files), curve = judged ?? await missionLightCurve(file!.file);
if (!judged) await writeFile(resolve(run, name), `${JSON.stringify({ mission: 'K2', window: curve.window, product: 'pdcsap', time: curve.time, flux: curve.flux })}\n`);
tried.push({ mission: 'K2', window: curve.window, method: paper.id, lightCurveFile: { url: file!.url, bytes: file!.bytes, pipeline: curve.pipeline, ...curve.aperture }, frames: curve.frames, lightCurve: name, analysis: paper.measures(row), says: paper.says(row), verdict: said });
read.push({ window: curve.window, time: curve.time, flux: curve.flux }); named.push({ ...one, window: curve.window }); }
rotation = kept; by = paper; mission = 'K2'; listed = named; accepted = read; } } }
// A star no method and no paper above gives a rotation: Kepler's light of it, on the verdict of Santos et al. Their
// samples are main-sequence stars and subgiants, cut at log g 3.5; a star its record puts below is not asked for.
if (!claimed && !star.otherLight && !((kind.surfaceGravityLogg ?? SANTOS_GRAVITY) < SANTOS_GRAVITY) && await isStar(id)) { const targets = await kepseismicAt(targetPlaces(star, 'Kepler')), kic = targets[0]?.kic; let there = false;
Expand Down
82 changes: 82 additions & 0 deletions packages/telescope-cli/src/archives/tess/reinhold-hekker-table.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/** A K2 star's rows in the catalogue of Reinhold & Hekker (2020): the paper's own verdict on the star.
*
* Reinhold & Hekker (2020), "Stellar rotation periods from K2 Campaigns 0-18", A&A 635, A43, Sects. 2 and 3; its
* catalogue is VizieR J/A+A/635/A43, table2. The method itself is wired (methods.mts `REINHOLD_HEKKER_2020`) and run on
* the K2 light the mission publishes now. This entry is the paper's table, of the published-verdict kind
* (published.mts): nothing is measured or judged here. It is asked of a star no method gives a rotation, as the TESS
* papers' tables are: the method run here can refuse a star the paper's own run accepted (a peak just under 0.3, three
* periods a little further apart), and it is not run at all on a star whose record holds a temperature or a gravity
* outside the paper's ranges, which the paper took from the EPIC catalogue, not from the star's record.
*
* What a row asserts (Sect. 3 and the table's ReadMe): the catalogue "contains the measured rotation periods" of the
* stars that passed the paper's criteria, one row a star and a campaign ("32,387 periods because several stars have been
* observed more than once"). `Prot` is the period, `e_Prot` its error, `HPeak` the periodogram's peak, `Rvar` the
* variability range in percent (the light's 95th less its 5th percentile, Sect. 2), and `_RA`, `_DE` the target's place
* from the EPIC catalogue. The light is the mission's own PDC-MAP light curve of each campaign listed.
*
* What is taken: a star is found by its place (within K2's 4 arcseconds of its place in one of K2's years, as K2's light
* curves are matched to it), so no archive is asked of a star the table does not list. A star with rows of several
* campaigns takes the paper's rule (Sect. 3.1, as `REINHOLD_HEKKER_2020.star` applies it): the mean of the campaigns'
* periods, and no verdict when they deviate by more than 20%. Every campaign the table lists must be among the star's
* K2 light curves at MAST, or the verdict is not on light the route can read. Campaigns 10 and 11 were split in two by
* the mission, and the route reads each one's longer, second part (kepler/light-curves.mts `SPLIT_CAMPAIGNS`): a row of
* campaign 10 is read as that part, whose light the file names window 102 (`windowOfCampaign`). That is this entry's reading of the table: the paper gives about 50 to 70
* days of light for campaigns 10 and 11 (Sect. 2) and does not say which part. */
import { VIZIER_TAP } from '../espadons/catalogue.mts';
import { MATCH_DEGREES, SPLIT_CAMPAIGNS } from '../kepler/light-curves.mts';
import type { PublishedJudgement, PublishedVerdict } from './published.mts';

const CATALOGUE = 'J/A+A/635/A43', TABLE = `${CATALOGUE}/table2`, COLUMNS = ['EPIC', 'Campaign', 'Prot', 'e_Prot', 'HPeak', 'Rvar', '_RA', '_DE'] as const, PAPER = 'Reinhold & Hekker (2020)';
/** How far apart two campaigns' periods may lie, as a share of their mean (Sect. 3.1). */
const DEVIATION = 0.2;
/** One campaign's row. */
export interface ReinholdHekkerCampaign { readonly campaign: number; readonly periodDays: number; readonly errorDays: number; readonly peakHeight: number; /** As a share of the mean light. */ readonly variabilityRange?: number }
/** A star's rows: its EPIC number and place, and one entry a campaign. */
export interface ReinholdHekkerRow { readonly epic: number; readonly raDegrees: number; readonly decDegrees: number; readonly campaigns: readonly ReinholdHekkerCampaign[] }

/** The star's rows: the nearest target within K2's 4 arcseconds of one of its places (reduce.mts `targetPlaces`). */
export function rowAtPlace(rows: Iterable<ReinholdHekkerRow>, places: readonly { readonly raDegrees: number; readonly decDegrees: number }[]): ReinholdHekkerRow | undefined {
let best: ReinholdHekkerRow | undefined, least = MATCH_DEGREES;
for (const row of rows) for (const place of places) { const apart = Math.hypot((row.raDegrees - place.raDegrees) * Math.cos(place.decDegrees * Math.PI / 180), row.decDegrees - place.decDegrees); if (apart <= least) { least = apart; best = row; } }
return best;
}
/** The window a light curve file of a campaign is read as (reduce.mts), for a campaign the table names. */
export const windowOfCampaign = (campaign: number) => SPLIT_CAMPAIGNS.includes(campaign) ? Number(`${campaign}2`) : campaign;
const two = (days: number) => Number(days.toFixed(2));

/** The table's rows, by the star's EPIC number. */
export function parseReinholdHekker(answered: readonly Readonly<Record<string, string>>[]): Map<number, ReinholdHekkerRow> {
const rows = new Map<number, { epic: number; raDegrees: number; decDegrees: number; campaigns: ReinholdHekkerCampaign[] }>();
for (const cells of answered) {
const number = (column: typeof COLUMNS[number], optional = false) => { const printed = (cells[column] ?? '').trim(), value = Number(printed);
if (printed === '' && optional) return undefined; if (printed === '' || !Number.isFinite(value)) throw new TypeError(`${TABLE}: a row holds no ${column}.`); return value; };
const epic = number('EPIC')!, campaign = number('Campaign')!, range = number('Rvar', true);
if (!Number.isInteger(epic) || !Number.isInteger(campaign)) throw new TypeError(`${TABLE}: EPIC ${epic} or campaign ${campaign} is not a whole number.`);
const row = rows.get(epic) ?? { epic, raDegrees: number('_RA')!, decDegrees: number('_DE')!, campaigns: [] };
if (row.campaigns.some(one => one.campaign === campaign)) throw new TypeError(`${TABLE}: EPIC ${epic} is listed twice for campaign ${campaign}.`);
row.campaigns.push({ campaign, periodDays: number('Prot')!, errorDays: number('e_Prot')!, peakHeight: number('HPeak')!, ...(range === undefined ? {} : { variabilityRange: range / 100 }) });
rows.set(epic, row); }
return rows;
}

/** A star's verdict on its K2 campaigns at MAST (as MAST's listing numbers them: a split campaign by its own number): the paper's period when every campaign it lists is among them and, for
* several, their periods agree within 20%. */
function judge(row: ReinholdHekkerRow, windows: readonly number[]): PublishedJudgement {
const periods = row.campaigns.map(one => one.periodDays), mean = two(periods.reduce((sum, days) => sum + days, 0) / periods.length), no = (reason: string): PublishedJudgement => ({ verdict: { detected: false, reason }, windows: [], gives: mean });
const listed = `${PAPER} list the star with ${row.campaigns.length === 1 ? `a rotation period of ${periods[0]} d in campaign ${row.campaigns[0]!.campaign}` : `rotation periods of ${periods.join(' and ')} d in campaigns ${row.campaigns.map(one => one.campaign).join(' and ')}`}`;
if (Math.max(...periods) - Math.min(...periods) > DEVIATION * mean) return { ...no(`${listed}, which deviate by more than the 20% beyond which the paper excludes a star.`), gives: undefined };
const own = row.campaigns.map(one => one.campaign), missing = own.filter(campaign => !windows.includes(campaign));
if (missing.length) return no(`${listed}, and K2 publishes no light curve of the star for ${missing.length === 1 ? 'that campaign' : 'those campaigns'} at MAST now (campaign ${missing.join(', ')}).`);
const ranges = row.campaigns.flatMap(one => one.variabilityRange === undefined ? [] : [one.variabilityRange]);
return { verdict: { detected: true, periodDays: mean, ...(ranges.length ? { amplitude: Number((ranges.reduce((sum, value) => sum + value, 0) / ranges.length).toFixed(6)) } : {}) }, windows: own, gives: mean };
}

export const REINHOLD_HEKKER_2020_TABLE: PublishedVerdict<ReinholdHekkerRow> = { id: 'reinhold-hekker-2020-catalogue', citation: 'Reinhold & Hekker (2020, A&A 635, A43)', url: 'https://arxiv.org/abs/2001.08214', where: 'Sects. 2 and 3; the catalogue of rotation periods', catalogue: CATALOGUE, table: `VizieR ${TABLE}`,
service: VIZIER_TAP, query: `SELECT ${COLUMNS.map(column => `"${column}"`).join(', ')} FROM "${TABLE}"`, missions: ['K2'],
lightCurve: 'the K2 mission\'s long-cadence PDC-MAP light curve of each campaign the catalogue lists for the star',
asks: 'the periodogram, the wavelet and the autocorrelation of a campaign\'s light to give periods within a day of each other under 10 days, two days to 20 and five beyond, with a periodogram peak over 0.3, a period between a day and half the light\'s span, and a variability range under 10%',
reliability: 'Of the paper\'s stars observed in two campaigns, 75.7% gave periods within 20% of each other.', swing: 'as the table prints it (the mean of the campaigns\' variability ranges)',
parse: parseReinholdHekker, judge,
says: row => row.campaigns.length === 1 ? `a rotation period of ${row.campaigns[0]!.periodDays} ± ${row.campaigns[0]!.errorDays} d in campaign ${row.campaigns[0]!.campaign}, with a periodogram peak of ${row.campaigns[0]!.peakHeight}`
: `rotation periods of ${row.campaigns.map(one => `${one.periodDays} ± ${one.errorDays} d (campaign ${one.campaign})`).join(' and ')}, whose mean is the star's`,
measures: row => ({ epic: row.epic, campaigns: row.campaigns.length, periodDays: two(row.campaigns.reduce((sum, one) => sum + one.periodDays, 0) / row.campaigns.length), peakHeight: Math.min(...row.campaigns.map(one => one.peakHeight)) }) };
Loading
Loading