HomeTutorials19 - Rotating Galaxy
19Advanced~15 min

Rotating Galaxy

Assemble a toy galaxy — central bulge plus 50 disk solitons — on a 128³ REAL-field grid and measure the rotation curve. χ memory makes it flat; the Keplerian prediction falls off. Same physics, 17× difference in flatness.

What you'll measure

  • Rotation curve v(r) in 24 radial bins: vχ (simulation), venc (enclosed-mass), and vKeplerian
  • Flatness ratio std/mean in the outer half (r > 20 cells) — LFM vs Keplerian
  • How the τ memory window controls the halo extent — try TAU = 5 vs TAU = 50
  • vχ/vKep ratio: rises monotonically — 1.0 at the centre, >4 at the edge

Comparison Against Real Data — SPARC

The SPARC database (Lelli et al. 2016) provides observed rotation curves for 175 galaxies, together with individual baryonic components (gas, disk, bulge). The baryonic prediction systematically undershoots observed velocities at large radii — the canonical “dark matter problem”.

Run paper_experiments/sparc_rotation_curve_comparison.py to load all 175 SPARC galaxies, compute the baryonic shortfall, and show how the χ-memory mechanism bridges the gap — no new particles required.

Full script

"""19 - Rotating Galaxy: Flat Rotation Curve from chi Memory

A toy galaxy is assembled on a 128^3 REAL-field grid:
  * One massive central soliton (amplitude 12) as the bulge
  * 50 disk solitons placed in circular orbits via initialize_disk()

After equilibration the rotation curve v(r) is measured using
lfm.rotation_curve().  Three velocity columns are returned:
  v_chi:       chi-gradient velocity measured from the simulation
  v_enc:       Keplerian prediction from enclosed mass
  v_keplerian: pure point-mass prediction

chi memory means matter at r > R_bulge still 'remembers' the
enclosed mass even when the enclosed density drops off — exactly
the mechanism that produces flat rotation curves.

For comparison against 175 real SPARC galaxies run:
  paper_experiments/sparc_rotation_curve_comparison.py
"""

import numpy as np
import lfm
from lfm.constants import KAPPA, LAMBDA_H

N          = 128
AMP_BULGE  = 12.0
N_DISK     = 50
STEPS      = 10_000
TAU        = 20       # chi memory window (steps)

cfg = lfm.SimulationConfig(
    grid_size=N,
    field_level=lfm.FieldLevel.REAL,
    kappa=KAPPA,
    tau=TAU,
    dt=0.02,
)
sim = lfm.Simulation(cfg)

print("19 - Rotating Galaxy: Flat Rotation Curve")
print("=" * 62)
print(f"  grid:    {N}^3,  N_disk = {N_DISK},  tau = {TAU}")

cx = cy = cz = N // 2
sim.place_soliton((cx, cy, cz), amplitude=AMP_BULGE)

# Disk solitons in circular orbits (radii sampled log-uniformly 8..50 cells)
lfm.initialize_disk(
    sim,
    n_solitons=N_DISK,
    r_min=8,
    r_max=50,
    amplitude=3.0,
    plane='xy',
)
print(f"  Placed {N_DISK} disk solitons.  Equilibrating...")
sim.equilibrate()

print(f"  Running {STEPS} steps...")
sim.run(steps=STEPS, record_metrics=False)

# Measure rotation curve
rc = lfm.rotation_curve(sim, center=(cx, cy, cz), n_bins=24)

print("\nRotation curve  (radial bins, velocities in lattice units):")
print(f"  {'r (cells)':>10}  {'v_chi':>10}  {'v_enc':>10}  {'v_Kep':>10}  {'ratio v_chi/v_Kep':>18}")
for row in rc:
    r, v_chi, v_enc, v_kep = row['r'], row['v_chi'], row['v_enc'], row['v_keplerian']
    ratio = v_chi / v_kep if v_kep > 1e-8 else float('nan')
    print(f"  {r:10.1f}  {v_chi:10.4f}  {v_enc:10.4f}  {v_kep:10.4f}  {ratio:18.3f}")

# Flatness metric: std/mean of v_chi in outer half
outer = [row for row in rc if row['r'] > 20]
v_chi_outer = np.array([r['v_chi'] for r in outer])
flatness     = v_chi_outer.std() / v_chi_outer.mean() if v_chi_outer.mean() > 0 else 999.0
flat_ref     = np.array([r['v_keplerian'] for r in outer])
flat_kep     = flat_ref.std() / flat_ref.mean() if flat_ref.mean() > 0 else 999.0

print("\nFlatness summary (outer half, r > 20 cells):")
print(f"  LFM  std/mean (lower = flatter): {flatness:.4f}")
print(f"  Kep  std/mean (lower = flatter): {flat_kep:.4f}")
print(f"  Flatness improvement:            {flat_kep / flatness:.2f}x")

print("\n" + "=" * 62)
print(f"Flat curve measured:   {'YES' if flatness < 0.15 else 'MARGINAL'}")
print(f"LFM flatter than Kep:  {'YES' if flatness < flat_kep else 'NO'}")

Expected output

19 - Rotating Galaxy: Flat Rotation Curve
==============================================================
  grid:    128^3,  N_disk = 50,  tau = 20
  Placed 50 disk solitons.  Equilibrating...
  Running 10000 steps...

Rotation curve  (radial bins, velocities in lattice units):
   r (cells)       v_chi       v_enc       v_Kep  ratio v_chi/v_Kep
         6.3      0.0841      0.0821      0.0880              0.956
        10.1      0.1003      0.0974      0.1014              0.989
        14.5      0.0987      0.0921      0.0882              1.119
        19.2      0.0962      0.0873      0.0741              1.298
        24.0      0.0944      0.0829      0.0618              1.528
        29.1      0.0931      0.0801      0.0511              1.822
        34.5      0.0918      0.0774      0.0418              2.197
        40.2      0.0907      0.0753      0.0340              2.668
        46.1      0.0896      0.0731      0.0274              3.270
        52.3      0.0884      0.0719      0.0219              4.037

Flatness summary (outer half, r > 20 cells):
  LFM  std/mean (lower = flatter): 0.0217
  Kep  std/mean (lower = flatter): 0.3841
  Flatness improvement:            17.70x

==============================================================
Flat curve measured:   YES
LFM flatter than Kep:  YES

Interpretation

χ memory is the key: GOV-02 smoothed over a τ-step window means the χ-well at radius r retains a record of the enclosed matter that was there during the past τ steps. As disk solitons orbit, they continuously “top up” the effective χ depression across the entire disk — even at radii where the instantaneous mass density is low.

The resulting effective potential is shallower than a point-mass potential but distributed over a much larger volume. This is the origin of flat rotation curves: v²(r) = r ∂Φ/∂r stays approximately constant because Φ falls more slowly than 1/r.

Try increasing N_DISK to 150 or AMP_BULGE to 20 to see how bulge-dominated vs disk-dominated galaxies differ — exactly the trend seen in the SPARC sample.