HomeTutorials03 – Measuring Gravity
03Beginner~3 min

Measuring Gravity

Use radial_profile() to extract χ(r) around a soliton and verify that the well falls off with distance the same way Newton's 1/r potential does.

What you'll learn

  • How to call lfm.radial_profile(chi_array, center, max_radius)
  • How to read prof["r"] and prof["profile"]
  • The Δχ/Δχ ratio test for 1/r fall-off
  • Why gravity is equivalent to curvature of the χ field

Which equation is running?

LevelFieldForces includedSpeed
REAL ←Ψ ∈ ℝGravity onlyFastest (this tutorial)
COMPLEXΨ ∈ ℂGravity + EM~2× slower
FULLΨₐ ∈ ℂ³All 4 forces~6× slower

The 1/r gravity profile measured here is identical at all three levels. We use REAL for speed. The Newtonian formula F = −GM/r² was not injected — the 1/r shape falls out of the equations automatically.

Why 1/r?

The quasi-static limit of GOV-02 (∂²χ/∂t² = 0) reduces to a Poisson equation:

∇²χ = (κ/c²)(|Ψ|² − E₀²)

This is identical in structure to Newtonian gravity (∇²Φ = 4πGρ). A point source produces a 1/r solution. We see this directly in the measured profile.

Full script

"""03 – Measuring Gravity

In example 02 we saw χ drop below 19 around a soliton.  But is
this really gravity?  Let's measure the radial profile of χ and
see if it looks like Newton's 1/r potential.

We place a single massive soliton, equilibrate, evolve, then use
lfm.radial_profile() to extract χ(r) – the gravitational well shape.
"""

import lfm

config = lfm.SimulationConfig(grid_size=64)
sim = lfm.Simulation(config)

center = (32, 32, 32)
sim.place_soliton(center, amplitude=6.0, sigma=4.0)
sim.equilibrate()   # GOV-04 Poisson solution: χ(r) ∝ 1/r is now in the field

print("03 – Measuring Gravity")
print("=" * 55)
print()

# Measure radial profile RIGHT after equilibration — before dynamic ripples.
# equilibrate() uses the Poisson equation (GOV-04 quasi-static limit of GOV-02)
# which gives the exact static 1/r profile we want to verify.
prof = lfm.radial_profile(sim.chi, center=center, max_radius=28)

print("Radial χ profile (distance → chi value):")
print(f"  {'r':>4s}  {'χ(r)':>8s}  {'Δχ':>8s}  bar")
print(f"  {'----':>4s}  {'--------':>8s}  {'--------':>8s}  ---")

chi0 = lfm.CHI0
for r, chi_val in zip(prof["r"], prof["profile"]):
    if r < 1 or r > 24:
        continue
    delta = chi0 - chi_val
    bar = "█" * int(max(0, delta) * 4)
    print(f"  {r:4.0f}  {chi_val:8.3f}  {delta:8.3f}  {bar}")

print()
print(f"  χ at center = {prof['profile'][0]:.3f}  (deepest point)")
print(f"  χ at r=16   = {prof['profile'][16]:.3f}  (approaching vacuum)")
print()

# Check 1/r character: Δχ(r) ∝ 1/r → Δχ(r=4)/Δχ(r=8) should be 8/4 = 2.0
# We measure the static Poisson solution (chi after equilibration, before dynamics).
# GOV-02 in the quasi-static limit ∇²χ = (κ/c²)(|Ψ|² − E₀²) is a Poisson equation,
# whose Green's function in 3D is 1/r — just like Newtonian gravity.
d4 = chi0 - prof["profile"][4]
d8 = chi0 - prof["profile"][8]
if d8 > 0.001:
    ratio = d4 / d8
    print(f"  Δχ(r=4) / Δχ(r=8) = {ratio:.2f}  (expect 2.0 for 1/r)")
    print(f"  GOV-02 Poisson solution: 1/r confirmed from wave mechanics alone.")
else:
    print("  (well is shallow – increase amplitude for clearer 1/r)")

# Power-law fit: more rigorous than one ratio at two points
# Use r=4..8: in-well but before periodic-boundary corrections kick in
delta_chi = chi0 - prof["profile"]
exponent, r_sq = lfm.fit_power_law(prof["r"], delta_chi, r_min=4.0, r_max=8.0)
print(f"  Power-law fit: Δχ ∝ r^{exponent:.2f}  (expect −1.00 for 1/r)")
print(f"  R² = {r_sq:.4f}  (1.0 = perfect 1/r fit)")

print()
print("The well is deepest at the center and drops off with distance,")
print("just like Newtonian gravity – but no F = -GM/r² was used.")
print()

# Confirm stability: run dynamics and check the soliton holds
sim.run(steps=500)
m = sim.metrics()
print(f"Stability check after 500 evolution steps:")
print(f"  χ_min = {m['chi_min']:.3f}  (well persists)")
print(f"  Energy = {m['energy_total']:.3e}  (conserved)")

# ─── 3D Lattice Visualization ──────────────────────────────────────────────
# Requires matplotlib.  Generates: tutorial_03_3d_lattice.png
try:
    import matplotlib; matplotlib.use("Agg")
    import matplotlib.pyplot as _plt
    import numpy as _np
    _N    = sim.chi.shape[0]
    _step = max(1, _N // 20)
    _idx  = _np.arange(0, _N, _step)
    _G    = _np.meshgrid(_idx, _idx, _idx, indexing="ij")
    _xx, _yy, _zz = _G[0].ravel(), _G[1].ravel(), _G[2].ravel()
    _e    = (sim.psi_real[::_step, ::_step, ::_step] ** 2).ravel()
    if sim.psi_imag is not None:
        _e  = _e + (sim.psi_imag[::_step, ::_step, ::_step] ** 2).ravel()
    _ch   = sim.chi[::_step, ::_step, ::_step].ravel()
    _bg   = "#08081a"
    _fig  = _plt.figure(figsize=(15, 5), facecolor=_bg)
    _fig.suptitle("03 – Measuring Gravity: 3D Lattice (Energy | χ Field | Combined)",
                  color="white", fontsize=11)
    _chi_range = _ch.max() - _ch.min()
    _chi_lo    = _ch.max() - max(_chi_range * 0.05, 0.1)
    for _col, (_ttl, _v, _cm, _lo) in enumerate([
        ("Energy |Ψ|²",            _e,  "plasma", max(_e.max() * 0.15, 1e-9)),
        ("χ Field (gravity wells)", _ch, "cool_r",  _chi_lo),
    ]):
        _ax = _fig.add_subplot(1, 3, _col + 1, projection="3d")
        _ax.set_facecolor(_bg)
        _mask = (_v < _lo) if _col == 1 else (_v > _lo)
        if _mask.any():
            _sc = _ax.scatter(_xx[_mask], _yy[_mask], _zz[_mask],
                              c=_v[_mask], cmap=_cm, s=8, alpha=0.70)
            _plt.colorbar(_sc, ax=_ax, shrink=0.46, pad=0.07)
        _ax.set_title(_ttl, color="white", fontsize=8)
        for _t in (_ax.get_xticklabels() + _ax.get_yticklabels() +
                   _ax.get_zticklabels()):
            _t.set_color("#666")
        _ax.set_xlabel("x", color="w", fontsize=6)
        _ax.set_ylabel("y", color="w", fontsize=6)
        _ax.set_zlabel("z", color="w", fontsize=6)
        _ax.xaxis.pane.fill = _ax.yaxis.pane.fill = _ax.zaxis.pane.fill = False
        _ax.grid(color="gray", alpha=0.07)
    _ax3 = _fig.add_subplot(1, 3, 3, projection="3d"); _ax3.set_facecolor(_bg)
    _em  = _e > _e.max() * 0.15 if _e.max() > 0 else _np.zeros_like(_e, dtype=bool)
    _cm2 = _ch < _chi_lo
    if _em.any():  _ax3.scatter(_xx[_em],  _yy[_em],  _zz[_em],
                                c="#ff9933", s=8, alpha=0.55, label="Energy")
    if _cm2.any(): _ax3.scatter(_xx[_cm2], _yy[_cm2], _zz[_cm2],
                                c="#33ccff", s=8, alpha=0.45, label="χ well")
    _ax3.legend(fontsize=7, labelcolor="white", facecolor=_bg, framealpha=0.5)
    _ax3.set_title("Combined", color="white", fontsize=8)
    for _t in (_ax3.get_xticklabels() + _ax3.get_yticklabels() +
               _ax3.get_zticklabels()):
        _t.set_color("#666")
    _ax3.set_xlabel("x", color="w", fontsize=6)
    _ax3.set_ylabel("y", color="w", fontsize=6)
    _ax3.set_zlabel("z", color="w", fontsize=6)
    _ax3.xaxis.pane.fill = _ax3.yaxis.pane.fill = _ax3.zaxis.pane.fill = False
    _plt.tight_layout()
    _plt.savefig("tutorial_03_3d_lattice.png", dpi=110, bbox_inches="tight",
                 facecolor=_bg)
    _plt.close()
    print()
    print("Saved: tutorial_03_3d_lattice.png  (3D: Energy | χ | Combined)")
except ImportError:
    print()
    print("(install matplotlib to generate 3D visualization)")

Step-by-step explanation

Step 1 — Set up and evolve

sim.place_soliton(center, amplitude=6.0, sigma=4.0) sim.equilibrate() sim.run(steps=1000)

Larger amplitude (6.0 vs 5.0 in tutorial 02) gives a deeper well, making the 1/r shape easier to measure.

Step 2 — Extract the radial profile

prof = lfm.radial_profile(sim.chi, center=center, max_radius=18) # prof["r"]       → [0, 1, 2, ..., 18] lattice cell distances # prof["profile"] → mean χ at each radius

radial_profile spherically averages the χ field in shells around the center, returning the mean χ at each integer radius.

Step 3 — Test the 1/r character

d4 = chi0 - prof["profile"][4]   # Δχ at r=4 d8 = chi0 - prof["profile"][8]   # Δχ at r=8 ratio = d4 / d8   # expect ~2.0 for perfect 1/r

For a pure Coulomb/Newton 1/r potential, the depth at r=4 should be exactly double the depth at r=8. We get close to 2 — small deviations are expected because the source has finite width (σ = 4).

Expected output

03 – Measuring Gravity
=======================================================

Radial χ profile (distance → chi value):
     r      χ(r)        Δχ  bar
  ----  --------  --------  ---
     1    15.297     3.703  ██████████████
     2    15.579     3.421  █████████████
     3    15.929     3.071  ████████████
     4    16.325     2.675  ██████████
     5    16.758     2.242  ████████
     6    17.142     1.858  ███████
     7    17.444     1.556  ██████
     8    17.695     1.305  █████
     9    17.916     1.084  ████
    10    18.096     0.904  ███
    11    18.234     0.766  ███
    12    18.349     0.651  ██
    13    18.450     0.550  ██
    14    18.536     0.464  █
    15    18.607     0.393  █
    16    18.669     0.331  █
    17    18.726     0.274  █
    18    18.775     0.225
    19    18.817     0.183
    20    18.854     0.146
    21    18.887     0.113
    22    18.926     0.074
    23    19.000     0.000
    24    19.000     0.000

  χ at center = 15.144  (deepest point)
  χ at r=16   = 18.669  (approaching vacuum)

  Δχ(r=4) / Δχ(r=8) = 2.05  (expect 2.0 for 1/r)
  GOV-02 Poisson solution: 1/r confirmed from wave mechanics alone.
  Power-law fit: Δχ ∝ r^-1.03  (expect −1.00 for 1/r)
  R² = 0.9904  (1.0 = perfect 1/r fit)

The well is deepest at the center and drops off with distance,
just like Newtonian gravity – but no F = -GM/r² was used.

Stability check after 500 evolution steps:
  χ_min = 17.038  (well persists)
  Energy = 2.477e+07  (conserved)

Saved: tutorial_03_3d_lattice.png  (3D: Energy | χ | Combined)

Visual preview

3D lattice produced by running the script above — |Ψ|² energy density, χ field, and combined view.

3D lattice visualization for tutorial 03