PulseCore

Chapter 4 · Section 7

Gravity on the Mesh

The BPT law

BPT does not state a method for computing gravity; it states the pull and its constant, and the mesh is a game method that serves them. BPT Part 1.11 (Pulse Diameter, Data Gravity and The Speed of Light) makes gravity the weight of data: "just as gravitational mass warps spacetime, informational weight warps computational flow". BPT Part 3.11 fixes its strength by the Gravity of a Universe, G = 1/(6πρ̄T²) (Part 4.3), and its Game's Reading says how the game must apply it: the matter "and the Data Matter that outweighs it 27 to 5, move in comoving cells, so the expansion appears as Hubble Drag on the bodies' motions (as 1/a²) and as gravity across the grid falling as a⁻³". The same Part explains why a flat grid is enough: on the horn torus "the field is drawn on it by an equal-area map", so that "gravity on the flat grid of cells is then exact on the surface it is drawn on". The code's header says why matter moves "as a particle-mesh code moves matter through the cosmos" (matter.js:9).

The established physics

  • Particle-mesh gravity: mass deposited on a grid by cloud-in-cell weights, Poisson's equation ∇²Φ = 4πG(ρ − ρ̄) solved on a periodic grid by fast Fourier transform, the force read back with the same weights (Hockney & Eastwood 1988).
  • The torus is periodic, so the mean density is subtracted (the "Jeans swindle" of every periodic cosmological code).
  • The discrete Laplacian's own eigenvalues, 2cos(2πk_r/GR) + 2cos(2πk_c/GC) − 4, make the solution exact for the grid.

The game's math

Mesh size. meshB() app.js:1169 picks the block side B (cells per mesh cell):

meshN = 64 on a phone; 256 if cores ≥ 12 and memory ≥ 8 GB; else 128
B = 1; while rows/B > meshN and B < 64: B = 2B

Desktop, 256-cell field: B = 2 (128 × 128 mesh) on most machines, B = 1 (256 × 256) on strong ones. Phone, 64 cells: B = 1. Level 5 on desktop, 128 cells: B = 1. The Matter constructor (matter.js:77) sets GR = ⌊rows/B⌋, GC = ⌊cols/B⌋ and refuses to run (ok = false) unless both are powers of two and at least 4.

Deposit. meshForces(wells) matter.js:212: each body's mass goes to the four nearest block centers with bilinear (cloud-in-cell) weights; its momentum likewise (vgR, vgC, for the mean flow). The Data Matter per block (this.dark, Part 4.10) is added to the density both bodies and wells feel; rho stays matter alone (star formation and the drawing read it). Each well's mass, max(1, mass), is deposited too: "a well is the data that collapsed out of its basin, so the basin and its well balance" (matter.js:207–211).

Two solves. Bodies feel the field of matter + Data Matter + every well (ar, ac). Wells feel the field of matter + Data Matter + sites only (arW, acW), "a well does not pull on itself through the grid" (matter.js:241). The mean of whatever was deposited is subtracted before each solve.

Poisson. poisson() matter.js:254:

Φ_k = 4πG · δρ_k / λ_k,     λ_k = 2cos(2πk_r/GR) + 2cos(2πk_c/GC) − 4,     Φ_0 = 0
a = −∇Φ by central differences:  a_r = −(Φ[r+1] − Φ[r−1]) / (2B)

The B² of density per cell and of the grid Laplacian cancel (matter.js:261). The FFT is an in-place radix-2 transform (fft1, fft2, matter.js:36–58).

Reading back. accel() matter.js:331 interpolates the field at the body with the same four weights.

Dust. condense() matter.js:182: before the burst seeded bodies, each block with data became one body of its pulsing cells (the old start, now unused because seedBodies() sets seeded). After it, each sub-step, if the total of pulsing cells has grown by B² or more since last counted, one body of mass B² is laid at a block chosen in proportion to its data, for each B² of growth (matter.js:193–204). A drop in the total resets the count.

Worked case. Desktop, B = 2: 128 × 128 = 16,384 mesh cells, two FFT solves of that size per force evaluation, two force evaluations per sub-step (kick and kick), four sub-steps a flip: sixteen 2D FFTs of 16,384 points each per flip.

In the game

The mesh itself is never drawn; what it does is. Its pull is what gathers the smooth sky of the burst into filaments and knots along the lattice (Part 4.6), and what carries the clumps toward each other until they merge (Part 4.12).

  • The starlight skin. In 3D, once there are stars, each patch of the torus's skin glows by the real stars it holds, brighter with the log of their number (paintStarlight() scene3d.js:890), so the densest places the mesh has gathered blaze and the voids stay dark. A shader scatters tiny stars over it in proportion to that light (starSkinMaterial() scene3d.js:847).
  • Density in 2D. "Colour by → Density" in the Universe panel paints each cell by the matter density of its mesh block (Part 4.6), so the mesh's own resolution shows as blocks of B × B cells.
  • The dust. As the data goes on growing after the burst, condense() lays new bodies of dust where the data is (below); in the 2D field they appear as faint gray dots.
  • The cost. The HUD's Speed line, "physics … ms/Pulse · drawing … ms · … fps · busy …%" (app.js:4426), is mostly the mesh and the leapfrog (Part 4.16). The run's saved record keeps the device's block side as mesh_b (app.js:4438).

The scale model

The mesh resolves pulls down to about one block; inside that, Part 4.8's direct pull takes over for wells. Bodies do not pull each other directly at all: a body's own neighbors are felt only through the mesh. At B = 1 the mesh is the field itself. The block side follows the device, so a strong desktop resolves gravity twice as finely as an ordinary one on the same universe: a device-class choice.

Where the game and the theory differ

  • The resolution follows the device. BPT Part 3.11 has gravity "exact on the surface it is drawn on". The game solves it on a mesh whose side depends on the device (64 on a phone, 128 on most desktops, 256 on strong ones), so the same universe at the same model level is resolved twice as finely on a strong desktop as on an ordinary one, and bodies within a block do not feel each other's pull at all.
  • An unused start. The first branch of condense() (each block with data becoming one body) is the old start; it no longer runs, since seedBodies() sets seeded at the burst.

Fixed numbers

Number Where What it is Kind
64 meshB() app.js:1172 Mesh side on a phone CAP (device class)
128, 256 meshB() app.js:1172 Mesh side on ordinary and strong desktops CAP (device class)
12, 8 meshB() app.js:1172 Cores and GB that make a device strong CAP (device class)
4, 4 meshB() app.js:1171 Memory and cores assumed when unreported METHOD (defaults)
64 meshB() app.js:1173 Coarsest block side CAP
8 Matter constructor matter.js:77 Default B METHOD (overridden by meshB)
4 Matter constructor matter.js:80 Fewest blocks a side to run METHOD (FFT and differences need room)
power of 2 pow2 matter.js:60 Mesh sides must be powers of two METHOD (radix-2 FFT)
4π poisson() matter.js:261 Poisson's 4πG PHYSICS (Newtonian gravity)
2, 2, 4 poisson() matter.js:260 Eigenvalues of the 5-point Laplacian METHOD (exact for the grid)
2 poisson() matter.js:267–268 Central difference over 2B METHOD
0.5 matter.js:217, 232, 247, 341 Block centers at half a block METHOD (cloud-in-cell)
1 meshForces() matter.js:232 Floor on a well's mass on the grid METHOD
B² (64 at B = 8) condense() matter.js:199, 202 Mass of one body of dust METHOD (one block's worth)
2654435761 nextSeed() matter.js:95 Hash multiplier for body seeds METHOD