The resolved star population has no bound once the halo drain is fixed, and dim stars are most of the cost for almost none of the light #72

Open
opened 2026-08-09 05:05:32 +00:00 by coilyco-ops · 2 comments
Member

Raised by Kai while reviewing #70: should the sim limit total star count, or do the gas mechanics already do it?

Measured answer: the gas mechanics do not, and the thing that currently does is the bug #70 fixes.

Nothing bounds the count

  • No global cap exists. BIRTH_MAX_STARS = 24 is per birth event.
  • Mass recycles, so the gas budget is not a ceiling. Supernovae return SN_GAS_RETURN = 0.8 of progenitor mass and the fountain cycles the rest. Count is a birth-rate against retirement-rate balance, not a mass budget.
  • Mortality is not a sink on session timescales. Lifetime is 900 * (30/m)^2 and the IMF is Salpeter-flavored between 3 and 120, so within a 2500-tick run only about 4% of stars born reach end of life. Half of all births are under 5 solar masses, which live 64800 ticks.

That leaves the spatial phase-mixing drain in process_stellar_halo as the only meaningful sink for the population that dominates the count. #70 cuts that drain by 30-80x, on purpose, because it was #65's halo bump.

Measured, size 500, irregular=>spiral, seed 12345

t baseline stars drained #70 fix stars drained fix, no radiation gate drained
2500 43656 3200 24018 110 90722 256
3000 49188 4270 25193 248 93070 636
4000 52305 6419 26755 568 112697 1694
5000 49458 8993 31095 802 131567 2451

Baseline turns over and starts falling, with 8993 stars retired. Neither fixed configuration turns over: the fix is still adding 4340 per thousand ticks at t=5000, and with the radiation gate open it is adding 18870 and accelerating. Gas does not stop them - 612690 remains at t=5000 against 1932968 at t=0.

So the baseline's plateau is the drain, and #70 removes the drain.

Why this matters beyond tidiness

The e2e runtime spec already reports stars=32081 fps=16.8 jank=99/101 at size 500. The render is marginal at 32k. 131k is not a slower version of that, it is a different product.

Proposal: retire on luminosity, not a count cap

Luminosity is m^2, so the number-heavy end of the IMF is the light-poor end:

share of births share of the light
m < 5 50.2% 3.9%
m < 10 80.9% 11.9%
m < 20 92.9% 24.3%

Half the population produces 4% of the light. Retiring dim main-sequence stars into the existing diffuse stellar-halo reservoir removes most of the point count while losing almost none of the light - and what it loses is smooth, spread-out glow that a gradient renders better than tens of thousands of individual dots. The standing population skews further this way than the birth numbers do, since the massive stars are the ones that die.

This is a generalization of a pattern already in the sim, not a new concept: REMNANT_RESOLVED_AGE, WHITE_DWARF_RESOLVED_AGE, and NEUTRON_STAR_RESOLVED_AGE already retire dim remnants into stellar_halo_mass. Today the criterion is "dim because dead". This extends it to "dim because small", which is the criterion that actually decides whether a star reads as a point on screen.

It is asymptotic by construction: the resolved count settles at birth rate times the time a star spends above the threshold. No arbitrary ceiling, no policy about which star to evict, and the mass ledger already has somewhere to put them.

Why not a hard cap

A count cap needs an eviction policy, and every policy is arbitrary in a way the luminosity threshold is not. It also fails the honesty test: a galaxy that stops forming stars because it hit a render budget is a rendering decision wearing physics clothing. The luminosity floor is a statement about what is resolvable, which is true independent of the budget.

Work

  1. Add a resolved-luminosity floor and retire main-sequence stars below it into stellar_halo_mass, preserving the baryonic ledger the way the remnant paths already do.
  2. Calibrate the threshold against the render budget rather than by eye, using the runtime-perf spec as the target - it already measures fps against star count.
  3. Confirm the diffuse reservoir's brightness accounts for the newly retired light, so total luminosity does not step when a star crosses the threshold.
  4. Check the elliptical hardest. It is the densest scenario and it ran to 268810 stars under one #70 configuration.

Sequencing

Blocks #70's item 6. The #70 fix removes the drain, so landing it without a replacement sink ships an unbounded population. Worth doing first, since it also helps the site today at 32k stars and 16.8 fps.

Raised by Kai while reviewing #70: should the sim limit total star count, or do the gas mechanics already do it? Measured answer: **the gas mechanics do not, and the thing that currently does is the bug #70 fixes.** ## Nothing bounds the count - No global cap exists. `BIRTH_MAX_STARS = 24` is per birth event. - **Mass recycles**, so the gas budget is not a ceiling. Supernovae return `SN_GAS_RETURN = 0.8` of progenitor mass and the fountain cycles the rest. Count is a birth-rate against retirement-rate balance, not a mass budget. - **Mortality is not a sink on session timescales.** Lifetime is `900 * (30/m)^2` and the IMF is Salpeter-flavored between 3 and 120, so within a 2500-tick run only about **4%** of stars born reach end of life. Half of all births are under 5 solar masses, which live 64800 ticks. That leaves the spatial phase-mixing drain in `process_stellar_halo` as the only meaningful sink for the population that dominates the count. #70 cuts that drain by 30-80x, on purpose, because it was #65's halo bump. ## Measured, size 500, irregular=>spiral, seed 12345 | t | baseline stars | drained | #70 fix stars | drained | fix, no radiation gate | drained | |---|---|---|---|---|---|---| | 2500 | 43656 | 3200 | 24018 | 110 | 90722 | 256 | | 3000 | 49188 | 4270 | 25193 | 248 | 93070 | 636 | | 4000 | 52305 | 6419 | 26755 | 568 | 112697 | 1694 | | 5000 | **49458** | 8993 | **31095** | 802 | **131567** | 2451 | Baseline turns over and starts falling, with 8993 stars retired. Neither fixed configuration turns over: the fix is still adding 4340 per thousand ticks at t=5000, and with the radiation gate open it is adding 18870 and accelerating. Gas does not stop them - 612690 remains at t=5000 against 1932968 at t=0. So the baseline's plateau is the drain, and #70 removes the drain. ## Why this matters beyond tidiness The e2e runtime spec already reports `stars=32081 fps=16.8 jank=99/101` at size 500. The render is marginal at 32k. 131k is not a slower version of that, it is a different product. ## Proposal: retire on luminosity, not a count cap Luminosity is `m^2`, so the number-heavy end of the IMF is the light-poor end: | | share of births | share of the light | |---|---|---| | m < 5 | 50.2% | 3.9% | | m < 10 | 80.9% | 11.9% | | m < 20 | 92.9% | 24.3% | Half the population produces 4% of the light. Retiring dim main-sequence stars into the existing diffuse stellar-halo reservoir removes most of the point count while losing almost none of the light - and what it loses is smooth, spread-out glow that a gradient renders **better** than tens of thousands of individual dots. The standing population skews further this way than the birth numbers do, since the massive stars are the ones that die. This is a generalization of a pattern already in the sim, not a new concept: `REMNANT_RESOLVED_AGE`, `WHITE_DWARF_RESOLVED_AGE`, and `NEUTRON_STAR_RESOLVED_AGE` already retire dim remnants into `stellar_halo_mass`. Today the criterion is "dim because dead". This extends it to "dim because small", which is the criterion that actually decides whether a star reads as a point on screen. It is asymptotic by construction: the resolved count settles at birth rate times the time a star spends above the threshold. No arbitrary ceiling, no policy about which star to evict, and the mass ledger already has somewhere to put them. ## Why not a hard cap A count cap needs an eviction policy, and every policy is arbitrary in a way the luminosity threshold is not. It also fails the honesty test: a galaxy that stops forming stars because it hit a render budget is a rendering decision wearing physics clothing. The luminosity floor is a statement about what is resolvable, which is true independent of the budget. ## Work 1. Add a resolved-luminosity floor and retire main-sequence stars below it into `stellar_halo_mass`, preserving the baryonic ledger the way the remnant paths already do. 2. Calibrate the threshold against the render budget rather than by eye, using the `runtime-perf` spec as the target - it already measures fps against star count. 3. Confirm the diffuse reservoir's brightness accounts for the newly retired light, so total luminosity does not step when a star crosses the threshold. 4. Check the elliptical hardest. It is the densest scenario and it ran to 268810 stars under one #70 configuration. ## Sequencing Blocks #70's item 6. The #70 fix removes the drain, so landing it without a replacement sink ships an unbounded population. Worth doing first, since it also helps the site today at 32k stars and 16.8 fps.
Author
Member

The floor works. Three seeds, and it lands the population where the site already runs.

Implemented as GALAXY_ABL_RESOLVED_LUMINOSITY_FLOOR, default off, on main. Rule: an unbound main-sequence star below the floor retires into the diffuse reservoir through the same path aged remnants already use. Association members are exempt whatever their brightness - a cluster should read as a cluster while it is one.

It produces a genuine asymptote

Resolved stars at size 500, t=5000, seeds 12345 / 12346 / 12347:

configuration t=2500 t=5000
#70 fix, no floor 90722 / 77152 / 85472 131567 / 126283 / 123491, still accelerating
fix + floor 25 62962 77559, drifting up
fix + floor 100 31108 / 29122 / 48460 30092 / 23466 / 30107, flat
baseline, no floor 43656 / 37707 / 31069 49458 / 40934 / 31005
baseline + floor 100 22380 / 20490 / 17687 14411 / 23399 / 25281

A floor of 100 - stars under ten solar masses - settles the population at 23-30k across every seed, against 123-131k unbounded. That is where the site already runs (stars=32081 in the perf spec), so #70 lands with no perf regression instead of a 4x one.

There is a neat symmetry: #70 removes a bad sink and #72 adds a good one, and they very nearly cancel. The halo drain was ejecting stars that should never have been ejected; the floor retires stars that were never resolvable. Same population size, honest mechanism.

Floor 25 is not enough - it bounds the growth but the count still drifts up to 77k.

It does not disturb the galaxy

Gas rotation vt sits at 1.0-1.56 across every configuration and seed with no systematic shift, and rotational support 0.17-0.32. So dropping roughly 47% of stellar mass out of the gravitating set does not perturb the gas, and the coarse diffuse-mass grid I expected to need is not needed. That is the measurement paying for itself: I would have built it on spec.

Gas spiral coherence at t=5000, baseline against floor: 0.31/0.34 against 0.26/0.30, and with the #70 fix 0.11/0.27 against 0.21/0.27. Comparable. The first seed showed 0.53 dropping to 0.09 and I nearly reported that as a real effect - the other two seeds show it was noise, which is what spiral coherence does when read off one run.

One result I am deliberately not banking

v/sigma improves with the floor: 1.48-1.69 to 2.24-2.84 at t=5000 under the #70 fix, and 0.20-0.26 to 0.79-1.03 under baseline. Some of that is real and some is compositional - the floor removes the old dim field stars, and those stars were the mush. That is not cheating, since #66's complaint was literally a uniform grey mess of dim points over good structure, and removing them from the point layer addresses it directly. But it is partly a change in what is being measured rather than in how the disk moves, and it should not be counted twice alongside #70's dynamical improvement.

The remaining piece is the renderer, and it is a fork

The retired light has to go somewhere visible. Today stellar_halo_mass renders as a halo ring from 0.62 to 1.32 disk_r with alpha capped at 0.085. Retired disk stars would add light to a component drawn outside the disk, and capped so low it barely brightens. Landing the sink as-is would remove about 12% of the light from the disk and put it nowhere - the galaxy would get dimmer and sparser. That is a visual regression, so the switch stays off until this is decided.

Two credible designs:

A. Retire to a diffuse disk glow. Track retired luminosity on a coarse grid, expose it, render it as a smooth additive layer. Physically cleanest and matches what the light actually is. Costs new Rust state, a serialization version bump, a wasm getter, and a JS render pass. Makes the point layer sparser and adds smooth light underneath.

B. Super-particle subsampling. Instead of retiring dim stars, keep one in every N and multiply its luminosity and mass by N. Total light and total mass are preserved exactly by construction, the spatial distribution statistically, and the count drops by N. No renderer change, no new state, no serialization change. Changes the texture though - fewer, brighter specks rather than many faint ones.

A is the honest physics. B is far cheaper and preserves the light exactly. They look different on screen and that is the deciding factor, not the code.

Status

  • Mechanism: measured and landed, off by default.
  • Threshold: 100, from data, three seeds.
  • Gravity concern: resolved, no diffuse-mass grid needed.
  • Renderer: open, awaiting the A-or-B call.
  • Elliptical stress test (work item 4): pending the design choice, since it changes what to measure.
## The floor works. Three seeds, and it lands the population where the site already runs. Implemented as `GALAXY_ABL_RESOLVED_LUMINOSITY_FLOOR`, default off, on `main`. Rule: an **unbound** main-sequence star below the floor retires into the diffuse reservoir through the same path aged remnants already use. Association members are exempt whatever their brightness - a cluster should read as a cluster while it is one. ### It produces a genuine asymptote Resolved stars at size 500, t=5000, seeds 12345 / 12346 / 12347: | configuration | t=2500 | t=5000 | |---|---|---| | #70 fix, no floor | 90722 / 77152 / 85472 | **131567 / 126283 / 123491**, still accelerating | | fix + floor 25 | 62962 | 77559, drifting up | | **fix + floor 100** | 31108 / 29122 / 48460 | **30092 / 23466 / 30107**, flat | | baseline, no floor | 43656 / 37707 / 31069 | 49458 / 40934 / 31005 | | baseline + floor 100 | 22380 / 20490 / 17687 | 14411 / 23399 / 25281 | A floor of 100 - stars under ten solar masses - settles the population at **23-30k across every seed**, against 123-131k unbounded. That is where the site already runs (`stars=32081` in the perf spec), so #70 lands with no perf regression instead of a 4x one. There is a neat symmetry: **#70 removes a bad sink and #72 adds a good one, and they very nearly cancel.** The halo drain was ejecting stars that should never have been ejected; the floor retires stars that were never resolvable. Same population size, honest mechanism. Floor 25 is not enough - it bounds the growth but the count still drifts up to 77k. ### It does not disturb the galaxy Gas rotation `vt` sits at 1.0-1.56 across every configuration and seed with no systematic shift, and rotational support 0.17-0.32. **So dropping roughly 47% of stellar mass out of the gravitating set does not perturb the gas**, and the coarse diffuse-mass grid I expected to need is not needed. That is the measurement paying for itself: I would have built it on spec. Gas spiral coherence at t=5000, baseline against floor: 0.31/0.34 against 0.26/0.30, and with the #70 fix 0.11/0.27 against 0.21/0.27. Comparable. **The first seed showed 0.53 dropping to 0.09 and I nearly reported that as a real effect** - the other two seeds show it was noise, which is what spiral coherence does when read off one run. ### One result I am deliberately not banking `v/sigma` improves with the floor: 1.48-1.69 to 2.24-2.84 at t=5000 under the #70 fix, and 0.20-0.26 to 0.79-1.03 under baseline. Some of that is real and some is **compositional** - the floor removes the old dim field stars, and those stars *were* the mush. That is not cheating, since #66's complaint was literally a uniform grey mess of dim points over good structure, and removing them from the point layer addresses it directly. But it is partly a change in what is being measured rather than in how the disk moves, and it should not be counted twice alongside #70's dynamical improvement. ### The remaining piece is the renderer, and it is a fork The retired light has to go somewhere visible. Today `stellar_halo_mass` renders as a **halo ring** from 0.62 to 1.32 `disk_r` with alpha capped at 0.085. Retired *disk* stars would add light to a component drawn outside the disk, and capped so low it barely brightens. Landing the sink as-is would remove about 12% of the light from the disk and put it nowhere - the galaxy would get dimmer and sparser. That is a visual regression, so the switch stays off until this is decided. Two credible designs: **A. Retire to a diffuse disk glow.** Track retired luminosity on a coarse grid, expose it, render it as a smooth additive layer. Physically cleanest and matches what the light actually is. Costs new Rust state, a serialization version bump, a wasm getter, and a JS render pass. Makes the point layer sparser and adds smooth light underneath. **B. Super-particle subsampling.** Instead of retiring dim stars, keep one in every N and multiply its luminosity and mass by N. Total light and total mass are preserved exactly by construction, the spatial distribution statistically, and the count drops by N. No renderer change, no new state, no serialization change. Changes the texture though - fewer, brighter specks rather than many faint ones. A is the honest physics. B is far cheaper and preserves the light exactly. They look different on screen and that is the deciding factor, not the code. ### Status - Mechanism: **measured and landed, off by default.** - Threshold: **100**, from data, three seeds. - Gravity concern: **resolved, no diffuse-mass grid needed.** - Renderer: **open, awaiting the A-or-B call.** - Elliptical stress test (work item 4): pending the design choice, since it changes what to measure.
Author
Member

Landed. The floor ships, and neither renderer design was needed.

The visual comparison answered a different question than it was asked

Captured the same galaxy at the same tick - seed 424242, size 500, t=1500, 39880 stars - under four treatments, and measured the disk region rather than eyeballing it:

treatment mean brightness RMSE against unchanged cost
drop - just do not draw them -1.5% 0.0129 nothing
super - keep 1 in 4, brighten 4x -1.0% 0.0114 weight field in the star SoA, flat-state changes
glow - accumulate into smooth haze +11% 0.0287 Rust grid, serialization bump, wasm getter, JS pass

glow was both the most expensive and the furthest from the original: the haze read as grey lumps competing with the gas rather than as starlight. Tunable, but it needed real work to become good.

super beat drop by 0.0015 RMSE, which is not worth super-particle bookkeeping.

So the premise both designs shared was false. They each existed to give 12% of the light somewhere to go, and deleting that light outright costs 1.5% of frame brightness. It is spread too thinly to matter. The Rust floor alone is the whole fix - no renderer change, no new state, no serialization bump.

One process note: the first capture run photographed a fresh galaxy at tick 0 for all three modes. Ticking the frontend on the main thread does not repaint what the worker publishes. Three internally consistent, plausible, useless frames - the same shape of error as the retracted scirc result earlier in this family of issues. The tick counter in the corner is what caught it. The harness now paints explicitly and says so in a comment.

What shipped

RESOLVED_LUMINOSITY_FLOOR = 100.0, ten solar masses, as a per-scenario parameter next to star_drag. An unbound main-sequence star below it retires into the diffuse reservoir through the path aged remnants already use. Association members are exempt whatever their brightness.

Verified active at size 500: 22380 stars at t=2500 with 25309 retired, against 43656 and 3200 before.

The elliptical opts out, and that is work item 4 answered

Applying the disk floor to the elliptical drops spheroid_concentration from the required 0.45-0.85 to 0.28-0.32, failing its scenario test. The reason is structural: the elliptical's defining feature is a concentrated stellar spheroid, and that spheroid is made of the accumulated faint old population. Retiring it is not shedding invisible light, it is deleting the object.

It also exposes a limit on the visual evidence above - those captures were taken on a spiral. For a scenario where the faint population is the image, "drop is fine" does not transfer, and I should not have assumed it would. The elliptical needs its retired light rendered before it can have a floor, so design A is not dead, it is scoped to the one scenario that actually needs it. Left open here.

Status

  • Floor: shipped, per scenario, disk scenarios on and elliptical off.
  • Renderer: not needed for disk scenarios, still needed for the elliptical.
  • Item 4: answered, and it is why the elliptical is exempt.
  • The ?dim= scaffolding is removed. The capture harness stays as e2e/visual-capture.spec.ts, gated behind GALAXY_CAPTURE, because #70 will change the look considerably more than this did.

91 Rust tests, clippy, rustfmt, and check-js green; e2e running. This unblocks #70 item 6.

## Landed. The floor ships, and neither renderer design was needed. ### The visual comparison answered a different question than it was asked Captured the same galaxy at the same tick - seed 424242, size 500, t=1500, 39880 stars - under four treatments, and measured the disk region rather than eyeballing it: | treatment | mean brightness | RMSE against unchanged | cost | |---|---|---|---| | **drop** - just do not draw them | **-1.5%** | **0.0129** | nothing | | super - keep 1 in 4, brighten 4x | -1.0% | 0.0114 | weight field in the star SoA, flat-state changes | | glow - accumulate into smooth haze | **+11%** | 0.0287 | Rust grid, serialization bump, wasm getter, JS pass | `glow` was both the most expensive and the furthest from the original: the haze read as grey lumps competing with the gas rather than as starlight. Tunable, but it needed real work to become good. `super` beat `drop` by 0.0015 RMSE, which is not worth super-particle bookkeeping. **So the premise both designs shared was false.** They each existed to give 12% of the light somewhere to go, and deleting that light outright costs 1.5% of frame brightness. It is spread too thinly to matter. The Rust floor alone is the whole fix - no renderer change, no new state, no serialization bump. One process note: the first capture run photographed a fresh galaxy at tick 0 for all three modes. Ticking the frontend on the main thread does not repaint what the worker publishes. Three internally consistent, plausible, useless frames - the same shape of error as the retracted `scirc` result earlier in this family of issues. The tick counter in the corner is what caught it. The harness now paints explicitly and says so in a comment. ### What shipped `RESOLVED_LUMINOSITY_FLOOR = 100.0`, ten solar masses, as a **per-scenario** parameter next to `star_drag`. An unbound main-sequence star below it retires into the diffuse reservoir through the path aged remnants already use. Association members are exempt whatever their brightness. Verified active at size 500: 22380 stars at t=2500 with 25309 retired, against 43656 and 3200 before. ### The elliptical opts out, and that is work item 4 answered Applying the disk floor to the elliptical drops `spheroid_concentration` from the required 0.45-0.85 to **0.28-0.32**, failing its scenario test. The reason is structural: the elliptical's defining feature is a concentrated stellar spheroid, and that spheroid is *made of* the accumulated faint old population. Retiring it is not shedding invisible light, it is deleting the object. It also exposes a limit on the visual evidence above - **those captures were taken on a spiral.** For a scenario where the faint population *is* the image, "drop is fine" does not transfer, and I should not have assumed it would. The elliptical needs its retired light rendered before it can have a floor, so design A is not dead, it is scoped to the one scenario that actually needs it. Left open here. ### Status - Floor: **shipped**, per scenario, disk scenarios on and elliptical off. - Renderer: **not needed** for disk scenarios, **still needed** for the elliptical. - Item 4: answered, and it is why the elliptical is exempt. - The `?dim=` scaffolding is removed. The capture harness stays as `e2e/visual-capture.spec.ts`, gated behind `GALAXY_CAPTURE`, because #70 will change the look considerably more than this did. 91 Rust tests, clippy, rustfmt, and `check-js` green; e2e running. This unblocks #70 item 6.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/galaxy-gen#72
No description provided.