Skip to content

feat(galaxies): physical surface-brightness rendering + radial colour gradient (look spike)#502

Merged
rulkens merged 14 commits into
mainfrom
worktree-galaxy-bloom-point-tuning
Jul 24, 2026
Merged

feat(galaxies): physical surface-brightness rendering + radial colour gradient (look spike)#502
rulkens merged 14 commits into
mainfrom
worktree-galaxy-bloom-point-tuning

Conversation

@rulkens

@rulkens rulkens commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Galaxy look spike: physical surface brightness + radial colour gradient

Live-tuning spike to make the current point / procedural-disk galaxies pass visually before committing to replace the procedural disks with the full galaxy renderer. The motivating problem: galaxy points were flat LDR and never crossed the HDR bloom threshold (2.0), so they read as dull dots that never bloom.

Run as a live-tuning spike rather than spec + plan, iterating on the dev server. All three stages have had a visual pass.

Stage 1: physical surface brightness in the points pass

Galaxy point brightness is driven by physical surface brightness instead of a flat constant.

  • Bake: sbAmp = 10^(-0.4·(M_abs − perCatalogZeroPoint)) / (diameterKpc/30)² written into interleaved slot 13 (SLOTS_PER_POINT 13 → 14). No .bin format change: magG + diameterKpc already live in the cloud, so this is bake-time only.
  • Vertex intensity: min(sbAmp, u.galaxySbMax) · u.galaxySbScale · source.sbBoost · fluxFalloff, where fluxFalloff = select(1, pow(resolvedFrac, u.galaxyFalloffStrength), depthFadeEnabled) and resolvedFrac = apparentPxRadius / pointSizePx. Constant surface brightness while resolved, inverse-square dimming once floored, so galaxies grow into view and bloom on approach. The old depthFade = 1/(1+(d/half)²) is removed and the toggle now gates the new falloff.

Three live knobs in Settings → Galaxies → Advanced (points Uniforms grown 176 → 192 bytes): sbScale (5.0), sbMax bloom ceiling (30), falloffStrength (0.7). Per-source sbBoost repurposes the dead intensityFloor field, 1.0 everywhere except Milliquas at 3.0 and Famous at 0.45.

Stage 2: the same physics in the procedural-disk pass

Both passes now bloom, and the point → disk crossfade holds constant brightness through the handoff.

The formula lives in shared pure helpers (src/utils/galaxy/galaxySbAmp.ts, galaxyMedianAbsMag.ts) called by both the bake and the disk planner, so the two cannot drift. The per-catalog zero-point rides on GalaxyCatalog.medianAbsMag as an optional field recomputed at decode, so there is no .bin format bump. The disk instance's effective amplitude is computed each frame from the live sliders and packed into the existing free extras.w slot, so no disk uniform grows and the sliders stay live. Core profile tightened: BULGE_SIGMA 0.4 → 0.28, weights 0.6/0.4 → 0.7/0.3.

Stage 3: warm-core / cool-rim radial colour gradient

Expressed in colour-index space re-using the shared ramp, not as an ad-hoc RGB tint. That choice is load-bearing: a red elliptical stays red and merely gains structure, and ramp saturating makes the gradient self-limiting at the extremes.

rampRadial(t, r) = ramp(t + DISK_TINT_SPREAD · (0.5 − r)), called by both passes from the same helper, so the divergence that got an earlier warm-bulge/cool-disk tint reverted is structurally impossible.

Two things came out of the first cut:

  • Varying budget. The first cut forwarded a rim/core delta on a new @location, which measured +1.49 ms on the point-sprites slot (about 21%) against a 0.07 ms noise floor. Collapsing the two ramp lookups into one rampRadial and repacking VSOut into instanceTint: vec4 (rgb = tint × intensity, w = colour index) and ellipse: vec3 (paCos, paSin, safeAB) returns it to the four locations it had before the gradient existed. Re-measured on an idle machine at +0.10 ms, at the noise floor, which confirms the varying rather than the arithmetic was the cost.
  • A pre-existing ramp discontinuity. ramp had both halves sharing s = saturate(t · 0.5), so the blue → white half only reached halfway to white at t = 1 while the white → red half started halfway to red. The function jumped by (+0.30, −0.05, −0.35) at t = 1 and never passed through its own white anchor. This stayed invisible while each galaxy sampled the ramp at exactly one point; the radial gradient sweeps one galaxy's argument across a range and turned the step into a hard ring inside a single galaxy. Independent confirmation it was a bug: UNKNOWN_COLOUR_RAMP_POSITION = 1.05 is documented as a deliberately pale, neutral position, which it only is under a continuous ramp.

This recolours the whole galaxy field. Endpoints are unchanged (t = 0 blue, t = 2 red), but the mid-range is markedly whiter and the large unknown-colour population at 1.05 goes from orange to pale cream.

DISK_TINT_SPREAD = 0.2 is grounded in a measured centre-to-edge gradient of about 0.15 mag from three sources (SDSS-DR4 spirals, SDSS early-type galaxies, and a bulge-vs-disc component offset), converted at the median per-source ramp factor of 1.33.

Catalog fixes found along the way

These are independent of the rendering work but were surfaced by it.

  • Sentinel magnitudes. Catalogs mark absent photometry with numeric sentinels that pass Number.isFinite: SDSS -9999, 2MRS 99.999, Milliquas a literal 0. New shared guard tools/utils/math/isPlausibleMagnitude.ts (-30 < mag < 40), an inclusive-range test rather than an enumeration of known sentinels so a new source with a new sentinel needs no edit. Applied in the SDSS, GLADE, 2MRS, Hipparcos and Milliquas parsers.

    This was the cause of SDSS medium rendering as a handful of dots while picking still resolved every galaxy. 55 rows with M = -10040 dragged the per-catalog zero-point by −3.49 mag at medium against −1.11 at large, the same 55 rows over a third as many galaxies. They survived every filter because subsampleByAbsMag ranks ascending, so they sorted as the 55 brightest galaxies and were guaranteed into every tier.

  • Median rather than mean zero-point. galaxyMeanAbsMaggalaxyMedianAbsMag (and the field it populates) as the complementary fix. A mean is movable by one wild value; a median is robust to any future contamination without knowing which sentinels exist. On real data they agree (−20.82 against a clean mean of −20.880).

  • Milliquas famous-seed dedup. buildAllBins ran dropFamousMatches on the cross-matched sources and then injected Milliquas afterwards, so Milliquas rows never saw the famous dedup and Cen A rendered twice. The documented "Milliquas bypasses crossMatch on purpose" rationale covers the crossMatch bypass (AGN core vs host is different science) but not the famous dedup, which was an unexamined side effect. Cost measured at 20 of 943,460 rows, with the drop count logged to stderr rather than culled silently.

Deploying this

.bin files reach production through R2, not git, so npm run sync-r2-secure from the main checkout is required after merge or production keeps serving the poisoned SDSS rows and the duplicated Cen A. The bins have already been rebuilt locally.

Gate

npm run typecheck clean; npm test green at 866 files / 5018 tests.

Follow-ups (filed in the backlog, not in this PR)

  • Physically-honest galaxy surface brightness. galaxySbAmp divides a catalog-relative luminosity by an absolute 30 kpc size reference, Famous is fudged with sbBoost 0.45, and GLADE's diameter is Tully-derived from its own B magnitude so it carries no independent surface-brightness information.
  • Per-source colour-gradient spread. DISK_TINT_SPREAD is one shared constant in ramp-position units, but ramp units per magnitude are per source, so the effective physical gradient runs about 0.04 mag for 2MRS to 0.30 mag for GLADE.
  • Famous-seed redshift-distance fallback. M90 carries 1.47 Mpc from the v3k / 70 fallback, invalid for a Virgo member whose peculiar velocity dominates its Hubble flow.

🤖 Generated with Claude Code

Galaxies rendered as flat LDR disks that never crossed the bloom
threshold. Replace the apparent-magnitude clamp with a physically
grounded surface-brightness amplitude so intrinsically bright/compact
galaxies emit into HDR and bloom, while diffuse ones stay dim.

- Bake per-galaxy sbAmp = 10^(-0.4*(M_abs - meanAbsMag_perCatalog))
  / (diameterKpc/30)^2 into a new interleaved slot (13->14 slots).
- Vertex intensity = sbAmp * sbScale * sbBoost * fluxFalloff, where
  fluxFalloff = pow(resolvedFrac, falloffStrength) gated by the depth
  toggle recovers inverse-square on approach while resolved galaxies
  hold constant surface brightness.
- Wire three live calibration knobs (sbScale, sbMax, falloffStrength)
  through the store to the points Uniforms (176->192 bytes) with
  sliders in Settings > Galaxies > Advanced.
- Repurpose the now-dead per-source intensityFloor field into an
  sbBoost multiplier; lift Milliquas 3x.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
skymap a06954c Commit Preview URL

Branch Preview URL
Jul 24 2026, 06:41 PM

rulkens and others added 12 commits July 24, 2026 16:11
Stage 2 of the galaxy-look spike. The close-approach procedural disk
handed off from a physically-lit point sprite to a flat-brightness disk
(peak ~1.0, never bloomed). Now both passes derive brightness from the
same physical surface-brightness model, so bright galaxies bloom in the
disk view too and the point->disk crossfade holds constant brightness.

- Extract the SB formula into shared pure helpers galaxySbAmp +
  galaxyMeanAbsMag; the bake and the disk planner both use them so the
  amplitude can't drift between passes.
- Store the per-catalog SB zero-point on GalaxyCatalog.meanAbsMag
  (optional; populated at decode/synthetic/empty/transfer, recomputed on
  decode so no .bin format bump). The bake reads it; the disk planner
  reads catalog.meanAbsMag directly, so no store map or frame-input
  threading of the zero-point.
- Disk planner computes each instance's effective amplitude
  min(sbAmp, sbMax) * sbScale * sbBoost * brightness from the live
  settings each frame and packs it into the instance's free extras.w
  slot; the fragment multiplies its profile by it. No disk-uniform
  growth, so the Advanced sliders stay live.
- Tighten the core profile: bulge sigma 0.4->0.28, weights 0.6/0.4 ->
  0.7/0.3, so the SB amplitude reads as a concentrated bloomy nucleus
  over a softer disk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The disk read as a flat blob: bulge sigma 0.28 with 0.7/0.3 weights left
the mid-disk floor only ~4x below the centre, so core and halo bloomed
together.

Tighten the bulge to sigma 0.18 and shift the weights to 0.85/0.15. The
weights still sum to 1.0, so the CENTRE peak stays at 1.0 x sbAmp — the
value that already matches the companion point sprite, so this cannot
reintroduce a brightness pop at the crossfade. Contrast comes entirely
from draining the halo and tightening the bulge, dropping the mid-disk
floor to ~14x below the peak: the nucleus clears the bloom threshold
while the halo stays under it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Famous galaxies rendered blown out on close approach. The cause is a
systematic in the surface-brightness model, not missing photometry — 77
of the 80 seed rows carry a real magB.

galaxySbAmp divides a catalog-RELATIVE luminosity (normalised against
the catalog's own meanAbsMag) by an ABSOLUTE size reference (fixed
30 kpc). Famous has a median diameter of 25.4 kpc, so the size term
alone inflates every row by 1/0.847^2 = 1.39x; with the log-space-mean-
then-exponentiate skew the measured median raw lands at 2.14 against a
nominal 1.0, tailing to 11.7 (NGC 4449). Every row then clears the 2.0
bloom threshold, so the whole catalog blooms at once. The sbMax ceiling
never engages — 0 of 80 rows reach it.

0.45 ~= 1/2.14 re-centres the catalog's median on the amplitude a
typical survey galaxy gets while preserving the internal spread. This
is a per-source trim; normalising the size term per-catalog would fix
the relative-vs-absolute mismatch generally, at the cost of re-tuning
every other catalog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…esign

The look spike grounded galaxy brightness in a physical surface-brightness
amplitude, but the normalisation is not actually physical: galaxySbAmp
divides a catalog-RELATIVE luminosity (against that catalog's own
meanAbsMag) by an ABSOLUTE 30 kpc size reference.

Records the three defects with derivations so the reasoning isn't lost:
meanAbsMag conflating a band zero-point with real luminosity differences;
Famous's sbBoost 0.45 being fitted to a symptom rather than photometry;
and GLADE's diameter being Tully(1988)-derived from its own B magnitude,
making SB ∝ L^-0.245 — a re-parameterisation of the magnitude carrying no
independent surface-brightness information.

Also notes the honest cheap option: keep the tuning, drop the "physically
grounded" claim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Circinus and the Milliquas copy of Centaurus A rendered as blown-out
white blobs. The cause is upstream data, not the brightness model.

Milliquas marks an absent magnitude with a literal `0`, not a blank. The
real Circinus row reads `Rmag="10.93" Bmag=" 0 "` — R measured, B absent
— but the parser only guarded blanks, so parseFloat('0') came through as
a real magnitude. 2169 rows carried magG=0.

Zero is catastrophic rather than merely wrong: at Circinus' 4.28 Mpc an
m=0 back-solves to M=-28.2, which galaxySbAmp reads as 243x a typical
galaxy's luminosity. Clamped at sbMax it still renders at sbAmp 450
against a catalog median of 15 — 30x too bright.

Treating 0 as missing is safe for this catalog specifically: the
brightest known AGN (3C 273) sits at ~12.9, so no row has a legitimate
magnitude near zero. NaN is the honest sentinel and downstream consumers
already handle it.

Regression test appends the real Circinus row to the fixture verbatim.
Takes effect on the next Milliquas bin rebuild.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Milliquas records were added straight into the per-source bucket after
dropFamousMatches had already run on the crossMatch output — which
Milliquas is deliberately absent from — so they silently skipped the
famous dedup entirely. A famous galaxy with an active nucleus therefore
rendered twice: once as its curated entry, once as a Milliquas point on
top. Centaurus A was the visible case.

The existing "Milliquas bypasses crossMatch on purpose" rationale is
sound but covers only crossMatch (an AGN core and its host are different
objects, and crossMatch dedups on RA/Dec/redshift). Skipping the famous
dedup was an unexamined side effect of where the injection happened.

Measured cost: 20 of ~943k Milliquas rows sit within 30" of a famous-seed
position, and only ONE is genuinely a different object — a ~1 Gpc
background quasar behind the Antennae. The other 19 are the host's own
nucleus, scattered in distance only by Milliquas' coarse 3-decimal
redshift (z=0.001 quantises to 4.28 Mpc, 0.002 to 8.56). Accepted trade;
a redshift-agreement test would save the quasar if it ever matters.

Drop count is logged rather than silent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…asses

Galaxies were flat-tinted: one ramp lookup per galaxy, the same hue from
core to rim. Real galaxies are not — the bulge is old red stars, the arms
young blue OB stars, a g-r difference of roughly 0.4.

The gradient is expressed in COLOUR-INDEX space and re-uses the existing
shared 'ramp', rather than as an ad-hoc RGB tint. That distinction is what
makes it safe on a catalog: a red elliptical stays red and merely gains
internal structure, instead of being force-tinted blue at its rim. The
split is symmetric, so a galaxy's integrated hue stays near its catalogued
colour index, and 'ramp' saturating at the extremes makes the gradient
self-limiting for already-extreme colours.

An earlier attempt at warm-bulge/cool-disk tinting was reverted because it
lived only in the procedural-disk pass and so diverged visibly from the
points pass across the 8-14 px crossfade. Both passes now read the same
'rampCore'/'rampRim' pair from 'lib/colorIndex.wesl', so that divergence
is structurally impossible rather than merely avoided.

The two passes spell the reconstruction differently for cost reasons, not
semantic ones. The procedural-disk pass has few instances and evaluates
both ramps per fragment. The points pass covers most of the screen at the
large tiers and is fragment/blend-bound, so it hoists both ramp lookups to
the vertex stage and forwards the rim-minus-core delta as a flat varying;
the fragment reconstructs with one fused multiply-add. Both are exact
lerps between the same two endpoints, and the endpoints are per-instance
constants, so the passes agree value-for-value.

Known approximation: the point sprite's billboard is padded relative to
the galaxy's true extent while the disk impostor's r = 1 is the disk edge,
so the gradient is radially compressed on the point relative to the disk.
The passes cross-fade rather than swap, so this reads as the gradient
gaining definition on approach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng budget

The radial gradient forwarded a precomputed rim-minus-core delta from the
points vertex stage, which needed a fifth @location on VSOut. That cost
+1.5 ms on the point-sprites slot (~21% at the full-survey scenario, 60
frames, alternating A/B) against a per-fragment saving of one sqrt and one
fused multiply-add. Varying count is a real budget here: each @location is
a per-fragment parameter fetch from tile memory paid for every covered
pixel, and this pass is fragment/blend-bound with billboards covering most
of the screen at the large tiers.

Two changes remove the slot.

'ramp' is piecewise linear in its saturated parameter, so mixing two
lookups that land on the same segment equals one lookup at the mixed
argument: mix(ramp(t+d), ramp(t-d), r) == ramp(t + d - 2*d*r). The
rampCore/rampRim pair collapses to a single 'rampRadial(t, r)'. No pass
needs to forward an endpoint delta any more, and the procedural-disk pass
drops from two ramp evaluations per fragment to one.

VSOut then repacks rather than extends. A WGSL @location is a 4-component
slot, so a lone vec2 wastes two components. Grouping by what actually
consumes the values together yields 'ellipse' (paCos, paSin, safeAB —
exactly the three inputs to the elliptical-mask transform) and
'instanceTint' (the per-instance modulator plus the ramp input it scales).
VSOut is back to 4 locations, its count before the gradient existed.

The ramp evaluation necessarily moves to the fragment stage: its argument
depends on the fragment's radius, so it could not live in the vertex stage
under any spelling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t in measurements

Two fixes to lib/colorIndex.wesl.

## The ramp had a hard seam at t = 1

Both halves shared 's = saturate(t * 0.5)', so the blue-to-white half only
reached HALFWAY to white at t = 1 while the white-to-red half STARTED
halfway to red. The function jumped by (+0.30, -0.05, -0.35) there and
never passed through its own white anchor, despite the header calling
white a shared join point and the transition continuous. Each half now
spans its own unit interval, 'saturate(t)' and 'saturate(t - 1)', so both
meet at white exactly.

This stayed invisible for as long as each galaxy sampled the ramp at a
single point: the step only separated one galaxy from another. The radial
colour gradient sweeps one galaxy's argument across a range as the
fragment radius varies, which turned the step into a hard-edged ring
inside a single galaxy. A ramp sampled once per galaxy can hide a
discontinuity; one swept per fragment cannot.

Independent confirmation this was a bug and not a deliberate palette:
UNKNOWN_COLOUR_RAMP_POSITION is documented as "a deliberately pale,
neutral position", and 1.05 only renders pale under a continuous ramp. It
rendered bright orange before.

Expect a global recolouring. The endpoints are unchanged, but the
mid-range becomes markedly whiter, and the large unknown-colour
population at 1.05 goes from orange to pale cream.

## The spread was a guess; now it is derived

DISK_TINT_SPREAD drops from 0.4 to 0.2. The old value rested on an
unsourced claim about bulge and arm colours. Three independent
measurements of the centre-to-edge gradient, all bluer-outward:

  - ~20,000 face-on SDSS-DR4 spirals: -0.006 mag/kpc in g-r, so about
    0.09 mag over a ~15 kpc disk radius.
  - SDSS early-types: d(g-r)/d log r of -0.11 to -0.16 mag/dex, about
    0.11 to 0.16 mag over the ~1 dex the visible disk spans.
  - Bulge-versus-disc component colours: about 0.23 mag in g-r.

Taking 0.15 mag. The constant is in RAMP-POSITION units, not magnitudes:
colourIndex.ts remaps each source's raw colour onto 0..2, so a magnitude
is worth 2/(rangeMax-rangeMin) ramp units, which is per-source — 2MRS
5.00, DESI 2.86, SDSS and Famous 1.33, Milliquas 1.00, GLADE 0.67. At the
median factor, 0.15 mag gives 0.2.

Known simplification, recorded in the file: one shared ramp-space constant
against per-source conversion factors means the effective physical
gradient differs by catalog, roughly 0.04 mag for 2MRS up to 0.30 mag for
GLADE. Making it exact needs a per-source spread derived from each
colourSpec range and that band's own measured gradient.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Main's #501 replaced the highlightFallback + realOnly boolean pair with a
general per-axis provenance system, and added persisted
orientationIsFallback / diameterIsFallback arrays to GalaxyCatalog. This
branch had independently moved the galaxy colour-ramp evaluation from the
points vertex stage to the fragment stage and repacked VSOut.

Most of the collision was additive and resolved as keep-both.
highlightFallback and realOnly are dropped rather than preserved: main
deleted the concept, so the entries, their reducers and their defaults go
with it.

The one real design clash was the points vertex stage. Main's provenance
highlight REPLACES the ramp colour with a flat debug colour and boosts
brightness, because multiplying a tint into a red galaxy darkens it
instead of making it pop. This branch no longer evaluates the ramp in the
vertex stage at all, since the radial gradient's argument depends on the
fragment's radius.

Resolved via the sign of instanceTint.w. pickColourIndex clamps the colour
index to [0, 2], so it is never negative, which makes a negative .w a free
and unambiguous sentinel for "highlight active: .rgb is already the final
colour, skip the ramp". The fragment collapses the ramp to a multiply by
white in that case. This is the same sign-bit-as-flag idiom the codebase
already uses for axisRatio (orientation fallback) and radiusMpc (diameter
fallback). A debug overlay therefore renders flat rather than
gradient-modulated, which is correct — its job is to read as unmistakably
synthetic.

Both branches also grew the same points uniform struct. The byte ranges
turned out disjoint: provenance at 112..127 with depthFadeEnabled moved to
156, against galaxySbScale/galaxySbMax/galaxyFalloffStrength at 172..180.
Verified the merged WESL struct against packPointUniforms.ts field by
field; UNIFORM_BYTES is 192. Main's "total: 176 bytes" layout comment was
stale and is corrected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…obust

SDSS medium rendered as a couple of dots — most of its galaxies invisible,
though picking still resolved them. The galaxies were being rasterised;
they were roughly 25x too dim.

SDSS marks missing photometry with the numeric sentinel -9999, not NaN and
not an empty field, and the CSV parser only rejected isNaN. The sentinel
reached magG, where absoluteFromApparent(-9999, d) returns about -10040 —
finite, so the zero-point's Number.isFinite guard accepted it.

55 such rows were enough. They dragged the per-catalog zero-point by
-3.49 mag in sdss-medium (157,640 rows) and -1.11 mag in sdss-large
(498,268 rows) — the same 55 rows over a third as many galaxies, which is
why medium vanished while large merely read dim. Since galaxySbAmp
normalises every galaxy against that zero-point, sdss-medium's median
sbAmp was 0.038 where DEFAULT_GALAXY_SB_SCALE's own docstring states the
invariant that a median row sits near 1.0. Measured, 0.05% of sdss-medium
rows exceeded intensity 0.5 against GLADE medium's 43%.

The rows survived every filter by design accident: subsampleByAbsMag ranks
ascending by absolute magnitude, so -10040 sorted as the 55 BRIGHTEST
galaxies and was guaranteed into every tier.

Two complementary fixes rather than one.

isPlausibleMagnitude rejects anything outside -30 < mag < 40 at the
parser. It is an inclusive-range test, not an equality check against known
sentinels: catalogs use several (-9999, -999, 99.99) and enumerating them
means editing the predicate every time a source is added. Applied to the
SDSS, GLADE, 2MRS, Hipparcos and Milliquas parsers. It also subsumes
2MRS's exact `=== 99.999` compare, which a truncated 99.99 would have
slipped past. Milliquas keeps its own literal-0 rule composed on top,
since 0 is a plausible magnitude in general and only Milliquas means
"absent" by it. The DESI, famous-seed and HyperLEDA parsers already guard
correctly and are untouched.

galaxyMeanAbsMag becomes galaxyMedianAbsMag and takes a median. A mean can
be moved arbitrarily far by a single wild value; a median is robust to any
future contamination class without this file needing to know which
sentinels exist — that knowledge belongs in the parser. On real data the
two agree: sdss-medium's median is -20.82 against a sentinel-free mean of
-20.880. The field holding it is renamed to medianAbsMag to keep the
concept from being half-renamed.

The fixes are not redundant. The parser repairs the data and the tier
ranking; the median makes the statistic unwreckable by anything that slips
through. Only the median takes effect on already-built bins — the parser
fix needs a build-tiers run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rulkens rulkens changed the title feat(galaxies): physical surface-brightness point rendering (look spike, stage 1) feat(galaxies): physical surface-brightness rendering + radial colour gradient (look spike) Jul 24, 2026
@rulkens
rulkens marked this pull request as ready for review July 24, 2026 18:31
Per-source colour-gradient spread: DISK_TINT_SPREAD is one shared constant
in ramp-position units, but ramp units per magnitude are fixed per source by
its own colourSpec range, so one knob renders a 7.5x spread in the physical
core-to-rim gradient (~0.04 mag for 2MRS to ~0.30 mag for GLADE).

Famous-seed redshift-distance fallback: M90 bakes at 1.47 Mpc because
HyperLEDA's mod0 misses its error gate by 0.05 mag and the chain falls to
v3k/70, invalid for a Virgo infall member whose CMB-frame velocity is mostly
peculiar motion. The wrong distance drags diameterKpc with it, so the galaxy
mispositions, undersizes, and mis-brightens at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rulkens
rulkens merged commit b8d65eb into main Jul 24, 2026
2 checks passed
@rulkens
rulkens deleted the worktree-galaxy-bloom-point-tuning branch July 24, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant