Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ For a body's sources, processing, evidence and known problems, read its
| Archived polarised spectra of a star to a map of its magnetic field | [A star's magnetic map from archived spectra](stellar-magnetic-maps-from-spectra.md) |
| A star's light in the TESS full-frame images to its rotation period and a brightness map | [A star's rotation and brightness map from TESS pixels](stellar-brightness-maps-from-tess.md) |
| A star's magnetic map to a corona drawn around it | [A star's corona from its magnetic map](stellar-corona-from-magnetic-maps.md) |
| A Cepheid's published light-curve model to its light at ten phases of one cycle | [A pulsating star's light through one cycle](pulsating-stars-light-through-a-cycle.md) |
| JWST images to sky band composites | [JWST imaging](jwst-imaging.md) |
| What JWST's public archive holds and what this project can reduce | [JWST ledger](jwst-ledger.md) |
| Hubble observations re-calibrated from raw and checked against the archive | [Hubble](hubble.md) |
Expand Down
Binary file added docs/images/pulsating-stars-steps.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
138 changes: 138 additions & 0 deletions docs/pulsating-stars-light-through-a-cycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# A pulsating star's light through one cycle

A Cepheid swells and shrinks, and its light rises and falls with the same period. The page of a Cepheid whose light
curve is published shows that cycle as a dataset, **Pulsation**: ten steps, a tenth of a period apart, each the star in
its own color at that moment's share of its brightest light. The steps can be stepped through or played as a loop.

The light is not measured or fitted here. It is the model Gaia DR3 publishes for the star.

## Whose data and whose model

| Step | Whose | What it is |
| --- | --- | --- |
| Light curve and its model | Gaia DR3, table `gaiadr3.vari_cepheid` (Ripepi et al. 2023, [A&A 674, A17](https://doi.org/10.1051/0004-6361/202243990)) | For each Cepheid: the period, and a truncated Fourier series fitted to its G-band time series, published harmonic by harmonic (zero point, reference time, frequency, amplitudes, phases) |
| Reading the model | [`light-curve.ts`](../packages/bake/src/photometry/light-curve.ts) | Reads the row a star's package archives and holds it to the same row's peak-to-peak amplitude, epoch of maximum, R21 and φ21 |
| The ten steps | [`pulsation-steps.mts`](../packages/telescope-cli/src/new-object/pulsation/pulsation-steps.mts) | Evaluates the model a tenth of a period apart from its maximum and turns magnitudes into shares of the light at maximum |
| The page's records | [`surface-maps.mts`](../packages/telescope-cli/src/new-object/maps/surface-maps.mts), the route every map kind uses | Table, manifest entry, raster surfaces, dataset steps, reader text |

The model, as Gaia's [data model page](https://gea.esac.esa.int/archive/documentation/GDR3/Gaia_archive/chap_datamodel/sec_dm_variability_tables/ssec_dm_vari_cepheid.html)
prints it under `phi21_g`:

mag(t) = zp + Σ A_i cos(i · 2π · ν · (t − T_ref) + φ_i)

with `zp_mag_g`, `fund_freq1`, `reference_time_g`, and the first `num_harmonics_for_p1_g` entries of
`fund_freq1_harmonic_ampl_g` and `fund_freq1_harmonic_phase_g`. The reader holds every row to the row's own published
values and refuses one that fails: the series must span `peak_to_peak_g` to 0.0001 mag, peak at `epoch_g` (within its
stated error, or a thousandth of a period), and give the row's `r21_g` and `phi21_g`. A reading with a sine in place of
the cosine misses the epoch by about a third of a cycle.

A magnitude difference to a share of light is arithmetic: 10^(−0.4 (m − m at maximum)).

## What a step draws

Step *k* is the model at phase *k*/10 after maximum light. Its surface is the star's Color dataset's color with each
channel's light multiplied by that share, then encoded for the display (IEC 61966-2-1): a step at 58% gives 58% of the
light of the step at maximum. Nothing is stretched. The disc is unresolved, so one value is drawn over all of it; the
darkening toward the edge is the Color dataset's limb law, from the same file.

![RY CMa's ten steps in the app](images/pulsating-stars-steps.webp)

The page keeps the dataset it opened on. A page that already plays the same model over its disc (the Light curves
switch, a black veil whose opacity follows the model) does not draw the veil over a step: the step already holds its
light, and the presentation profile names the step group for that (`lightCurve.stills`).

## Which stars

A star has the steps when its package keeps a Gaia DR3 `vari_cepheid` row that the reader accepts: a fundamental-mode
model whose harmonics reproduce the row's own published values.

Counted on 2026-10-08, of the 477 Cepheid pages:

| Where | Pages | With steps | Why the others have none |
| --- | --- | --- | --- |
| Milky Way | 54 | 49 | Gaia's table has no row for the star's source (Mekbuda, T Mon, TZ Mon, XX Sgr); Polaris has no Gaia source |
| Magellanic Clouds | 41 | 38 | Gaia's table has no row for the star's source (HV 900, HV 2827, HV 12815) |
| M33 | 154 | 34 | No Gaia Cepheid within an arcsecond of the page's place (111); Gaia's period not within 1% of the catalogue row's (6); a first-overtone model (3) |
| M31 | 52 | 4 | No Gaia Cepheid within an arcsecond of the page's place (48) |
| Beyond 2 Mpc | 176 | 0 | Too faint for Gaia. Their own papers print a period, mean magnitudes and amplitudes, with no epochs and no fitted terms (Hoffmann et al. 2016, `J/ApJ/830/10`: 169 pages) |

A Milky Way or Magellanic star's package names its Gaia source, and the row is that source's. A Cepheid of M31 or M33 is
placed by its paper's table and names no Gaia source: `--from-pulsation gaia:HOST` ties it to the one Cepheid of Gaia's
table within an arcsecond of the page's place whose period is within 1% of the one the package's own catalogue row
prints, and writes the row into the package with those two numbers.

The 44 other pulsating star pages (28 long-period variables and candidates, 13 δ Scuti stars, a Mira, a β Cephei and a
γ Doradus star) have no steps. Gaia's Cepheid table does not hold them. Its long-period-variable table prints a
frequency and an amplitude for 8 of them, with no phase and no harmonics: that is not a light curve to draw a cycle from.

## Where the model leaves Gaia's measurements

A Fourier series is held by the measurements only where there are measurements. Gaia DR3 also releases each star's G-band
epochs (DataLink, `EPOCH_PHOTOMETRY`). Folded on the model's period, the epochs Gaia did not reject (18 to 122 a star)
leave at least one of the ten tenths of the cycle empty for 78 of the 125 stars, and for some the model swings there
beyond anything measured. The six largest, each with its extreme inside the widest stretch of the cycle without a
measurement:

| Star | Model's swing and its printed error (G mag) | Span of the measurements | Model beyond them | Empty stretch of the cycle |
| --- | --- | --- | --- | --- |
| X Pup | 2.49 ± 0.26 | 0.92 (56 epochs) | 1.54 brighter | 0.24 |
| HV 1345 | 1.35 ± 0.93 | 0.68 (48) | 0.67 brighter | 0.26 |
| VHK 25 | 1.44 ± 0.27 | 0.81 (43) | 0.57 fainter | 0.27 |
| VHK 12 | 1.47 ± 0.41 | 0.90 (44) | 0.47 brighter | 0.28 |
| V916 Aql | 1.00 ± 0.11 | 0.65 (35) | 0.33 fainter | 0.34 |
| M33SSS J013330.80+303111.3 | 1.29 ± 0.22 | 1.12 (35) | 0.25 brighter | 0.23 |

Over all 125: the model passes the brightest or the faintest measurement by more than 0.2 mag for these 6, by more than
0.1 mag for 20, by more than 0.05 mag for 35, and by nothing for 31. X Pup's steps read 100, 32, 20, 18, 16, 14, 12,
11, 10 and 25% of maximum, where Gaia's own measurements span a factor of 2.3.

The steps draw the model as published for every star, these six included: no star is left out on this comparison.
Ripepi et al. print a bootstrap error for each amplitude and no rule that says when a model is to be set aside, and a
comparison made here is not such a rule. The Light curves veil plays the same model on X Pup, HV 1345 and V916 Aql.

## What here is not printed in a paper

1. **Ten steps, a tenth of a period apart, the first at maximum light.** A display choice (`PHASES`). The model is
continuous, and its faintest moment falls between two steps.
2. **The G band stands for all colors.** Gaia's G band is wide (330 to 1050 nm). The step dims the star's whole color by
the G-band share; the true change differs from blue to red.
3. **A share of the light at maximum.** The scale's top is the model's own maximum, not a calibrated brightness.
4. **One value over the disc.** The table's four nodes hold the same number: the grid is the file layout the bake
reads, not a map.
5. **Which row is a star's, in M31 and M33.** One arcsecond (the radius the generator already takes for "the star at
this place") and 1% in period (`AT_PLACE_ARCSEC`, `SAME_PERIOD`). Each can only leave a star without steps.

## Not drawn, and why

- **Color.** The star's temperature changes through the cycle, and Gaia DR3 publishes the same kind of model in its BP
and RP bands. Turning that color into a temperature, and so into a drawn color, needs a published calibration that
covers Cepheids in those bands, with reddening handled as its paper does. None was found (searches of 2026-10-08).
Mucciarelli, Bellazzini & Massari (2021, A&A 653, A90) calibrate dwarfs and giants, not supergiants. The steps
therefore keep the star's own color.
- **Size.** The radius changes by several percent through the cycle. A page draws one radius, and no dataset here
changes it.

## Running it

node packages/bake/cli/restore-source-inputs.mts --object=ry-cma # the star's archived Gaia row
pnpm telescope new-object --from-pulsation ry-cma --out output/pulsation/spec.json
pnpm telescope new-object output/pulsation/spec.json --bake

`--from-pulsation all` drafts every star whose package keeps a model. A spec may be run again: the records follow it.

## Limits and leads

- The model is Gaia's fit to 2014 to 2017. A step is a phase of the cycle, not a date.
- Between two measurements a step is the model alone ([where it leaves them](#where-the-model-leaves-gaias-measurements)).
- Gaia measures a star in a crowded field together with whatever shares its window. In M31 and M33 these Cepheids are
at G magnitudes 18.4 to 20.7; the model is of what Gaia measured there, and nothing here corrects it.
- A page of M31 from Li et al. (2021) takes its place from columns printed to a thousandth of a degree, 3.6
arcseconds, so a Cepheid Gaia does hold can lie outside the arcsecond. At least four more would be tied by the five
decimals in the table's own identifier (DIRECT V1791, V1934, V6165 and V7209). Not done: it would move the page's
place too.
- Leads for the stars Gaia's table does not hold, none read yet: the PAndromeda light curves of 2,686 Cepheids in M31
(Kodric et al. 2018, VizieR `J/AJ/156/130`, files `lc_g`, `lc_r`, `lc_i`); the B and V light curves of Vilardell et al.
(2006, `J/A+A/459/321`); the OGLE Collection of Variable Stars for the three Magellanic Cepheids Gaia's table lacks.
- Leads for color: the temperatures measured spectrum by spectrum through the cycle of bright Milky Way Cepheids (Luck
2018, AJ 156, 171, table 3, and the paper Mekbuda's record cites for its gravity from 125 spectra, bibcode
2023A&A...678A.195D), and V and I light curves with a calibration its paper applies to Cepheids phase by phase.
4 changes: 3 additions & 1 deletion docs/reader-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ For example, the Moon's **Mineral composition** group switches between
plagioclase, olivine and the two pyroxenes. Mars groups five chemical elements;
Pluto groups three modeled ice fractions; Ceres groups two mineral absorption
features. Betelgeuse, CE Tauri and WASP-12b group observations by date. BE Ceti,
χ¹ Orionis, HD 29615 and HD 35296 group three magnetic-field directions.
χ¹ Orionis, HD 29615 and HD 35296 group three magnetic-field directions. A Cepheid
such as RY CMa groups ten phases of one pulsation
([method note](pulsating-stars-light-through-a-cycle.md)).

Keep units and limits beside each map. Mars's thorium scale uses parts per
million while its other element scales use weight percent. Ceres's band depths
Expand Down
33 changes: 33 additions & 0 deletions packages/bake/src/presentation/lighting/emissive-stills.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { parsePresentationProfile } from '@cssearth/objects';
import { sourceTest } from '@cssearth/objects/node/source-test';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import assert from 'node:assert/strict';
import { prepareCssPresentation } from '../css-presentation.ts';
import { presentationHostAdapters } from '../../objects/host-adapters/index.ts';
import * as solarGeometry from '../../../../../src/platform/solar-geometry.mts';
import type { PresentationInputs } from '../types.ts';

const test = sourceTest(), root = resolve(import.meta.dirname, '../../../../..');
const read = async (file: string): Promise<unknown> => JSON.parse(await readFile(resolve(root, file), 'utf8'));

// RY CMa plays its Gaia light curve as a veil over its disc, and its Pulsation datasets each draw the star at one phase of
// that light curve.
test('a dataset that is a still of the light curve takes the veil off; a body with no still writes nothing on its veil', { timeout: 60_000 }, async () => {
const id = 'ry-cma', base = `src/objects/${id}/prepared`;
const profile = parsePresentationProfile(await read(`src/objects/${id}/source/preparation/presentation.json`));
const [scene, assets, datasets, sun, controls, solarSource] = await Promise.all([
...['scene', 'assets', 'datasets', 'sun', 'controls'].map(file => read(`${base}/${file}.json`)), read(`src/objects/${id}/source/presentation/solar-system.json`)]);
const stills = (datasets as { controls: { id: string; step?: { group: string } }[] }).controls.filter(control => control.step?.group === profile.lightCurve?.stills).map(control => control.id);
assert.equal(stills.length, 10);
const track = { durationMs: 1559, keyframes: [{ offset: 0, opacity: '0.2010' }, { offset: 0.5, opacity: '0.0000' }, { offset: 1, opacity: '0.2010' }] };
const compile = (lightCurve: PresentationInputs['lightCurve']) => prepareCssPresentation({ namespace: profile.namespace, mode: profile.mode, lightCurve, scene, assets, datasets, sun, controls, solarSource } as PresentationInputs, presentationHostAdapters(solarGeometry));
const prepared = await compile({ ...track, stills }), veil = prepared.tree.nodes.findIndex(node => node.className === `${id}-light-veil`);
assert.ok(veil >= 0); assert.equal(prepared.motion?.[0]?.target, veil);
for (const variant of prepared.variants) assert.deepEqual(variant.writes.filter(write => write.target === veil),
[{ kind: 'style', target: veil, name: 'display', value: stills.includes(String(variant.when.datasetId)) ? 'none' : 'block' }], String(variant.when.datasetId));
// Without stills the veil is written by no selection, as every star that only plays its light curve was prepared.
const played = await compile(track);
assert.ok(played.variants.every(variant => variant.writes.every(write => write.target !== veil)));
await assert.rejects(compile({ ...track, stills: ['no-such-dataset'] }), /names the still no-such-dataset, which is not a dataset of the body/u);
});
5 changes: 5 additions & 0 deletions packages/bake/src/presentation/lighting/emissive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export async function prepareEmissive(input: PresentationInputs, adapters: Prese
const veil = lightCurve ? b.element('div', `${ns}-light-veil`, `position:absolute;left:50%;top:50%;width:${diameter}px;height:${diameter}px;` +
`margin:${-diameter / 2}px 0 0 ${-diameter / 2}px;border-radius:50%;background:#000;opacity:${lightCurve.keyframes[0]!.opacity}`, { 'aria-hidden': 'true' }) : null;
if (veil) b.append(limb, veil);
// A dataset that draws the star at one moment of the light curve already holds that moment's light: the veil would dim
// it a second time, so its selection takes the veil out. A body with no such dataset writes nothing more.
const stills = new Set(veil ? lightCurve?.stills ?? [] : []);
for (const id of stills) if (!datasets.controls.some(dataset => dataset.id === id)) throw new TypeError(`${ns}: the light curve names the still ${id}, which is not a dataset of the body.`);
const { tree, index } = b.finish({ camera, scene });
const targets = [body, body, corona, limb];
// Every dataset gets a variant. One that names a companion cloud draws the plates of the surface it borrows, so the
Expand All @@ -67,6 +71,7 @@ export async function prepareEmissive(input: PresentationInputs, adapters: Prese
...LAYERS.map((layer, i) => ({ kind: 'texture' as const, target: index(targets[i]!), name: `--${ns}-${layer}-image`,
resource: keys.has(`${layer}:${surfaceId}`) ? `${layer}:${surfaceId}` : null, quoted: true })),
{ kind: 'attribute', target: -1, name: 'data-dataset', value: dataset.id }, { kind: 'attribute', target: -1, name: 'data-view', value: null },
...(veil && stills.size ? [{ kind: 'style' as const, target: index(veil), name: 'display', value: stills.has(dataset.id) ? 'none' : 'block' }] : []),
], materials: [] };
});
return { schema: PREPARED_PRESENTATION_SCHEMA, camera: plan.camera, sky: plan.starfield, sun: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ test('a tree whose slots hold no first image keeps its property table, entry for
assert.deepEqual(withTextureImageRecords({ ...expanded, tree: { ...expanded.tree, textureBindings: [{ target: 2, name: '--surface', leaves: [3, 4] }] } }, [{ target: 5, name: 'backgroundImage', node: 5 }]), records);
});

test('a plate that draws its image itself keeps no read of the variable, as a property or inline', () => {
// A star's limb plate with a veil on it: the emissive builder gives the plate its read of the limb variable.
for (const read of ['property', 'inline'] as const) {
const plate = variable(), at = plate.tree.properties.length;
plate.tree.properties.push({ name: 'backgroundImage', value: 'var(--limb)', custom: false });
plate.tree.nodes[5] = read === 'property' ? { parent: 0, style: '', properties: [2, at] } : { parent: 0, style: 'background-image:var(--limb);opacity:1;', properties: [2] };
const records = withTextureImageRecords(plate, containers), node = records.tree.nodes[5]!;
assert.equal(JSON.stringify(records).includes('--'), false, read);
assert.deepEqual([node.style, node.properties.map(id => records.tree.properties[id]!)], [read === 'property' ? '' : 'opacity:1;', [{ name: 'width', value: '32px', custom: false }]], read);
assert.deepEqual(records.variants[0]!.writes[3], texture(5, 'backgroundImage', 'limb:a'));
}
});

test('an element that reads a selected image outside every slot is refused by node', () => {
const stray = variable();
stray.tree.textureBindings = [{ target: 2, name: '--surface', leaves: [3] }];
Expand Down
Loading
Loading