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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/bake/authoring/ngc-3132/grid-fit.mts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ const file = await readFile(resolve(sourceDirectory, 'source.jpg')), tags = file
const tag = (name: string) => { const found = new RegExp(`<avm:${name}>([\\s\\S]*?)</avm:${name}>`, 'u').exec(tags) ?? new RegExp(`avm:${name}="([^"]*)"`, 'u').exec(tags); if (!found) throw new Error(`source.jpg has no avm:${name}.`); return found[1]!.replace(/<[^>]+>/gu, ' ').trim().split(/\s+/u).map(Number); };
const pixelArcsec = Math.abs(tag('Spatial.Scale')[0]!) * 3600, rotation = tag('Spatial.Rotation')[0]! * rad;
const full = await sharp(file).removeAlpha().raw().toBuffer({ resolveWithObject: true }), fullWidth = full.info.width, fullHeight = full.info.height;
// Where the tags alone put the star: the recipe's target, from the tags' reference place and pixel (counted from the bottom).
// Where the tags alone put the star: the recipe's target, from the tags' reference place and pixel (counted from the bottom left, the first pixel's middle at 1, as FITS counts).
const target = (JSON.parse(await readFile(resolve(sourceDirectory, 'recipe.json'), 'utf8')) as { target: { centerRaDeg: number; centerDecDeg: number } }).target, reference = tag('Spatial.ReferenceValue'), referencePixel = tag('Spatial.ReferencePixel'), degrees = Math.abs(tag('Spatial.Scale')[0]!);
const eastPixels = (target.centerRaDeg - reference[0]!) * Math.cos(target.centerDecDeg * rad) / degrees, northPixels = (target.centerDecDeg - reference[1]!) / degrees;
const tagged = [referencePixel[0]! - eastPixels * Math.cos(rotation) - northPixels * Math.sin(rotation), fullHeight - (referencePixel[1]! - eastPixels * Math.sin(rotation) + northPixels * Math.cos(rotation))] as const, within = Math.round(STAR_WITHIN_ARCSEC / pixelArcsec);
const tagged = [referencePixel[0]! - 1 - eastPixels * Math.cos(rotation) - northPixels * Math.sin(rotation), fullHeight - (referencePixel[1]! - eastPixels * Math.sin(rotation) + northPixels * Math.cos(rotation))] as const, within = Math.round(STAR_WITHIN_ARCSEC / pixelArcsec);
// The star: the largest patch of saturated pixels that starts within that reach of the place.
const saturated = (p: number) => full.data[3 * p]! >= 250 && full.data[3 * p + 1]! >= 250 && full.data[3 * p + 2]! >= 250, seen = new Uint8Array(fullWidth * fullHeight); let star = { pixels: 0, x: 0, y: 0, from: 0 };
for (let y = Math.max(0, Math.round(tagged[1] - within)); y <= Math.min(fullHeight - 1, tagged[1] + within); y++) for (let x = Math.max(0, Math.round(tagged[0] - within)); x <= Math.min(fullWidth - 1, tagged[0] + within); x++) { const start = y * fullWidth + x; if (seen[start] || !saturated(start) || Math.hypot(x - tagged[0], y - tagged[1]) > within) continue; let pixels = 0, sumX = 0, sumY = 0; const stack = [start]; seen[start] = 1;
Expand Down
5 changes: 5 additions & 0 deletions packages/telescope-cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ own through `workspace-commands/new-object.mts`, `new-object-cli.mts` is the sta
The generated solar geometry (`src/platform/solar-geometry.mts`) stays generated: the entries load it with `solar-epoch.mts` and
pass the epoch down, so no module here imports it.

A published picture as one more dataset of a page is `src/new-object/pictures/`: `--from-esa` and a spec's `pictures`. `esa-image.mts` reads
ESA's picture pages and the sky tags in a JPEG, `registration.mts` owns the arithmetic from those tags and one pixel to a recipe's
observation, plane and rim, `picture-bank.mts` writes one entry's records from the bank it is like, and `pictures.mts` drafts, writes
and bakes. It never writes a sentence a reader sees and never picks a value for a color: those are the spec's, written by a person.

The star survey behind `telescope stars GALAXY` is `src/stars/`. It and the generator's `--from-table` route
(`src/new-object/archives/tables/table-stars.mts`) read VizieR's table metadata and SIMBAD through the same two modules,
`vizier-tables.mts` and `simbad-tap.mts` beside it; a star class is a branch of SIMBAD's own type tree, never a
Expand Down
2 changes: 2 additions & 0 deletions packages/telescope-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ In a terminal, `explore` shows the observations, unknown metadata, unsupported r

`telescope simulations OBJECT` lists the Zenodo dataset records that name the object and speak of a simulation or a model, each with its license, size and files. It downloads nothing, and a listed record is a lead. To show one, read its paper and write an entry for `node packages/telescope-cli/src/new-object/new-object-cli.mts --simulation entries.json` ([simulation-dataset.mts](src/new-object/simulation/simulation-dataset.mts) documents the entry). That command checks the record's license, that it names the object and that it lists the file, and writes the dataset with its labels. It never fetches the release whole: two range requests bring the file's first bytes (its header and coordinate variables) and the bytes of the selected grid into the body's source directory, each declared with its byte range in the source manifest. A 3.5 GB run costs about 130 kB. The [published simulations](../../.agents/skills/celestial-skill/references/scientific-faithfulness.md#published-simulations) rule gives the conditions.

`telescope new-object --from-esa PAGE_ID=PICTURE_URL... --out SPEC.json` drafts a published picture as one more dataset of a page that already shows a shaped layer bank: a picture ESA publishes for Hubble or Webb (`m57=https://esawebb.org/images/weic2320c/`). It reads the picture's page (title, credit, release date, colors) and the sky tags in its JPEG, finds the star the picture shows where the tags put the page's place, and names the bank the picture will lie on like: the one the page opens on. A picture that does not hold the page's place is refused. A person then writes the dataset's sentences and, under `geometry`, what this picture's light takes on those walls: printed values, each with its source. `telescope new-object SPEC.json` writes the new bank's records, the picture, the page's dataset and a README whose measured lines are filled in; `--bake` bakes the banks and their pages in one chain. A spec can be run again after a change, and the README a person wrote is kept. The entry's fields are in [picture-bank.mts](src/new-object/pictures/picture-bank.mts).

`telescope stars GALAXY` counts the stars SIMBAD lists inside a galaxy's catalogued outline by class (Cepheids, RR Lyrae stars, long-period variables, supergiants), names the papers they come from, and reads VizieR's tables of those papers for what a placed star needs: a period and a position of its own. It also names the single stars of no class that SIMBAD holds there with no parallax and no proper motion, the most cited first: a star one paper studied alone (M51-DS1) is often typed a plain star, and is drafted by hand from that paper. Last, it asks the papers API (OpenAlex, with arXiv when OpenAlex's daily budget is spent; the same index `telescope papers` reads) for the works whose title or abstract names the galaxy and a kind of star, those with the star in the title first: SIMBAD and VizieR take months to hold a new paper and never hold some, so a 2026 paper of Cepheids in M61 and M77 and a 2003 paper of one star in NGC 253's halo were found only there. It downloads nothing, and a star SIMBAD lists inside the outline may still be in front of the galaxy. A table it calls ready drafts a star: `telescope new-object --from-table cepheid:m81=J/ApJ/743/176/table1 --out spec.json` writes the spec of one Cepheid of that table, placed where its sight line crosses the disc the galaxy is drawn as ([table-stars.mts](src/new-object/archives/tables/table-stars.mts) documents the row choices). A table that gives every row its galaxy's centre, as a reanalysis does, places its stars at SIMBAD's position of the SIMBAD name CDS added to each row. A table that lists each star by its detector pixel (a chip, X and Y, as the Hubble Cepheid papers do) needs the exposure those pixels were measured on, which only the paper says: `--from-table "cepheid:m66=J/ApJ/529/723/appen#exposure=mast:HST/product/u35i0101r_c0m.fits,firstPixel=1"` reads that chip's header from MAST by byte range (11,520 bytes of a 10 MB file) and places the star where the header's world coordinates put the pixel. `firstPixel` is the coordinate the paper's software gives the centre of the first pixel: 1 for DAOPHOT and ALLFRAME, 0.5 for HSTphot. The position is Hubble's pointing as archived, not registered against a catalogue: on the one exposure checked, a supernova with a catalogued position fell 1.2″ from it ([image-pixel.mts](src/new-object/archives/images/image-pixel.mts)); a hand-written spec does the same with `position.archive: "mast"`. Coordinates a paper prints in a table that neither VizieR nor SIMBAD holds yet are used as printed with `position.archive: "paper"`: the paper is cited by its DOI and nothing is archived.

When you know the scientific acceptance criteria, save an explicit request:
Expand Down
2 changes: 2 additions & 0 deletions packages/telescope-cli/src/cli.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ test('new-object bakes a spec after generating it, or objects already in the tre
const draft=parseCli(['new-object','--from-debcat','47 Tuc V69','CM Dra','--out','spec.json']);assert.equal(draft.command,'new-object');if(draft.command!=='new-object')return;
assert.deepEqual([draft.from,draft.names],['debcat',['47 Tuc V69','CM Dra']]);
assert.throws(()=>parseCli(['new-object','--from-apokasc','--out','spec.json']),/--from-apokasc NAME\.\.\. --out SPEC\.json/u);
const picture=parseCli(['new-object','--from-esa','m57=https://esawebb.org/images/weic2320c/','--out','spec.json']);assert.equal(picture.command,'new-object');if(picture.command!=='new-object')return;
assert.deepEqual([picture.from,picture.names],['esa',['m57=https://esawebb.org/images/weic2320c/']]);
});
test('local import has one bounded data-only entry point',()=>{
const parsed=parseCli(['import','spec.json','--out','run','--json']);assert.equal(parsed.command,'import');if(parsed.command!=='import')return;
Expand Down
7 changes: 5 additions & 2 deletions packages/telescope-cli/src/help.mts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Human entry points:
telescope leads TARGET | --class CLASS [--json] [--out DIRECTORY]
telescope stars GALAXY [--json] [--out DIRECTORY]
telescope new-object SPEC.json [--check] [--skip-existing] [--json]
telescope new-object --from-archive HOST... | --from-debcat SYSTEM... | --from-apokasc KIC... | --from-cepheids NAME... | --from-k2 EPIC... | --from-tess TIC... | --from-gaia SOURCE_ID... | --from-hipparcos HIP... | --from-iau all|NAME... | --from-chara HD... | --from-npoi HD... | --from-sh0es HOST[/ID]... | --from-m31cepheids all|V1|ID... | --from-m33cepheids all|ID... | --from-table CLASS:GALAXY=TABLE[#ROW]... --out SPEC.json
telescope new-object --from-archive HOST... | --from-debcat SYSTEM... | --from-apokasc KIC... | --from-cepheids NAME... | --from-k2 EPIC... | --from-tess TIC... | --from-gaia SOURCE_ID... | --from-hipparcos HIP... | --from-iau all|NAME... | --from-chara HD... | --from-npoi HD... | --from-sh0es HOST[/ID]... | --from-m31cepheids all|V1|ID... | --from-m33cepheids all|ID... | --from-table CLASS:GALAXY=TABLE[#ROW]... | --from-esa PAGE_ID=PICTURE_URL... --out SPEC.json
telescope ascl SOFTWARE [--json]
telescope ascl --product PRODUCT.json|RUN/pick-N/result.json [--json]
telescope wwt-fits EXPLORE.json --pick N --level N --x X --y Y --out DIRECTORY
Expand Down Expand Up @@ -50,7 +50,10 @@ Only prose is left marked TODO(new-object), unless the spec carries drafted text
NASA Exoplanet Archive's default parameter sets (their transiting planets, with drafted text); --from-debcat drafts both stars of
an eclipsing binary from DEBCat, --from-apokasc a Kepler-field giant from APOKASC-3, --from-cepheids a Cepheid from Groenewegen
(2013), --from-k2 a K2-field giant and --from-tess a TESS giant from Khan et al. (2023), --from-gaia any star from Gaia DR3 FLAME, --from-hipparcos a Hipparcos star from the model-atmosphere fit to its light (McDonald et al. 2017), --from-iau the stars the IAU has named that the universe lacks, each by the first of those routes that measures it, --from-chara a nearby star whose disc CHARA measured (Boyajian et al. 2012), --from-npoi a bright star whose disc the NPOI measured (Baines et al. 2018, 2021), --from-sh0es the Cepheids Hubble found in a SN Ia host galaxy (Hoffmann et al. 2016), --from-m31cepheids Hubble's V1 and the Cepheids Hubble measured in Andromeda (Li et al. 2021), --from-m33cepheids those it measured in Triangulum (Breuval et al. 2023), --from-table a star of another galaxy from any VizieR table that lists it with a period (cepheid:m81=J/ApJ/743/176/table1), placed in its galaxy by the table's position, by SIMBAD's, or by the table's detector pixel on the exposure the request names (#exposure=mast:HST/product/FILE.fits,firstPixel=N), each naming what the spec still needs from the paper; --check runs the bake's first steps on
what was generated; --skip-existing leaves objects already in the universe alone. The spec format is in packages/telescope-cli/src/new-object/spec.mts.
what was generated; --skip-existing leaves objects already in the universe alone. --from-esa drafts a picture ESA publishes for Hubble or Webb as one more
dataset of a page that shows a shaped layer bank (m57=https://esawebb.org/images/weic2320c/): it reads the picture's page and the sky tags in its JPEG, finds the
star the picture shows at the page's place, and leaves the sentences and what each color takes on the bank's walls to write; a spec of pictures writes each
picture's bank and its page's dataset, may be run again, and bakes with SPEC.json --bake. The spec format is in packages/telescope-cli/src/new-object/spec.mts.
Papers lists up to 20 OpenAlex works that name the target, by any spelling of its catalogue names, in their title or
abstract, with the instrument and any one of the --about phrases (singular or plural); --fulltext asks for the works whose
indexed full text names them. It uses arXiv's Atom API when OpenAlex is unavailable or its daily budget is spent, and the
Expand Down
7 changes: 7 additions & 0 deletions packages/telescope-cli/src/new-object/cli.mts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export function parseNewObjectOptions(value:unknown):NewObjectOptions{
return {...string('spec'),...strings('ids'),...string('from'),...strings('names'),...string('out'),check:flag('check'),bake:flag('bake'),refresh:flag('refresh'),skipExisting:flag('skipExisting'),json:flag('json')};
}

const holdsPictures=async(spec:string)=>{const {readFile}=await import('node:fs/promises'),value:unknown=JSON.parse(await readFile(spec,'utf8'));return typeof value==='object'&&value!==null&&'pictures' in value;};

/** The result text and exit code for one parsed `new-object` command. */
export async function newObjectCommand(options:NewObjectOptions,root:string,stderr:(text:string)=>void):Promise<{readonly text:string;readonly code:number}>{
let text:string,code:number;
Expand All @@ -38,6 +40,11 @@ export async function newObjectCommand(options:NewObjectOptions,root:string,stde
}else if(options.from){
const result=await writeDrafts(options.from,options.names??[],options.out!,{root,archive:liveArchive,progress:line=>stderr(`${line}\n`)});
text=options.json?`${JSON.stringify(result)}\n`:`${result.report.join('\n')}\n${result.entries} entries written to ${result.path}\n`;code=result.entries?0:3;
}else if(await holdsPictures(options.spec!)){
// A spec of published pictures: each one a dataset on a page that shows a shaped layer bank (pictures/pictures.mts).
const {runPictures,bakePictures,formatPictures}=await import('./pictures/pictures.mts'),results=await runPictures(options.spec!,{root,archive:liveArchive,progress}),good=results.filter(result=>!result.failed);
const baked=options.bake&&good.length?await bakePictures(good,{root,progress}):true;
text=options.json?`${JSON.stringify(results)}\n`:formatPictures(results,options.spec!,baked&&options.bake&&good.length>0);code=baked&&good.length===results.length?0:1;
}else{
const results=await runNewObject(options.spec!,{root,progress:line=>stderr(`${line}\n`),skipExisting:options.skipExisting,solarEpoch:await loadSolarEpoch(root)});
const good=results.filter(result=>!result.failed).map(result=>result.id),baked=(options.check||options.bake)&&good.length?await prepareObjects(good,{root,progress,...(options.bake?{}:{to:'page'})}):true;
Expand Down
10 changes: 6 additions & 4 deletions packages/telescope-cli/src/new-object/drafts.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import type { Archive } from './archives/archives.mts';

export interface Drafts { readonly stars: readonly unknown[]; readonly report: readonly string[] }
export interface Drafts { readonly stars: readonly unknown[]; readonly pictures?: readonly unknown[]; readonly report: readonly string[] }
interface Context { readonly root: string; readonly progress: (line: string) => void; readonly archive: Archive }
export const DRAFT_ROUTES: Readonly<Record<string, { readonly names: string; readonly draft: (names: readonly string[], context: Context) => Promise<Drafts> }>> = {
// Transiting planet hosts from the NASA Exoplanet Archive's default parameter sets (from-archive.mts).
Expand Down Expand Up @@ -38,14 +38,16 @@ export const DRAFT_ROUTES: Readonly<Record<string, { readonly names: string; rea
m33cepheids: { names: 'all | ID', draft: async (names, { archive, root }) => (await import('./archives/m33-cepheids.mts')).draftsFromM33Cepheids(names, archive, root) },
// A star of another galaxy from any VizieR table that lists it with a position and a period (tables/table-stars.mts); `telescope stars GALAXY` finds the tables.
table: { names: 'CLASS:GALAXY=TABLE[#ROW]', draft: async (names, { archive, root }) => (await import('./archives/tables/table-stars.mts')).draftsFromTable(names, archive, root) },
// A picture ESA publishes for Hubble or Webb, as one more dataset of a page that shows a shaped layer bank (pictures/pictures.mts).
esa: { names: 'HOST=PAGE_URL', draft: async (names, context) => (await import('./pictures/pictures.mts')).draftsFromEsa(names, context) },
};

/** Draft `names` through `route` and write the spec file at `out`. */
export async function writeDrafts(route: string, names: readonly string[], out: string, context: Context) {
const source = DRAFT_ROUTES[route];
if (!source) throw new TypeError(`No draft route ${route}; the routes are ${Object.keys(DRAFT_ROUTES).map(key => `--from-${key}`).join(', ')}.`);
if (!names.length) throw new TypeError(`Usage: new-object --from-${route} ${source.names}... --out spec.json`);
const { stars, report } = await source.draft(names, context), path = resolve(context.root, out);
await mkdir(dirname(path), { recursive: true }); await writeFile(path, `${JSON.stringify({ stars }, null, 2)}\n`);
return { path, entries: stars.length, report };
const { stars, pictures, report } = await source.draft(names, context), path = resolve(context.root, out);
await mkdir(dirname(path), { recursive: true }); await writeFile(path, `${JSON.stringify(pictures ? { pictures } : { stars }, null, 2)}\n`);
return { path, entries: (pictures ?? stars).length, report };
}
2 changes: 1 addition & 1 deletion packages/telescope-cli/src/new-object/new-object.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,7 @@ test('APOKASC-3 and Groenewegen (2013) rows draft single stars through the one r
const { physicalValues } = await import('./generate.mts'), values = physicalValues(measured, { sourceId: '1', ra: 0, dec: 0, g: 13, hasXpSampled: false });
assert.deepEqual([values.gm, values.logg], [0, 1.2]);
assert.match(values.massText, /No mass is measured, so GM is 0/u);
assert.deepEqual(Object.keys(DRAFT_ROUTES), ['archive', 'debcat', 'apokasc', 'cepheids', 'k2', 'tess', 'gaia', 'hipparcos', 'iau', 'chara', 'npoi', 'sh0es', 'm31cepheids', 'm33cepheids', 'table']);
assert.deepEqual(Object.keys(DRAFT_ROUTES), ['archive', 'debcat', 'apokasc', 'cepheids', 'k2', 'tess', 'gaia', 'hipparcos', 'iau', 'chara', 'npoi', 'sh0es', 'm31cepheids', 'm33cepheids', 'table', 'esa']);
await assert.rejects(writeDrafts('gcvs', ['X'], 'output/x.json', { root, progress: () => {}, archive: {} as Archive }), /No draft route gcvs; the routes are --from-archive, --from-debcat, --from-apokasc, --from-cepheids, --from-k2, --from-tess, --from-gaia/u);
});

Expand Down
Loading
Loading