diff --git a/packages/bake/cli/prepare-catalogue-points.mts b/packages/bake/cli/prepare-catalogue-points.mts index c148e061a1a..e623a32e0ac 100644 --- a/packages/bake/cli/prepare-catalogue-points.mts +++ b/packages/bake/cli/prepare-catalogue-points.mts @@ -33,7 +33,7 @@ import { readFitsHdus, binaryTable, tableColumn, numbers } from '@cssearth/bake/ import { parseCieTable, linearToSrgb } from '@cssearth/bake/objects/color'; import { spectrumLinearSrgb } from '@cssearth/bake/objects/stellar'; import { readCie1931ColorMatching } from '@cssearth/bake/objects/sources'; -import { type CatalogueSizeBy, checkCatalogueSizeBy, checkCatalogueToneBy, placeGroupMembers, recipePublished, toneCataloguePalette, writeCatalogueBank } from '@cssearth/bake/volume/node'; +import { type CatalogueSizeBy, checkCatalogueSizeBy, checkCatalogueToneBy, placeGroupMembers, placeMeasuredRows, recipePublished, toneCataloguePalette, writeCatalogueBank } from '@cssearth/bake/volume/node'; const [objectDirectoryArgument, id] = process.argv.slice(2); if (!objectDirectoryArgument || !id || !/^[a-z][a-z0-9-]*$/u.test(id)) throw new TypeError('Usage: prepare-catalogue-points.mts '); @@ -65,7 +65,11 @@ const table = recipe.table as { path: string; bytes: number; format: 'whitespace * distance unit), spread along the line of sight as widely as the group spreads across the sky: `depth: 'isotropic'` * gives each member the sky-plane offset of the member half the group away in table order. A row's own distance is kept * where its group has one member here or no distance. `source` and `basis` say whose groups and why. */ - groupDistance?: { group: Column; distance: Column; depth: 'isotropic'; source: string; basis: string } }; + groupDistance?: { group: Column; distance: Column; depth: 'isotropic'; source: string; basis: string }; + /** Rows a paper measures one by one: a CSV with a header, `name` (this table's name column) and `distance` in `unit`. Such a + * row sits at that distance on its own sight line, whatever its column, its redshift or its group says: a galaxy whose + * Cepheids Hubble measured is where they put it, not at its group's average. `source` and `basis` say whose measurements. */ + measuredDistance?: { path: string; unit: 'distance-modulus'; source: string; basis: string } }; const discPlacement = (recipe.frame as { placement?: unknown } | undefined)?.placement === 'image-layer-disc'; // A galaxy with no disc (M87): each ICRS row at a depth drawn from a published spheroid's density along its sight line. const spheroidPlacement = (recipe.frame as { placement?: unknown } | undefined)?.placement === 'spheroid'; @@ -282,7 +286,7 @@ if r['disc']: xyz = [] elif r['icrs']: xyz = SkyCoord(ra=[k[1] for k in kept] * u.deg, dec=[k[2] for k in kept] * u.deg, distance=[k[3] for k in kept] * scale, frame='icrs').cartesian.xyz.to(out).value.T else: xyz = SkyCoord(l=[k[1] for k in kept] * u.deg, b=[k[2] for k in kept] * u.deg, distance=[k[3] for k in kept] * scale, frame='galactic').icrs.cartesian.xyz.to(out).value.T json.dump({'rows': rows, 'selected': len(kept) + missing, 'missingDistance': missing, 'excluded': excluded, 'astropy': astropy.__version__, - 'points': [[round(float(v), 4) for v in p] for p in xyz], 'groups': grouped if r['group'] else None, 'colors': [k[4] for k in kept], 'magnitudes': [k[6] for k in kept], 'bands': [k[7] for k in kept], 'redshifts': redshifts, + 'points': [[round(float(v), 4) for v in p] for p in xyz], 'names': [k[0] for k in kept], 'groups': grouped if r['group'] else None, 'colors': [k[4] for k in kept], 'magnitudes': [k[6] for k in kept], 'bands': [k[7] for k in kept], 'redshifts': redshifts, 'sigmas': [None if k[5] is None else round(min(k[5], 1e6), 4) for k in kept], 'sky': [[k[1], k[2]] for k in kept] if r['disc'] else None, 'maxDistanceKpc': 0 if r['disc'] else max(k[3] for k in kept) * (.001 if r['unit'] in ('pc', 'distance-modulus', 'redshift-planck18') else 1)}, sys.stdout)`; const { astroqueryToolchainSync } = await import('@cssearth/telescope/node'); @@ -294,7 +298,7 @@ const run = spawnSync(toolchain.python, ['-c', python], { env: { ...process.env, disc: skyPlacement, filters, colorColumn: colorBy?.column ?? colorByClass?.column ?? colorByBv?.column ?? null, toneColumn: toneBy?.magnitudeColumn ?? null, nanomaggies: toneBy?.nanomaggies === true, bands: colorByBands ? [colorByBands.red, colorByBands.green, colorByBands.blue] : null, outUnit: frame.unit ?? 'kpc', ...(table.missingDistance === undefined ? {} : { missing: table.missingDistance }) }) }); if (run.status !== 0) throw new Error(`Catalogue point conversion failed for ${table.path}: ${run.stderr.slice(-2000)}`); -const converted = JSON.parse(run.stdout) as { rows: number; selected: number; missingDistance: number; excluded: number; astropy: string; points: number[][]; colors: (number | null)[]; +const converted = JSON.parse(run.stdout) as { rows: number; selected: number; missingDistance: number; excluded: number; astropy: string; points: number[][]; names: string[]; colors: (number | null)[]; magnitudes: (number | null)[]; bands: (number[] | null)[]; redshifts: number[] | null; sigmas: (number | null)[]; sky: [number, number][] | null; maxDistanceKpc: number; groups: [string, string][] | null }; /** A disc placement may spread the rows through the disc's published thickness: each row keeps its place in the disc (the @@ -316,6 +320,10 @@ if (groupDistance) { } groupPlaced = placeGroupMembers(converted.points, converted.groups!.map(([group]) => group), group => groupDistances.get(group) ?? null); } +const measuredDistance = table.measuredDistance; +if (measuredDistance && (skyPlacement || measuredDistance.unit !== 'distance-modulus' || !measuredDistance.source || !measuredDistance.basis)) throw new TypeError(`${at('table.measuredDistance')} needs a path, unit distance-modulus, a source and a basis, and a table placed by distance.`); +if (measuredDistance) await readFile(resolve(objectDirectory, '../../sources', `${measuredDistance.source}.json`)).catch(() => { throw new TypeError(`${at('table.measuredDistance.source')} ${measuredDistance.source} has no record in src/sources.`); }); +const measuredPlaced = measuredDistance ? placeMeasuredRows(converted.points, converted.names, await readFile(resolve(sourceDirectory, measuredDistance.path)), measuredDistance.path, frame.unit === 'Mpc' ? 1e-6 : 1e-3) : 0; const discThickness = (recipe.frame as { discThickness?: unknown } | undefined)?.discThickness as undefined | { profile: 'sech2' | 'exponential'; scaleHeightPc: number | { atCentrePc: number; perKpcPc: number }; source: string; basis: string }; const flare = typeof discThickness?.scaleHeightPc === 'object' && discThickness.scaleHeightPc !== null ? discThickness.scaleHeightPc : null; @@ -576,6 +584,7 @@ const bank = { schema: 'cssearth-catalogue-points@1', id, source, meaning: recip counts: { rows: converted.rows, selected: converted.selected, points: converted.points.length, missingDistance: converted.missingDistance, ...(table.exclude ? { excluded: converted.excluded } : {}), ...(bulgePlacement ? { bulge: bulgeMembers } : {}) }, ...(bulgePlacement ? { bulge: { source: bulgePlacement.source, basis: bulgePlacement.basis } } : {}), + ...(measuredDistance ? { measuredDistance: { source: measuredDistance.source, basis: measuredDistance.basis, placed: measuredPlaced } } : {}), // Each point's group, which a merge that keeps groups first reads (merge-catalogue-points.mts). ...(groupDistance ? { groupDistance: { source: groupDistance.source, depth: groupDistance.depth, basis: groupDistance.basis, placed: groupPlaced }, groups: converted.groups!.map(([group]) => group) } : {}), diff --git a/packages/bake/src/volume/node/catalogue-groups.test.ts b/packages/bake/src/volume/node/catalogue-groups.test.ts index e3dcbe73606..fa38f6c9be2 100644 --- a/packages/bake/src/volume/node/catalogue-groups.test.ts +++ b/packages/bake/src/volume/node/catalogue-groups.test.ts @@ -1,7 +1,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { isDeepStrictEqual } from 'node:util'; -import { placeGroupMembers, selectByShell } from './catalogue-groups.ts'; +import { placeGroupMembers, placeMeasuredRows, selectByShell } from './catalogue-groups.ts'; test('a group sits at its distance, as deep as it is wide; a lone member or a group without a distance keeps its place', () => { // Four members around +x at 10 units, spread along y (east of +x is +y), plus a lone galaxy and a group with no distance. @@ -37,3 +37,13 @@ test('sky bands split each shell\'s room evenly over the sky, so a densely cover assert.equal(kept.filter(point => point.reference[1]! < 0).length, 1); assert.throws(() => selectByShell(east, 10, () => 4, undefined, 0), /skyBands/); }); + +test('a row a paper measures sits at that distance on its own sight line; a listed row the table lacks is refused', () => { + // Three galaxies in Mpc: the first a group member moved to 19.7, the second placed by its redshift at 32, the third untouched. + const points = [[19.7, 0, 0, 5], [0, 32, 0], [3, 4, 0]], names = ['13179', '12342712+0211163', '50063']; + const placed = placeMeasuredRows(points, names, 'name,distance\n13179,31.307\n12342712+0211163,30.906\n', 'cepheid-hosts.csv', 1e-6); + assert.equal(placed, 2); + assert.deepEqual(points, [[18.2558, 0, 0, 5], [0, 15.1775, 0], [3, 4, 0]]); + assert.throws(() => placeMeasuredRows(points, names, 'name,distance\n99999,31\n', 'cepheid-hosts.csv', 1e-6), /99999 is not among the table's rows/u); + assert.throws(() => placeMeasuredRows(points, names, 'PGC,DM\n13179,31.307\n', 'cepheid-hosts.csv', 1e-6), /the header is name,distance/u); +}); diff --git a/packages/bake/src/volume/node/catalogue-groups.ts b/packages/bake/src/volume/node/catalogue-groups.ts index a0d33ba4a4a..32995e0ace6 100644 --- a/packages/bake/src/volume/node/catalogue-groups.ts +++ b/packages/bake/src/volume/node/catalogue-groups.ts @@ -3,6 +3,7 @@ * `packages/bake/cli/prepare-catalogue-points.mts` (`table.groupDistance`) and `merge-catalogue-points.mts` * (`densityCap.groupsFirst`) call these. */ +import { gunzipSync } from 'node:zlib'; /** Members of each group of two or more at the group's distance, `distance(group)` in the points' unit: each on its own * sight line, moved along it by the sky-plane offset (from the members' mean direction, along the axis east of it) of the @@ -84,3 +85,30 @@ export function selectByShell(order: readonly T[], width: return true; }); } + +/** Rows a paper measures one by one, at that distance on their own sight line whatever their column, redshift or group says: + * a galaxy whose Cepheids Hubble measured is where they put it, not at its group's average (NGC 4536 was drawn 17 Mpc from + * its Cepheids, by its redshift, 2026-10-01). `file` is a CSV, gzipped or not, of `name,distance` with distance moduli; `perPc` turns parsecs into the + * points' unit. A listed row the table does not hold is a mistake in the list. Returns how many rows moved. */ +export function placeMeasuredRows(points: number[][], names: readonly string[], file: Uint8Array | string, path: string, perPc: number, + round = (value: number) => Math.round(value * 1e4) / 1e4): number { + // The tracked list is gzipped, as the tables are (a bare .csv under source/ is a download, and ignored). + const csv = typeof file === 'string' ? file : (path.endsWith('.gz') ? gunzipSync(file) : Buffer.from(file)).toString('utf8'); + const [header, ...lines] = csv.trim().split('\n'); + if (header !== 'name,distance') throw new TypeError(`${path}: the header is name,distance, not "${header}".`); + const wanted = new Map(lines.map(line => { + const [name, modulus] = line.split(','), value = Number(modulus); + if (!name || !modulus || !Number.isFinite(value)) throw new TypeError(`${path}: "${line}" is not a name and a distance modulus.`); + return [name, 10 ** (value / 5 + 1) * perPc] as const; + })); + let placed = 0; + for (const [index, name] of names.entries()) { + const distance = wanted.get(name); + if (distance === undefined) continue; + const point = points[index]!, length = Math.hypot(point[0]!, point[1]!, point[2]!); + for (const axis of [0, 1, 2]) point[axis] = round(point[axis]! / length * distance); + wanted.delete(name); placed++; + } + if (wanted.size) throw new TypeError(`${path}: ${[...wanted.keys()].join(', ')} ${wanted.size === 1 ? 'is' : 'are'} not among the table's rows.`); + return placed; +} diff --git a/packages/telescope-cli/authoring/nearby-universe/cepheid-hosts.mts b/packages/telescope-cli/authoring/nearby-universe/cepheid-hosts.mts new file mode 100644 index 00000000000..915dfc7b1a5 --- /dev/null +++ b/packages/telescope-cli/authoring/nearby-universe/cepheid-hosts.mts @@ -0,0 +1,43 @@ +#!/usr/bin/env node +/** The galaxies of the Nearby Universe field whose distance Hubble measured from their Cepheids, as the rows the field's two + * galaxy tables hold them by: + * + * node packages/telescope-cli/authoring/nearby-universe/cepheid-hosts.mts + * + * Riess et al. (2016), table 5, gives each of 19 type Ia supernova hosts a Cepheid distance modulus (`RIESS_2016_HOSTS`, the same + * values that place their Cepheids' own packages). SIMBAD resolves each host's position, and the row within `MATCH_ARCSEC` of it + * in the Cosmicflows-4 table (named by PGC number) or, when Cosmicflows-4 does not hold the galaxy, in the 2MRS table (named by + * its 2MASS identifier) is listed with that modulus. `packages/bake/cli/prepare-catalogue-points.mts` (`table.measuredDistance`) + * then places the row there, instead of at its group's average or its redshift's distance. + * + * Output: `source/galaxies/cepheid-hosts.csv.gz` and `source/galaxies-2mrs/cepheid-hosts.csv.gz`, `name,distance`. It prints each match. */ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { gunzipSync, gzipSync } from 'node:zlib'; +import { resolveSkyTarget, WORKSPACE } from '@cssearth/telescope/node'; +import { hostName, RIESS_2016_HOSTS } from '../../src/new-object/sh0es.mts'; + +/** A galaxy's catalogue position and SIMBAD's agree to a few arcseconds; the nearest other galaxy in these tables is arcminutes away. */ +const MATCH_ARCSEC = 30; +const source = resolve(WORKSPACE, 'src/objects/nearby-universe/source'); +const TABLES = [{ bank: 'galaxies', path: 'cf4-hyperleda.csv.gz' }, { bank: 'galaxies-2mrs', path: 'twomrs-sample.csv.gz' }] as const; + +const radians = (degrees: number) => degrees * Math.PI / 180; +const separationArcsec = (ra1: number, dec1: number, ra2: number, dec2: number) => Math.acos(Math.min(1, + Math.sin(radians(dec1)) * Math.sin(radians(dec2)) + Math.cos(radians(dec1)) * Math.cos(radians(dec2)) * Math.cos(radians(ra1 - ra2)))) * 180 / Math.PI * 3600; +// Both tables are name, RAJ2000, DEJ2000, … with a header (their points.json columns 1 to 3). +const rows = await Promise.all(TABLES.map(async table => gunzipSync(await readFile(resolve(source, table.bank, table.path))).toString('utf8').trim().split('\n').slice(1) + .map(line => line.split(',')).filter(cells => cells[1] && cells[2]).map(cells => ({ name: cells[0]!, ra: Number(cells[1]), dec: Number(cells[2]) })))); + +const listed: string[][] = TABLES.map(() => []); +for (const [host, [modulus]] of Object.entries(RIESS_2016_HOSTS)) { + const galaxy = hostName(host), resolved = await resolveSkyTarget(WORKSPACE, galaxy); + if (!resolved) throw new Error(`SIMBAD does not know ${galaxy}.`); + const { raDegrees, decDegrees } = resolved.target; + const found = rows.map(table => table.map(row => ({ row, arcsec: separationArcsec(row.ra, row.dec, raDegrees, decDegrees) })).reduce((nearest, next) => next.arcsec < nearest.arcsec ? next : nearest)); + const bank = found.findIndex(match => match.arcsec <= MATCH_ARCSEC); + if (bank < 0) throw new Error(`${galaxy}: no row within ${MATCH_ARCSEC}" in ${TABLES.map(table => table.path).join(' or ')} (nearest ${found.map(match => `${match.arcsec.toFixed(0)}"`).join(', ')}).`); + listed[bank]!.push(`${found[bank]!.row.name},${modulus}`); + console.log(`${galaxy}: ${TABLES[bank]!.bank} ${found[bank]!.row.name}, ${found[bank]!.arcsec.toFixed(1)}" from SIMBAD's position, modulus ${modulus}`); +} +for (const [index, table] of TABLES.entries()) await writeFile(resolve(source, table.bank, 'cepheid-hosts.csv.gz'), gzipSync(`name,distance\n${listed[index]!.join('\n')}\n`, { level: 9 })); diff --git a/src/objects/nearby-universe/README.md b/src/objects/nearby-universe/README.md index c512027f044..8cc8e5b089d 100644 --- a/src/objects/nearby-universe/README.md +++ b/src/objects/nearby-universe/README.md @@ -21,12 +21,12 @@ The [galaxy recipe](source/galaxies/points.json) records both queries and the jo ## Processing -1. `packages/bake/cli/prepare-catalogue-points.mts` places each galaxy at 10^(DM/5 + 1) pc in its J2000 direction, in the Sun-centred frame, in Mpc. A galaxy's own distance modulus carries a median uncertainty of 0.5 mag (about 26%), which drew Coma as a spike: its members' own distances scatter by 21 Mpc (rms) along the line of sight, against 2 Mpc across the sky. So the 11,113 galaxies in groups of two or more sit at their group's distance instead, spread in depth as widely as the group spreads across the sky (`groupDistance`). That the group is as deep as it is wide is an assumption, not a measurement of any member's depth. It checks against one: Virgo comes out 0.64 Mpc deep (rms), and [Mei et al. (2007)](https://arxiv.org/abs/astro-ph/0702510) measured 0.6 ± 0.1 Mpc from the surface brightness fluctuation distances of 79 members. They also found Virgo slightly elongated, its long axis 20 to 40° from the line of sight, which the assumption leaves out. +1. `packages/bake/cli/prepare-catalogue-points.mts` places each galaxy at 10^(DM/5 + 1) pc in its J2000 direction, in the Sun-centred frame, in Mpc. A galaxy's own distance modulus carries a median uncertainty of 0.5 mag (about 26%), which drew Coma as a spike: its members' own distances scatter by 21 Mpc (rms) along the line of sight, against 2 Mpc across the sky. So the 11,113 galaxies in groups of two or more sit at their group's distance instead, spread in depth as widely as the group spreads across the sky (`groupDistance`). That the group is as deep as it is wide is an assumption, not a measurement of any member's depth. It checks against one: Virgo comes out 0.64 Mpc deep (rms), and [Mei et al. (2007)](https://arxiv.org/abs/astro-ph/0702510) measured 0.6 ± 0.1 Mpc from the surface brightness fluctuation distances of 79 members. They also found Virgo slightly elongated, its long axis 20 to 40° from the line of sight, which the assumption leaves out. Nineteen galaxies are placed one by one instead: the type Ia supernova hosts whose Cepheids the Hubble Space Telescope measured ([Riess et al. 2016](https://arxiv.org/abs/1604.01424), table 5), each at its own Cepheid distance ([18 in Cosmicflows-4](source/galaxies/cepheid-hosts.csv.gz), [NGC 4536 in 2MRS](source/galaxies-2mrs/cepheid-hosts.csv.gz), listed by [`cepheid-hosts.mts`](../../../packages/telescope-cli/authoring/nearby-universe/cepheid-hosts.mts)). A group's average put NGC 3021 5.6 Mpc from that distance, and its redshift put NGC 4536 17 Mpc from it; their packaged Cepheids sit at the same distance, so each galaxy's dot is among them. 2. It colours each galaxy by its type class: the template spectrum of that class through the CIE 1931 observer into sRGB, the route the app uses for star colours. The colours are E #ffdec0, S0 #ffdfc1, Sa #ffdcc7, Sb #ffe1cb and Sc #d9d7ff. The 1,400 galaxies HyperLEDA gives no type are white. Each colour is then darkened by the galaxy's absolute B magnitude: full at M_B −21.5, 30% at −17.5. The 967 galaxies without btc take the darkest tone. Each dot takes one of three sizes by the same magnitude, ranked among the galaxies at about its distance (16 shells of equal count): the brightest quarter 0.9 px, the middle half 0.65 px, the faintest quarter 0.5 px. Ranked over the whole catalogue, the large dots would be the far ones, since only luminous galaxies are seen far away; within shells every depth has the same mix. The radii keep the field's total light within 2% of its one 0.75 px size before. 3. [`local-group-sample.mts`](../../../packages/bake/authoring/nearby-universe/local-group-sample.mts) takes the Local Group catalogue's galaxies nearer than the field's nearest galaxy, leaving out those drawn as their own objects (M31, M33, the Magellanic Clouds). The same route places them and tones them by absolute V magnitude, LVDB having no B; nearly all are dwarfs and take the faintest tone. LVDB gives no type, so they are white. 4. [`twomrs-sample.mts`](../../../packages/bake/authoring/nearby-universe/twomrs-sample.mts) keeps the 2MRS galaxies more than 10 arcsec from every Cosmicflows-4 galaxy, with their redshifts moved to the cosmic microwave background's frame (Planck's dipole: 369.82 km/s toward l 264.021°, b 48.253°), and the same route places each at its Planck 2018 comoving distance ([recipe](source/galaxies-2mrs/points.json)). They reach down to 5° from the Milky Way's plane, where Cosmicflows-4's surveys stop at about 20°, and fill the southern sky Cosmicflows-4's northern SDSS sample leaves thin. A redshift distance carries the galaxy's own motion, so they sit less exactly than Cosmicflows-4's; 54% have no type in 2MRS and are white; they are toned by absolute Ks, ranked within the sample (full at −25.5, 30% at −22.5), and sized by it as the Cosmicflows-4 galaxies are by B. 5. `packages/bake/cli/merge-catalogue-points.mts` thins the Cosmicflows-4 and 2MRS galaxies into four nested levels, counted in shells around the Sun; in the field and the 60 Mpc level each shell's room is split evenly over equal-area cells of the sky (32 and 8), so a direction one survey covers densely does not take another's room. The field allows 0.18 galaxies per 1,000 Mpc³ out to 200 Mpc, half what CF4 still holds at its edge. Around the Milky Way, levels out to 60, 20 and 10 Mpc bring that up to 5, 25 and 90. In each shell, group members take the room first, the richest group first, and as many again drawn at random from the rest fill the space between them (`groupsFirst`). Every member of a group of five or more is in the field, whatever the room, so a cluster far from the Milky Way is whole from anywhere. A fifth level holds the 200 Local Volume Database galaxies and every Cosmicflows-4 galaxy the other levels left out: from inside the Local Group the screen shows a narrow cone of the sky, and the thinned levels alone put a third as many dots there as the view from outside. -6. `packages/bake/cli/stack-catalogue-points.mts` joins the levels into [one bank](source/dots/stack.json) of 39,904 dots (13,960, 2,316, 348, 54 and 23,226; the innermost keeps one in 3 of the 2MRS galaxies the others leave, since a bank draws at most 40,000). The app draws a growing share of it as you zoom in, so a level's edge is never on screen and a dot you have seen stays. The 60 Mpc level fills as the view narrows from 100 to 20 Mpc across, over two wheel notches; the innermost level as it narrows from 3 to 1 Mpc. Seen from outside, the field draws at most one dot per 170 square pixels, the density of the galaxies and quasars around it (its [recipe](source/dots/stack.json) gives the measurement), so it does not show as a ball twice as dense as the sky around it. Every galaxy beyond the Local Group draws at 75% opacity, a backdrop behind the object in view. +6. `packages/bake/cli/stack-catalogue-points.mts` joins the levels into [one bank](source/dots/stack.json) of 39,903 dots (13,960, 2,302, 350, 54 and 23,237; the innermost keeps one in 3 of the 2MRS galaxies the others leave, since a bank draws at most 40,000). The app draws a growing share of it as you zoom in, so a level's edge is never on screen and a dot you have seen stays. The 60 Mpc level fills as the view narrows from 100 to 20 Mpc across, over two wheel notches; the innermost level as it narrows from 3 to 1 Mpc. Seen from outside, the field draws at most one dot per 170 square pixels, the density of the galaxies and quasars around it (its [recipe](source/dots/stack.json) gives the measurement), so it does not show as a ball twice as dense as the sky around it. Every galaxy beyond the Local Group draws at 75% opacity, a backdrop behind the object in view. ## Beyond the Cosmicflows-4 field diff --git a/src/objects/nearby-universe/inventory.json b/src/objects/nearby-universe/inventory.json index 24b23696ad9..a061550538f 100644 --- a/src/objects/nearby-universe/inventory.json +++ b/src/objects/nearby-universe/inventory.json @@ -10,8 +10,8 @@ { "location": "prepared", "filename": "dots.bin", - "bytes": 443181, - "sha256": "7cb4164362ebae703160459b0846260a093a5a7e8286c27ba5d0906a7959c9e9" + "bytes": 443168, + "sha256": "05e3e952b4820a0411df662076a162540525232b001ad6b044d36cef0a42d0ac" }, { "location": "prepared", diff --git a/src/objects/nearby-universe/source/galaxies-2mrs/cepheid-hosts.csv.gz b/src/objects/nearby-universe/source/galaxies-2mrs/cepheid-hosts.csv.gz new file mode 100644 index 00000000000..56c818541b1 Binary files /dev/null and b/src/objects/nearby-universe/source/galaxies-2mrs/cepheid-hosts.csv.gz differ diff --git a/src/objects/nearby-universe/source/galaxies-2mrs/points.json b/src/objects/nearby-universe/source/galaxies-2mrs/points.json index 25f0f98794b..3d9c9ee2de4 100644 --- a/src/objects/nearby-universe/source/galaxies-2mrs/points.json +++ b/src/objects/nearby-universe/source/galaxies-2mrs/points.json @@ -14,7 +14,13 @@ "decDeg": 3, "distance": 4 }, - "distanceUnit": "redshift-planck18" + "distanceUnit": "redshift-planck18", + "measuredDistance": { + "path": "cepheid-hosts.csv.gz", + "unit": "distance-modulus", + "source": "arxiv-1604-01424", + "basis": "Riess et al. (2016), table 5, column mu_Ceph: each host's distance modulus from its own Cepheids, measured by the Hubble Space Telescope to about 0.05 mag, where a group's average or a redshift places it megaparsecs away. The packaged Cepheids of these galaxies sit at the same distance." + } }, "sample": "The tracked file keeps the 27,897 of 2MRS's 44,599 galaxies that Cosmicflows-4 does not hold (none within 10 arcsec of a Cosmicflows-4 position), with a redshift from Cosmicflows-4's nearest galaxy to z 0.05 in the cosmic microwave background's frame (packages/bake/authoring/nearby-universe/twomrs-sample.mts: Planck's dipole, 369.82 km/s toward l 264.021, b 48.253), with ID, RA, DEC, Z, the ZCAT numerical type T and Kcmag.", "filters": [], diff --git a/src/objects/nearby-universe/source/galaxies/cepheid-hosts.csv.gz b/src/objects/nearby-universe/source/galaxies/cepheid-hosts.csv.gz new file mode 100644 index 00000000000..ab575cf747d Binary files /dev/null and b/src/objects/nearby-universe/source/galaxies/cepheid-hosts.csv.gz differ diff --git a/src/objects/nearby-universe/source/galaxies/points.json b/src/objects/nearby-universe/source/galaxies/points.json index a4668901f5e..2d5bd315432 100644 --- a/src/objects/nearby-universe/source/galaxies/points.json +++ b/src/objects/nearby-universe/source/galaxies/points.json @@ -39,6 +39,12 @@ ], "generator": "packages/bake/authoring/nearby-universe/cf4-groups.mts", "note": "Columns 7 and 8: the group's dominant galaxy (table 2, 1PGC) and the group's distance modulus on the calibrated scale (table 3, DMzp), joined by PGC; column 8 is empty for the 7 galaxies whose group table 3 does not list." + }, + "measuredDistance": { + "path": "cepheid-hosts.csv.gz", + "unit": "distance-modulus", + "source": "arxiv-1604-01424", + "basis": "Riess et al. (2016), table 5, column mu_Ceph: each host's distance modulus from its own Cepheids, measured by the Hubble Space Telescope to about 0.05 mag, where a group's average or a redshift places it megaparsecs away. The packaged Cepheids of these galaxies sit at the same distance." } }, "sample": "The tracked file keeps the 27,277 of CF4's 55,877 galaxies whose distance modulus puts them between 3 and 200 Mpc (27.386 to 36.505 mag), in PGC order. Nearer, the Local Group catalogue (src/objects/local-group) draws each galaxy; farther, only the SDSS fundamental-plane sample reaches, over part of the northern sky.", diff --git a/src/objects/nearby-universe/source/manifest.json b/src/objects/nearby-universe/source/manifest.json index f68bfc0fff2..c519cb93f33 100644 --- a/src/objects/nearby-universe/source/manifest.json +++ b/src/objects/nearby-universe/source/manifest.json @@ -297,6 +297,54 @@ "galaxies-table" ], "license": "Original catalogue terms apply; cite Huchra et al. (2012) and CDS/VizieR." + }, + { + "id": "galaxies-cepheid-hosts", + "path": "src/objects/nearby-universe/source/galaxies/cepheid-hosts.csv.gz", + "origin": "https://arxiv.org/abs/1604.01424", + "sourceUrl": "https://arxiv.org/abs/1604.01424", + "title": "Galaxies of this table with a Cepheid distance from the Hubble Space Telescope", + "credit": "Riess et al. (2016), ApJ 826, 56, table 5; positions resolved by SIMBAD (CDS, Strasbourg)", + "acquisition": "The generator lists each of Riess et al.'s 19 hosts by the row SIMBAD's position matches within 30 arcsec, with the paper's Cepheid distance modulus. Written 2026-10-01.", + "generator": "packages/telescope-cli/authoring/nearby-universe/cepheid-hosts.mts", + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "arxiv-1604-01424", + "role": "material", + "evidence": "src/objects/nearby-universe/source/galaxies/points.json#/table/measuredDistance" + } + ] + }, + "dependencies": [ + "galaxies-table" + ], + "license": "Published measurements; cite Riess et al. (2016)." + }, + { + "id": "galaxies-2mrs-cepheid-hosts", + "path": "src/objects/nearby-universe/source/galaxies-2mrs/cepheid-hosts.csv.gz", + "origin": "https://arxiv.org/abs/1604.01424", + "sourceUrl": "https://arxiv.org/abs/1604.01424", + "title": "Galaxies of this table with a Cepheid distance from the Hubble Space Telescope", + "credit": "Riess et al. (2016), ApJ 826, 56, table 5; positions resolved by SIMBAD (CDS, Strasbourg)", + "acquisition": "The generator lists each of Riess et al.'s 19 hosts by the row SIMBAD's position matches within 30 arcsec, with the paper's Cepheid distance modulus. Written 2026-10-01.", + "generator": "packages/telescope-cli/authoring/nearby-universe/cepheid-hosts.mts", + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "arxiv-1604-01424", + "role": "material", + "evidence": "src/objects/nearby-universe/source/galaxies-2mrs/points.json#/table/measuredDistance" + } + ] + }, + "dependencies": [ + "galaxies-2mrs-sample" + ], + "license": "Published measurements; cite Riess et al. (2016)." } ], "documents": [