Real-time 2D gravity sandbox with causal propagation (finite c), GW-strain heatmaps, and a virtual LIGO probe. Numba JIT, CPU-only.
1
stars
45
commits
Python
primary language
Sep 8, 2026
updated
🇬🇧 English · 🇮🇹 Italiano
Canonical repository: https://github.com/alessandro-pioli/AstroCausal_Engine
A real-time laboratory where gravity is recreated as a genuinely causal phenomenon: gravitational information always travels at a finite speed c. Around this core, you can observe scenarios ranging from the complete solar system to the merging of black holes to impacts between dwarf galaxies, with vectors and telemetry, an interactive orbital spawner for Keplerian orbits and Lagrange points, a complete suite of gravitational heatmaps, and the emerging visual manifestation of analogous gravitational waves.
How to navigate the documentation. The project is described in three complementary documents, each for a different reader:
| dΦ/dt spirals (NS binary) | GW Strain at the pericenter (EMRI) | Liénard-Wiechert (0.7c → c) (extreme what if) |
|---|---|---|
![]() | ![]() | ![]() |
| Causal chaos: neutron stars around Sag A* (dΦ/dt) | Roche topology (Alpha Centauri AB) | Shell pattern (double eccentricity BNS) |
|---|---|---|
![]() | ![]() |
In summary:
AstroCausal Engine is an interactive gravitational simulator and a 2D celestial mechanics laboratory. Designed as an astronomical "sandbox", it allows the exploration of stable orbital dynamics in real scale (from the Solar System to systems of moons and satellites) and the study of frontier physical phenomena through a natively causal gravitational model: the forces do not act instantaneously, but propagate at the speed of light c through a system of optimized circular history buffers.
The fundamental architectural choice of the project is to operate in a 2+1D spacetime (two spatial dimensions plus time as an explicit axis) on a flat Euclidean background, without solving the field equations of General Relativity. However, the laws remain those of real three-dimensional physics ($1/r^2$, not the $1/r$ of an intrinsically two-dimensional gravity) and time is absolute in the precise sense of the coordinated time of a distant observer, the same convention by which binary pulsars and gravitational waves are timed in reality. This geometric simplification allows the engine to run in real time on any consumer PC, stressing the CPU to calculate the physics in double-precision floating point (float64) via JIT parallelization.
On the causal core (Newtonian gravity evaluated at the retarded time $t - r/c$), when certain conditions are met, the engine triggers the real radiation reaction of order 2.5PN (Post-Newtonian, the perturbative order at which the loss of energy due to gravitational waves appears) and gives rise to emergent behaviors qualitatively consistent with real relativity (causal aberration, contraction of the Liénard-Wiechert-like field for sources close to $c$, i.e., the same deformation that the field of an electric charge in rapid motion undergoes, chirps and analog gravitational waves measured by a virtual LIGO probe), offering an educational laboratory for exploring and comparing classical mechanics and finite-delay causal dynamics.
The balance between physical fidelity and graphic fluidity is largely in the hands of the user: the engine offers a balance but allows the time step, calculation speed, and heatmap resolution to be adjusted in real time.
These dynamic behaviors are not explicitly programmed, but emerge naturally from the dynamics and dead reckoning (the extrapolation of position from known position and velocity) of the causal gravitational interaction:
The thumbnails are clickable and open the folder with all available media. Each phenomenon listed here is discussed in depth, including equations and validation graphs, in the Physics and Scenario Guide.
| Causal propagation visible to the naked eye: By instantly destroying or creating a body, gravitational information propagates visually at the speed of light. Distant bodies continue to "feel" the destroyed body until the absence front reaches them (and, conversely, a new body remains invisible to distant bodies until the causal front arrives). | ![]() |
| Visual analogy of gravitational waves: In the dΦ/dt heatmap, compact binaries in the inspiral phase (the progressive spiral approach before the merger) produce concentric wavefronts with increasing frequency and amplitude, in perfect visual analogy with the real gravitational waves emitted by the mergers. | ![]() |
| Lagrange points L1–L5: They emerge in the Earth-Moon system and can be dynamically visualized in the Lagrange Hunter. | ![]() |
| Breathing of the Roche Lobe: The periodic expansion and contraction of the lunar Roche Lobe in phase with its orbital eccentricity (the lobe expands at apogee and contracts at perigee). | ![]() |
| Liénard-Wiechert distortion: The causal geometric contraction of the isolines of the potential $\Phi$ transversely to the direction of motion for high-speed sources. | ![]() |
| Gravitational Chirp: The progressive increase in frequency and amplitude of the potential wavefronts emitted by compact binaries in inspiral driven by quadrupole dissipation. | ![]() |
| Parameter-free BNS validation on Peters' formula: in the GW170817 scenario (neutron star binary), the chirp mass estimated by the spectral pipeline of the virtual LIGO probe matches Peters' analytical formula with an error of only 0.97%, in parameter-free mode (only equations from first principles, no calibration coefficient). Details, graphs, and limits in §6.6.1 of the physics guide. | ![]() |
| Emergence of the ISCO and the plunge: In the BBH scenario (black hole binary, GW150914), the orbital separation decays until the masses reach the ISCO threshold, the Innermost Stable Circular Orbit, the last stable circular orbit below which every trajectory plunges (theoretical frequency of 62.06 Hz). At this point, the system spontaneously triggers the rapid spiral fall (plunge) at a frequency of 62.40 Hz without any forcing in the code. | ![]() |
| Parameter-free BBH validation on NR SXS: in the GW150914 scenario (black hole binary), the simulated chirp trace follows the reference numerical relativity curve (SXS:BBH:0305) with an average error of 1.27% throughout the inspiral, compared to 7.47% for Peters' analytical formula at the dominant order (details and graphs in §6.6.2 of the physics guide). | ![]() |
| The single-body dipole in dΦ/dt: the time derivative of the potential of a body in motion alone produces a dipole front, blue in front and red behind, the base on which the spirals of the binary pair are then grafted. | ![]() |
| The passage to perihelion in EMRI: in the early stages of the inspiral, each pericenter releases an isolated strain pulse that propagates as a concentric shell at speed $c$, separated from the next one by large regions of silence. | ![]() |
| Apsidal precession in a strong field: in compact orbits (e.g., EMRI), the orbit precesses in a rosette shape not due to a dedicated routine, but due to the Paczyński-Wiita correction at the pericenter, reinforced by the dead reckoning residue beyond the 2nd order. | ![]() |
How to read the table: DT is the time step, i.e., how much simulated time advances with each tick (smaller = more precise and "slower" in real time). The Causal Radius ($D_{max}$) is the distance within which the forces travel at finite speed $c$ by querying the history buffers; beyond that radius, the interaction returns to instantaneous Newtonian. Unit: $1\text{ AU}$ (Astronomical Unit) $= 149,597,870.7\text{ km}$, the average Earth-Sun distance.
| Scenario | Bodies | DT | Causal Radius | Description |
|---|---|---|---|---|
| Complete Solar System | 36 | 150 s | 64 AU | Sun, 8 planets, Pluto and 26 main moons |
| Solar System (Light) | 10 | 512 s | 64 AU | Only the Sun and 9 planets, without moons: higher DT without losing Keplerian fidelity, outer orbits observable in a reasonable time |
| Galactic Orbit (Sgr A*) | 11 | 512 s | 64 AU | Solar System orbiting at 230 km/s around Sagittarius A* |
| Chaotic Cluster | 100 | 64 s | 64 AU | N-body stress test with central BH of 1000 M☉ |
| Earth - Moon - ISS - Hubble | 4 | 1 s | 1 AU | Geocentric regime with ISS and Hubble in LEO orbit |
| Sun - Earth - Moon - Artemis II | 4 | 0.16 s | 1 AU | Passive translunar cruise of Orion on real JPL Horizons vectors, up to the free-return flyby |
| Complete Jovian System | 14 | 60 s | 1 AU | Jupiter and 13 moons (inner, Galilean, irregular) |
| Approach to c (0.999c) | 1 | 0.16 s | 320 LY (~20M AU) | Sun at 0.999c: Liénard-Wiechert distortion (20 GB RAM) |
| Approach to c (0.9c) | 1 | 1.6 s | 1742 LY (~110M AU) | Light version (10 GB RAM) |
| Approach to c (0.7c) | 1 | 16 s | 8710 LY (~550M AU) | Ultra-light version (5 GB RAM) |
| NS Binary: Stable Orbit | 2 | 1 ms | 640 AU | Two neutron stars ~1.5 M☉ at 40,000 km |
| NS Binary: Extreme Eccentricity | 2 | 1 μs | 3 AU | Highly eccentric twin orbits (apocenter 4000 km, pericenter 200 km) |
| NS Binary: Pre-Collision | 2 | 1 μs | 2 AU | Late inspiral, merger in ~59.7 s simulated |
| GW170817 | 2 | 1 μs | 3 AU | Replica of the first multi-messenger event (merger in ~13.9 s simulated) |
| GW150914 | 2 | 1 μs | 3 AU | First GW event detected by LIGO (merger in 52.034 s simulated, theoretically initialized at T-60s) |
| GW190814 | 2 | 1 μs | 3 AU | The most asymmetrical merger (q = 0.112): 23 M☉ BH and 2.6 M☉ mass gap object (initialized at T-20s via Peters) |
| Alpha Centauri + Polyphemus | 9 | 150 s | 32 AU | Real triple system + fictional system from Avatar |
| Extreme Orbits Laboratory | 6 | 0.2 s | 2 AU | Central BH + 5 test particles (e=0 → hyperbolic) |
| EMRI: Relativistic Plunge | 2 | 0.05 s | 1200 AU | Extreme Mass Ratio Inspiral: a light black hole spirals into a much more massive one (ratio 1:100) |
| Collision between Dwarf Galaxies | 202 | 150 s | 64 AU | Near-frontal collision of two 100-star galaxies |
| Empty Scenario | 0 | 1 s | From astro_settings.ini | Empty universe for free construction (can be set via .ini file) |
| Key | Action |
|---|---|
Mouse drag | Camera pan |
Mouse wheel | Zoom in/out |
WASD / Directional arrows | Camera pan (continuous movement of the view) |
Double click on body | Lock camera on selected body |
Double click on empty space | Field probe at cursor point (Φ, dΦ/dt, Tidal) |
TAB | Cycle between active bodies |
| Key | Action |
|---|---|
SPACE | Pause / Resume |
1-5 | TPS multiplier: 1×, 10×, 100×, 1000×, 10000× physics steps per frame, adjusts simulation speed without affecting model accuracy |
T | Halves the DT: more precise, halves the simulation speed, more RAM used |
Y | Doubles the DT: less precise, doubles the simulation speed, less RAM used |
C | Switch Newtonian ↔ Causal (complete reconstruction) |
BACKSPACE | Close and return to the launcher |
| Key | Action |
|---|---|
H | Cycle heatmap mode: OFF → Φ Scalar [Causal] → dΦ/dt [Causal] → Tidal Stress [Newtonian] → OFF |
L | Cycle pair heatmaps: Lagrange Hunter → Roche Topology [Newtonian] → GW Strain [Causal] → Φ (requires body with lock and dominant attractor) |
R | Show/hide orbital trails |
G | Cycle heatmap resolution: AUTO → 1/1 → 1/2 → 1/4 → ... → AUTO |
M | Toggle legend (in Tidal) or theoretical Lagrange markers (in Lagrange Hunter) or ideal circular orbit (in Roche Topology) |
F | Key legend (overlay) |
| Key | Action |
|---|---|
P | Place/remove LIGO probe at cursor position |
N | Open the Orbital Spawner at the cursor position |
K | Request the destruction of the body with lock (confirm Y/N) |
numpy
pygame-ce
numba
matplotlib
scipy
# Clone the repository
git clone https://github.com/alessandro-pioli/AstroCausal_Engine.git
cd AstroCausal_Engine
# Install the dependencies
pip install -r requirements.txt
# Start the launcher
python launcher.py
Note: On the first startup, Numba compiles the physics and graphics kernels and caches them to disk (therefore, only the first time). Compilation is fast, but it can produce brief stutters the first time you activate a function that has not yet been compiled during use (for example, during the first cycle between heatmaps). This is normal and disappears immediately afterwards.
[Causal] (H key)Color map of the gravitational potential, calculated from the historical (causal) positions of the bodies. For a single moving body, it shows the potential well that accompanies it; for bodies in rapid uniform rectilinear motion, the Liénard-Wiechert denominator deforms and compresses the isolines transversely to the direction of motion (analogous to the distortion of the electric field of a moving charge). The classic red-blue "dipole" does not belong to this map, but to the dΦ/dt variation described below.
[Causal] (H key × 2)Represents the temporal variation of the scalar gravitational potential, calculated from the historical (causal) positions. For a moving body sufficiently distant from the others, the characteristic dipole appears: a leading blue front (where the potential deepens as the body approaches) and a trailing red front (where it relaxes). In compact binary mergers, the fronts become concentric and increase in frequency and amplitude: the visual scalar analogue of gravitational waves.
[-4, 2]): increases or decreases the visual intensity. The higher it is, the easier it is to see the fronts of the dipoles merge and blend with those emitted by the more massive distant bodies; the lower it is, the more the nearby fronts are isolated, preventing the screen from being overwhelmed.[Newtonian] (H key × 3)Map of the deviatoric norm of the Hessian matrix of the Newtonian gravitational potential, calculated from the instantaneous positions. The components of the Hessian $\partial^2 \Phi / \partial x_i \partial x_j$ are calculated analytically for each body:
$$H_{ij} = G \cdot m \left(\frac{\delta_{ij}}{r^3} - \frac{3 x_i x_j}{r^5}\right)$$
The stress displayed is the difference between the two eigenvalues of the Hessian, $\sqrt{(\Phi_{xx} - \Phi_{yy})^2 + 4\Phi_{xy}^2}$ (proportional to the deviatoric part of the tensor): it measures the maximum shear stress, i.e., how much a body would be stretched in one direction and compressed in the orthogonal direction. It highlights areas of extreme tidal stress (for example, the orbit of Io around Jupiter). The coloring is on fixed physical scales: from blue (safe region) to red (structural disintegration) to white (near the singularity). The M key shows the legend with the thresholds, so you can see at a glance at what stress a body would be disintegrated.
[Newtonian] (L key with selected body)Co-rotating 2-body frame (selected body + dominant attractor) for identifying orbital equilibria. This mode does not render a continuous heatmap of the field, but identifies and highlights the L1–L5 Lagrange points as discrete bright points on a completely black background. The kernel calculates the gradient and the Hessian of the potential and uses a distance estimator based on the Newton-Raphson method in 2D ($r_{est} = |H^{-1} \nabla \Phi|$) to draw shaded "blobs" at the gradient zeros. The points are classified topologically using the Hessian: the unstable saddle points (L1, L2, L3) appear as red points, while the stable maxima of the co-rotating potential (L4, L5) appear as blue points. The local minima of the potential (gravitational wells at the center of the bodies, with $D > 0$ and $\text{Tr}(H) > 0$) are excluded from the filter on the Hessian trace, preventing the appearance of false blue blobs superimposed on the bodies. Pressing M overlays the analytical theoretical markers for an immediate comparison with the points that emerge numerically from the calculation.
[-8, 8], default 0.0): Adjusts the size of the displayed Lagrange points. By reducing the sensitivity, the points shrink to indicate the exact equilibrium coordinate precisely; by increasing it, the points expand to show the surrounding area of gravitational attraction. Thanks to automatic calibration, the default value of 0.0 clearly shows the points for any system (from Saturn to binary black holes).[Newtonian] (L key × 2)Map of the effective potential in the co-rotating frame of the selected pair (body + dominant attractor). The angular velocity $\omega$ is kinematically derived from the instantaneous specific angular momentum of the pair ($h = \vec{r} \times \vec{v}_{rel}$, $\omega = h / r^2$): the frame rotates like a rigid disk (each point co-rotates at the same $\omega$, with linear velocity $v = \omega r$). The effective potential adds the complete N-body gravity and the centrifugal term, net of the free-fall drag due to third bodies.
The map encodes two independent pieces of information, to be read separately:
The Roche lobe does not coincide with the boundary between red and blue (that is the line where the curvature $D$ changes sign, a different geometric locus): it is the equipotential of $\Phi_{eff}$ that passes through L1, and it can be visually read from the dark channels around the bodies. The low-force "eight" figure that closes right on the L1 saddle marks the maximum volume that a body can occupy before its matter overflows (Roche Lobe Overflow). The more extreme the mass ratio, the more the secondary's lobe shrinks into an elongated "drop" along the tidal axis.
[-8, 8]): raises or lowers the overall brightness to bring out the faintest details or darken the background.[0, 100]): controls the sharpness of the brightness transition; increasing it thins the dark channels around L1 and makes them sharper, making it easier to identify the overflow point.[Causal] (L key × 3)The most sophisticated visualization of the dynamic field: it maps the projected quadrupole gravitational strain of the selected pair. For each pixel, the kernel reads the position and velocity of each body at the retarded time of that pixel (double causal retrieval on the history buffers), subtracts the motion of the center of mass, and projects the retarded velocity along the pixel-source direction: the quadratic difference between the radial and tangential components reproduces the exact quadrupole angular symmetry ($\ell=2$) of the real gravitational radiation, with the characteristic four alternating cyan/red lobes and, for compact binaries in inspiral, the radiative macro-spirals propagating outward at speed $c$. It is the spatial counterpart of the point LIGO probe (same physics, same kinetic regularization). The complete mathematics, the effects of per-body causality, and the post-merger artifacts are documented in §7.6 of the physics guide.
The model is 2+1D with absolute time: all the dynamics exist in the two dimensions of the plane (a slice of the 3D universe, with the $1/r^2$ laws of three-dimensional physics) while a single universal clock marks the time for each body. It is the point of view of a distant observer, without time dilation or curved metric. Relativity enters from the side of the interactions, with causality at finite speed $c$ and the corrections described below (full framework in §1 of the physics guide).
The interaction between each pair of bodies follows Newton's law of universal gravitation:
F = G · M · m / r²
with the crucial difference that the position, velocity, and mass of the source body are taken from the history buffer at the retarded time $t_{ret} = t - r/c$, where $r$ is the distance and $c$ is the speed of light.
For sources in relativistic motion, the gravitational potential is corrected by inserting the classical Liénard-Wiechert denominator $(dist - \vec{v} \cdot \vec{r}/c)$ to describe the field contraction, concentrating the gravitational force orthogonally to the direction of motion (further details in §5 of the physics guide).
[!NOTE]
How the simulator avoids aberration instability
In discrete causal gravity, the aberration of force (due to the fact that gravity points towards the retarded position) introduces a fictitious torque that tends to rapidly widen the celestial orbits. To mitigate this numerical instability and preserve long-term Keplerian stability, the engine implements a Hybrid Dead Reckoning (the technique, borrowed from navigation, of estimating where a body is now based on where it was and its velocity) at the JIT kernel level:
- Quadratic Dead Reckoning (2nd order Taylor): for stable orbits and ordinary velocities, the position of the source is extrapolated by integrating historical velocity and acceleration at the instant of emission: $$\vec{x}_ {eff} = \vec{x}_ {ret} + \vec{v}_ {ret} \Delta t_ {flight} + \frac{1}{2}\vec{a}_ {ret} \Delta t_ {flight}^2$$
- Dead Reckoning bypass in the GW Regime: in an extreme relativistic regime (close to the merger, with relative velocity of the pair greater than 10% of $c$ and distance less than $1000 \cdot R_s$; for equal masses the criterion is equivalent to 5% of $c$ for a single body, but it also remains valid for asymmetric pairs where the heavy body moves slowly), the engine disables linear extrapolation and uses the exact present position of the source for both direction and distance in the calculation of forces. This fundamentally eliminates the accumulation of periodic radial error $O((v/c)^2)$ responsible for orbital instability.
In compact binary mergers, the orbit decays due to the emission of gravitational waves. The engine implements first-order non-conservative dissipative acceleration (2.5PN-order radiation reaction) according to the Damour-Deruelle real relativistic formulation for the relative acceleration $\vec{a}_{rel}$ (theoretical context and history of implementation in §6.2-6.5 of the physics guide):
$$\vec{a}_{rel} = \frac{8}{5}\frac{G^2 M \mu}{c^5 r^3}\Big[\dot{r}\big(18v^2 + \tfrac{2}{3}\tfrac{GM}{r} - 25\dot{r}^2\big)\hat{n} - \big(6v^2 - 2\tfrac{GM}{r} - 15\dot{r}^2\big)\vec{v}\Big] $$
where $M$ is the total mass of the pair, $\mu$ is the reduced mass, $\hat{n}$ is the separation unit vector, and $\vec{v}$ is the relative velocity. This acceleration is calculated and applied to each body based on its reciprocal mass contribution ($m_{src}/M$), ensuring the conservation of the overall linear momentum. The calculation operates in parameter-free mode, delegating the evolution of the orbit solely to the theoretical expression of order $2.5\text{PN}$.
To ensure the conservation of orbital energy and the long-term stability of complex gravitational systems, the engine adopts a Velocity Verlet integration scheme (implemented in the Numba JIT kernels in kernel_single.py, kernel_double.py, and kernel_triple.py; the analysis of the truncation error is in §4 of the physics guide). Each physics integration step follows this precise time sequence:
The DT parameter ($\Delta t$) is the fundamental constant that governs the temporal discretization of the model. Its choice is the most determining factor in the balance between physical accuracy, sampling capacity, and system resources, due to three competing dynamics:
As the time step of the Velocity Verlet algorithm, $\Delta t$ defines the local truncation error of the trajectory ($O(\Delta t^4)$ for the positions).
Since gravitational forces propagate at the finite speed $c$, each body must calculate the interactions by going up its light cone to the maximum flight time: $$t_{flight_max} = \frac{D_{max}}{c}$$ where $D_{max}$ is the maximum operational causal distance set for the scenario. The logical depth of the memory ring buffers for each body must cover at least $t_{flight_max}$. The number of elements $N_{elements}$ to be allocated for each buffer of each body therefore scales as: $$N_{elements} = \frac{t_{flight_max}}{\Delta t} \propto O\left(\frac{1}{\Delta t}\right)$$
This relationship shows how the RAM requirement is inversely proportional to $\Delta t$. However, the constraint is managed upstream: each preset chooses its own causal cone radius ($D_{max}$), and the SimulationManager reads it, sizing the buffers accordingly to optimize memory. For this reason, the predefined scenarios have "ideal" values of $D_{max}$, chosen on a case-by-case basis:
SimulationManager sets the maximum causal radius $D_{max}$ to only 3 AU (Astronomical Units), a small distance but amply sufficient to describe the entire final inspiral and coalescence phase of the pair.With the same hardware performance (TPS - Ticks Per Second), a smaller $\Delta t$ slows the progression of the simulated real time with respect to the user's real clock time. The engine compensates for this effect by multiplying the calculations per frame (via the in-game speed multiplier 1-5), but at the cost of an additional linear calculation load on the CPU.
The Simulation Radius (or Sim Radius) defines the maximum extent of the causal interaction. It works as a sort of "radar" or causal horizon centered on each celestial body:
For an ideal simulation, the simulation radius must be set to a value large enough to allow each body to easily reach any other active coordinate in the scenario. This causes the causal horizons to overlap entirely, ensuring reciprocal and consistent causality throughout the simulation.
AstroCausal_Engine/
├── launcher.py # Tkinter launch GUI (preset, DT, resolution)
├── main_gui.py # Main Pygame loop (events, physics, rendering)
├── ligo_analyzer.py # LIGO post-processing (spectrograms, chirp mass)
├── astro_settings.ini # User configuration file (editable)
├── config.py # Internal settings loader (do not modify)
├── core/
│ ├── data.py # Global state: NumPy arrays, physical constants
│ ├── engine.py # Physics engine (JIT kernel orchestrator)
│ ├── bodies.py # CelestialBody class
│ ├── presets.py # Scenario definitions (Solar System, GW, etc.)
│ ├── simulation_manager.py # Dynamic reconstruction (rebuild, alloc, snapshot)
│ ├── space_probe.py # LIGO probe controller
│ ├── global_state.py # UI/simulation state (pause, view mode, etc.)
│ ├── event_handler.py # Pygame event dispatcher
│ └── jit_kernels/ # Numba JIT kernels
│ ├── kernel_single.py # Integration with single buffer (L0)
│ ├── kernel_double.py # Integration with double buffer (L0 + L1)
│ ├── kernel_triple.py # Integration with triple buffer (L0 + L1 + L2)
│ ├── graphics_kernel.py # Field rendering (Φ, dΦ, Roche, Tidal)
│ └── kernel_helper_inline.py # Core of the computation: causal forces, dead reckoning, collisions, LIGO probe
├── ui/
│ ├── camera.py # 2D camera (pan, zoom, lock)
│ ├── gravity_renderer.py # GPU-like heatmap renderer on CPU
│ ├── master_renderer.py # Final layer composition
│ ├── overlay_renderer.py # HUD, telemetry, legends
│ ├── input_controller.py # Input → action mapping
│ ├── orbital_spawner.py # Interactive spawner with Lagrange points
│ ├── hud_components.py # Vertical faders for sensitivity
│ ├── game_console.py # In-game console with timestamped logs
│ └── tutorial_popup.py # Tutorial popup system
└── utils/
├── loading_splash.py # Loading splash screen
├── formatting.py # Unit formatting (km, AU, dt)
├── performance_manager.py # Heatmap resolution auto-tuner
├── event_logger.py # Impact and death tracker
└── gc_worker.py # Asynchronous garbage collector
launcher.py ──(subprocess)──► main_gui.py
│
├─ show_splash_and_load()
│ ├─ presets.get_preset()
│ └─ rebuild_simulation() ← allocates buffers, computes memory
│
├─ Engine(bodies) ← compiles JIT kernels
│
└─ MAIN LOOP
├─ EventHandler.handle_events()
├─ Engine.tick(speed_mult)
│ └─ kernel_single / kernel_double / kernel_triple
└─ MasterRenderer.render_all()
└─ GravityRenderer → graphics_kernel
For the full account of the engineering choices (DOD, branchless dispatch, asynchronous GC, adaptive LOD buffers), see the document ARCHITECTURE_DEEP_DIVE.md.
AstroCausal Engine is an educational and numerical exploration tool. It has the following physics limitations with respect to formal general relativity:
Running an interactive physics simulation at 60 FPS (the frames generated per second) means having a maximum budget of 16.6 ms per frame. In this time interval, the CPU must sequentially run both the physics engine and the rendering of the gravitational field heatmaps in the background.
The real load is largely in the hands of the user. The engine offers a balance but allows it to be adjusted in real time: you can deliberately throttle either the graphics side (high resolution with many bodies in the scene) or the physics side (many bodies with a high multiplier).
The two extremes: with more than twenty bodies at multiplier 5 (10,000×: the engine aims for 10,000 physics ticks per frame), it is normal to drop below 10 FPS. It is a user choice, because it is needed when you want the maximum advancement of the simulated time (more TPS, at a potentially massive cost of FPS). On the other hand, in compact mergers at $\Delta t = 1 \mu\text{s}$, few bodies are needed: there, the engine can handle 600,000 TPS and 60 FPS stably even with the dΦ/dt heatmap at native 2K resolution, and the high multipliers run without bottlenecks. In fact, several presets use $\Delta t = 1 \mu\text{s}$ with the expected event at 15 simulated seconds or more: at 600,000 TPS, the simulation runs at about 0.6 simulated seconds for each real second, enough to quickly reach the vicinity of the event, before lowering the multiplier and slowing down by orders of magnitude until the individual microseconds can be observed in super slow-motion. In short, the engine finds the balance but it remains interactive: the strategies below are used to consciously govern it, not to survive everyday use.
The asymptotic analysis of the cost per frame (physics versus rendering per pixel, with the worst-case formula) is documented in §2.3 and §3 of ARCHITECTURE_DEEP_DIVE.md. For practical purposes, one relationship is sufficient: the TPS (Ticks Per Second), the actual physics advance rate, are the product of the multiplier's ticks per frame (keys 1-5: 1, 10, 100, 1000, or 10000) times the real FPS. The engine adopts a predefined target of 60 FPS (which can be unlocked or modified in the .ini file): at 60 FPS with a multiplier of 5, the theoretical ceiling is 600,000 TPS (as in the presets of compact 2-body mergers). In very crowded scenarios (e.g., galactic clashes with ~200 bodies), the weight of the physics lowers the real FPS, proportionally dragging the TPS down as well.
Concrete example (complete Solar System, 36 bodies, on the reference hardware). Physics alone supports a ceiling of about 75,000 TPS. Since the multiplier does not require TPS but ticks per frame, the effect is as follows:
5 (10,000 ticks/frame): to stay at 60 FPS, 600,000 TPS would be needed, far beyond the limit. The engine still executes the 10,000 ticks required per frame, so the FPS drops to $75{,}000 / 10{,}000 = 7.5$ FPS.4 (1,000 ticks/frame): $1,000 \times 60 = 60,000$ TPS is enough for 60 FPS, below the ceiling. The scenario returns to being fluid at 60 FPS.The rule of thumb is $\text{FPS} = \min(60,\ \text{TPS ceiling} / \text{ticks per frame})$: when a scenario is populated, simply lower the multiplier to bring the FPS back to the maximum, choosing each time how much simulated time to sacrifice for smoothness.
There is also a lever orthogonal to the multiplier: if you need to accelerate the simulated time without losing FPS, just double the DT (Y key). Each tick advances twice the simulated time with the same calculation cost, so the speed of time doubles without sacrificing a single frame. Here the price is not FPS but numerical precision, so it is only worthwhile where the physical context allows it (stable orbits, far from relativistic regimes that require a microscopic DT).
The pixel-by-pixel calculation of heatmaps is the dominant graphics load on the CPU. To contain it, the engine uses three levers:
Summary: how to recover FPS and speed up the simulation
| Action | Effect | Cost |
|---|---|---|
Lower the speed multiplier (keys 1–5) | fewer physics ticks per frame, therefore FPS recovered (raising it towards 5 can cause them to drop, but not always) | less simulated time per frame |
Double the DT time step (Y key) | more simulated time speed with the same FPS, and even less RAM | only numerical precision (where the physical context allows it) |
Reduce the resolution of the heatmap (G key, cycles through the scale factors) | lighter field rendering, FPS recovered | visual detail of the heatmap |
Turn off the heatmap (H key, cycle through the modes until OFF) | resets the graphics cost: the entire frame budget goes to physics, maximum FPS gain | no heatmap on screen |
Causal propagation requires access to past states that are arbitrarily deep in time. To avoid RAM explosion and protect the CPU cache, the history is a ring buffer system with three hierarchical levels of resolution: L0 samples every single tick for close interactions, L1 and L2 sample the remote past at gradually lower resolution. Not all levels are always allocated: at each startup, the engine chooses the combination (only L0, L0+L1, or all three) by comparing the estimated footprint with the L3 cache of the CPU detected on the machine, so the same simulation can allocate different buffers on different PCs. If the requested memory exceeds the available memory, an OOM (Out Of Memory) protection intercepts the error and displays it with a graphic dialog instead of closing the program.
The fine mechanics of the buffers are fully documented in §2 of ARCHITECTURE_DEEP_DIVE.md, where you can find: the 3D data structure [body, slot, 5 parameters] with dimensions to the power of 2 and indices managed via AND bitmask (to eliminate the cost of modulo division in hot loops); the sampling strides of L1 (32 ticks) and L2 (256 ticks) with the mode selection criteria; the ultra-ECO placeholder allocation with OOM protection; the L0 → L1 → L2 causal cascade query with the complete Earth-Sun numerical example; the double causal retrieval (two cascade readings to solve the implicit time-of-flight equation); and the reconstruction of historical accelerations by finite differences (the buffers do not store them, to save memory).
The LIGO analyzer is an independent pipeline accessible from the launcher, intended for the spectral post-processing of the .npy binary dumps generated by the simulation probe.
During a simulation, the probe is placed with the P key on a point in space (ideally near a binary in inspiral; the system suggests when and where through the RADAR alerts). From that moment, at each tick the probe accumulates in a ring buffer the strain $(v_x^2 - v_y^2)\cdot m/r$ relative to the center of mass, always reading the L0 buffer at high resolution. The signal is saved as .npy in ligo_output/ upon exiting the simulation (or when changing DT), ready to be loaded into the analyzer.
$$f(\tau) = \frac{1}{\pi} \left(\frac{5}{256}\right)^{3/8} \left(\frac{c^3}{G M_{chirp}}\right)^{5/8} \tau^{-3/8}$$
$$M_{chirp} = \frac{c^3}{G} \left[\frac{5}{96 \pi^{8/3}} \frac{\dot{f}}{f^{11/3}} \right] ^{3/5}$$
where $f$ is the instantaneous frequency detected and $\dot{f} = df/dt$ is its time derivative.
[!WARNING]
Validation Disclaimer
The author is neither a physicist nor a mathematician by trade. The engine, its architecture, the numerical choices and the validation work are the author's own. The simulator explicitly computes a small set of standard formulas (retarded-time gravity, Velocity Verlet, Liénard-Wiechert, Paczyński-Wiita, Damour-Deruelle $2.5\text{PN}$ reaction), taken from standard references; no free calibration coefficients remain and the engine runs parameter-free. Language models were used as writing assistance for the documentation, and for a few implementation details that are flagged in the relevant sections. The formal treatment would still benefit a great deal from a look by professionals in the field, an aspect already outlined in the Roadmap below.
float64) on the CPU to avoid drift and numerical errors. Relevant side effect: once the rendering kernels (currently parallelized with prange on the width axis, see core/jit_kernels/graphics_kernel.py) leave the CPU, the freed cores return milliseconds per tick to the physics loop O(N²), opening up a currently non-existent calculation budget that could allow the calculation of conservative post-Newtonian terms (1PN, 2PN).jinja2 or string.Template) that produces the specific single/double/triple and parallel/sequential files from a single abstract kernel, with the buffer constants statically expanded. It would keep the loop hot without if (the template pastes the right code for each variant before compilation), eliminating manual duplication of the scaffolding.This project is distributed under the GNU GPL v3.0 license. In summary: anyone can study, use, and modify the code, but any redistribution, even modified, must remain open source under the same license and acknowledge the origin. The full text is in the LICENSE file.
Copyright © 2026 Alessandro Pioli
Developed by Alessandro Pioli. Independent project for the simulation and visualization of emergent causal gravitational physics.
45 commits
Python
100.0%
Real-time 2D gravity sandbox with causal propagation (finite c), GW-strain heatmaps, and a virtual LIGO probe. Numba JIT, CPU-only.
1
stars
45
commits
Python
primary language
Sep 8, 2026
updated
🇬🇧 English · 🇮🇹 Italiano
Canonical repository: https://github.com/alessandro-pioli/AstroCausal_Engine
A real-time laboratory where gravity is recreated as a genuinely causal phenomenon: gravitational information always travels at a finite speed c. Around this core, you can observe scenarios ranging from the complete solar system to the merging of black holes to impacts between dwarf galaxies, with vectors and telemetry, an interactive orbital spawner for Keplerian orbits and Lagrange points, a complete suite of gravitational heatmaps, and the emerging visual manifestation of analogous gravitational waves.
How to navigate the documentation. The project is described in three complementary documents, each for a different reader:
| dΦ/dt spirals (NS binary) | GW Strain at the pericenter (EMRI) | Liénard-Wiechert (0.7c → c) (extreme what if) |
|---|---|---|
![]() | ![]() | ![]() |
| Causal chaos: neutron stars around Sag A* (dΦ/dt) | Roche topology (Alpha Centauri AB) | Shell pattern (double eccentricity BNS) |
|---|---|---|
![]() | ![]() |
In summary:
AstroCausal Engine is an interactive gravitational simulator and a 2D celestial mechanics laboratory. Designed as an astronomical "sandbox", it allows the exploration of stable orbital dynamics in real scale (from the Solar System to systems of moons and satellites) and the study of frontier physical phenomena through a natively causal gravitational model: the forces do not act instantaneously, but propagate at the speed of light c through a system of optimized circular history buffers.
The fundamental architectural choice of the project is to operate in a 2+1D spacetime (two spatial dimensions plus time as an explicit axis) on a flat Euclidean background, without solving the field equations of General Relativity. However, the laws remain those of real three-dimensional physics ($1/r^2$, not the $1/r$ of an intrinsically two-dimensional gravity) and time is absolute in the precise sense of the coordinated time of a distant observer, the same convention by which binary pulsars and gravitational waves are timed in reality. This geometric simplification allows the engine to run in real time on any consumer PC, stressing the CPU to calculate the physics in double-precision floating point (float64) via JIT parallelization.
On the causal core (Newtonian gravity evaluated at the retarded time $t - r/c$), when certain conditions are met, the engine triggers the real radiation reaction of order 2.5PN (Post-Newtonian, the perturbative order at which the loss of energy due to gravitational waves appears) and gives rise to emergent behaviors qualitatively consistent with real relativity (causal aberration, contraction of the Liénard-Wiechert-like field for sources close to $c$, i.e., the same deformation that the field of an electric charge in rapid motion undergoes, chirps and analog gravitational waves measured by a virtual LIGO probe), offering an educational laboratory for exploring and comparing classical mechanics and finite-delay causal dynamics.
The balance between physical fidelity and graphic fluidity is largely in the hands of the user: the engine offers a balance but allows the time step, calculation speed, and heatmap resolution to be adjusted in real time.
These dynamic behaviors are not explicitly programmed, but emerge naturally from the dynamics and dead reckoning (the extrapolation of position from known position and velocity) of the causal gravitational interaction:
The thumbnails are clickable and open the folder with all available media. Each phenomenon listed here is discussed in depth, including equations and validation graphs, in the Physics and Scenario Guide.
| Causal propagation visible to the naked eye: By instantly destroying or creating a body, gravitational information propagates visually at the speed of light. Distant bodies continue to "feel" the destroyed body until the absence front reaches them (and, conversely, a new body remains invisible to distant bodies until the causal front arrives). | ![]() |
| Visual analogy of gravitational waves: In the dΦ/dt heatmap, compact binaries in the inspiral phase (the progressive spiral approach before the merger) produce concentric wavefronts with increasing frequency and amplitude, in perfect visual analogy with the real gravitational waves emitted by the mergers. | ![]() |
| Lagrange points L1–L5: They emerge in the Earth-Moon system and can be dynamically visualized in the Lagrange Hunter. | ![]() |
| Breathing of the Roche Lobe: The periodic expansion and contraction of the lunar Roche Lobe in phase with its orbital eccentricity (the lobe expands at apogee and contracts at perigee). | ![]() |
| Liénard-Wiechert distortion: The causal geometric contraction of the isolines of the potential $\Phi$ transversely to the direction of motion for high-speed sources. | ![]() |
| Gravitational Chirp: The progressive increase in frequency and amplitude of the potential wavefronts emitted by compact binaries in inspiral driven by quadrupole dissipation. | ![]() |
| Parameter-free BNS validation on Peters' formula: in the GW170817 scenario (neutron star binary), the chirp mass estimated by the spectral pipeline of the virtual LIGO probe matches Peters' analytical formula with an error of only 0.97%, in parameter-free mode (only equations from first principles, no calibration coefficient). Details, graphs, and limits in §6.6.1 of the physics guide. | ![]() |
| Emergence of the ISCO and the plunge: In the BBH scenario (black hole binary, GW150914), the orbital separation decays until the masses reach the ISCO threshold, the Innermost Stable Circular Orbit, the last stable circular orbit below which every trajectory plunges (theoretical frequency of 62.06 Hz). At this point, the system spontaneously triggers the rapid spiral fall (plunge) at a frequency of 62.40 Hz without any forcing in the code. | ![]() |
| Parameter-free BBH validation on NR SXS: in the GW150914 scenario (black hole binary), the simulated chirp trace follows the reference numerical relativity curve (SXS:BBH:0305) with an average error of 1.27% throughout the inspiral, compared to 7.47% for Peters' analytical formula at the dominant order (details and graphs in §6.6.2 of the physics guide). | ![]() |
| The single-body dipole in dΦ/dt: the time derivative of the potential of a body in motion alone produces a dipole front, blue in front and red behind, the base on which the spirals of the binary pair are then grafted. | ![]() |
| The passage to perihelion in EMRI: in the early stages of the inspiral, each pericenter releases an isolated strain pulse that propagates as a concentric shell at speed $c$, separated from the next one by large regions of silence. | ![]() |
| Apsidal precession in a strong field: in compact orbits (e.g., EMRI), the orbit precesses in a rosette shape not due to a dedicated routine, but due to the Paczyński-Wiita correction at the pericenter, reinforced by the dead reckoning residue beyond the 2nd order. | ![]() |
How to read the table: DT is the time step, i.e., how much simulated time advances with each tick (smaller = more precise and "slower" in real time). The Causal Radius ($D_{max}$) is the distance within which the forces travel at finite speed $c$ by querying the history buffers; beyond that radius, the interaction returns to instantaneous Newtonian. Unit: $1\text{ AU}$ (Astronomical Unit) $= 149,597,870.7\text{ km}$, the average Earth-Sun distance.
| Scenario | Bodies | DT | Causal Radius | Description |
|---|---|---|---|---|
| Complete Solar System | 36 | 150 s | 64 AU | Sun, 8 planets, Pluto and 26 main moons |
| Solar System (Light) | 10 | 512 s | 64 AU | Only the Sun and 9 planets, without moons: higher DT without losing Keplerian fidelity, outer orbits observable in a reasonable time |
| Galactic Orbit (Sgr A*) | 11 | 512 s | 64 AU | Solar System orbiting at 230 km/s around Sagittarius A* |
| Chaotic Cluster | 100 | 64 s | 64 AU | N-body stress test with central BH of 1000 M☉ |
| Earth - Moon - ISS - Hubble | 4 | 1 s | 1 AU | Geocentric regime with ISS and Hubble in LEO orbit |
| Sun - Earth - Moon - Artemis II | 4 | 0.16 s | 1 AU | Passive translunar cruise of Orion on real JPL Horizons vectors, up to the free-return flyby |
| Complete Jovian System | 14 | 60 s | 1 AU | Jupiter and 13 moons (inner, Galilean, irregular) |
| Approach to c (0.999c) | 1 | 0.16 s | 320 LY (~20M AU) | Sun at 0.999c: Liénard-Wiechert distortion (20 GB RAM) |
| Approach to c (0.9c) | 1 | 1.6 s | 1742 LY (~110M AU) | Light version (10 GB RAM) |
| Approach to c (0.7c) | 1 | 16 s | 8710 LY (~550M AU) | Ultra-light version (5 GB RAM) |
| NS Binary: Stable Orbit | 2 | 1 ms | 640 AU | Two neutron stars ~1.5 M☉ at 40,000 km |
| NS Binary: Extreme Eccentricity | 2 | 1 μs | 3 AU | Highly eccentric twin orbits (apocenter 4000 km, pericenter 200 km) |
| NS Binary: Pre-Collision | 2 | 1 μs | 2 AU | Late inspiral, merger in ~59.7 s simulated |
| GW170817 | 2 | 1 μs | 3 AU | Replica of the first multi-messenger event (merger in ~13.9 s simulated) |
| GW150914 | 2 | 1 μs | 3 AU | First GW event detected by LIGO (merger in 52.034 s simulated, theoretically initialized at T-60s) |
| GW190814 | 2 | 1 μs | 3 AU | The most asymmetrical merger (q = 0.112): 23 M☉ BH and 2.6 M☉ mass gap object (initialized at T-20s via Peters) |
| Alpha Centauri + Polyphemus | 9 | 150 s | 32 AU | Real triple system + fictional system from Avatar |
| Extreme Orbits Laboratory | 6 | 0.2 s | 2 AU | Central BH + 5 test particles (e=0 → hyperbolic) |
| EMRI: Relativistic Plunge | 2 | 0.05 s | 1200 AU | Extreme Mass Ratio Inspiral: a light black hole spirals into a much more massive one (ratio 1:100) |
| Collision between Dwarf Galaxies | 202 | 150 s | 64 AU | Near-frontal collision of two 100-star galaxies |
| Empty Scenario | 0 | 1 s | From astro_settings.ini | Empty universe for free construction (can be set via .ini file) |
| Key | Action |
|---|---|
Mouse drag | Camera pan |
Mouse wheel | Zoom in/out |
WASD / Directional arrows | Camera pan (continuous movement of the view) |
Double click on body | Lock camera on selected body |
Double click on empty space | Field probe at cursor point (Φ, dΦ/dt, Tidal) |
TAB | Cycle between active bodies |
| Key | Action |
|---|---|
SPACE | Pause / Resume |
1-5 | TPS multiplier: 1×, 10×, 100×, 1000×, 10000× physics steps per frame, adjusts simulation speed without affecting model accuracy |
T | Halves the DT: more precise, halves the simulation speed, more RAM used |
Y | Doubles the DT: less precise, doubles the simulation speed, less RAM used |
C | Switch Newtonian ↔ Causal (complete reconstruction) |
BACKSPACE | Close and return to the launcher |
| Key | Action |
|---|---|
H | Cycle heatmap mode: OFF → Φ Scalar [Causal] → dΦ/dt [Causal] → Tidal Stress [Newtonian] → OFF |
L | Cycle pair heatmaps: Lagrange Hunter → Roche Topology [Newtonian] → GW Strain [Causal] → Φ (requires body with lock and dominant attractor) |
R | Show/hide orbital trails |
G | Cycle heatmap resolution: AUTO → 1/1 → 1/2 → 1/4 → ... → AUTO |
M | Toggle legend (in Tidal) or theoretical Lagrange markers (in Lagrange Hunter) or ideal circular orbit (in Roche Topology) |
F | Key legend (overlay) |
| Key | Action |
|---|---|
P | Place/remove LIGO probe at cursor position |
N | Open the Orbital Spawner at the cursor position |
K | Request the destruction of the body with lock (confirm Y/N) |
numpy
pygame-ce
numba
matplotlib
scipy
# Clone the repository
git clone https://github.com/alessandro-pioli/AstroCausal_Engine.git
cd AstroCausal_Engine
# Install the dependencies
pip install -r requirements.txt
# Start the launcher
python launcher.py
Note: On the first startup, Numba compiles the physics and graphics kernels and caches them to disk (therefore, only the first time). Compilation is fast, but it can produce brief stutters the first time you activate a function that has not yet been compiled during use (for example, during the first cycle between heatmaps). This is normal and disappears immediately afterwards.
[Causal] (H key)Color map of the gravitational potential, calculated from the historical (causal) positions of the bodies. For a single moving body, it shows the potential well that accompanies it; for bodies in rapid uniform rectilinear motion, the Liénard-Wiechert denominator deforms and compresses the isolines transversely to the direction of motion (analogous to the distortion of the electric field of a moving charge). The classic red-blue "dipole" does not belong to this map, but to the dΦ/dt variation described below.
[Causal] (H key × 2)Represents the temporal variation of the scalar gravitational potential, calculated from the historical (causal) positions. For a moving body sufficiently distant from the others, the characteristic dipole appears: a leading blue front (where the potential deepens as the body approaches) and a trailing red front (where it relaxes). In compact binary mergers, the fronts become concentric and increase in frequency and amplitude: the visual scalar analogue of gravitational waves.
[-4, 2]): increases or decreases the visual intensity. The higher it is, the easier it is to see the fronts of the dipoles merge and blend with those emitted by the more massive distant bodies; the lower it is, the more the nearby fronts are isolated, preventing the screen from being overwhelmed.[Newtonian] (H key × 3)Map of the deviatoric norm of the Hessian matrix of the Newtonian gravitational potential, calculated from the instantaneous positions. The components of the Hessian $\partial^2 \Phi / \partial x_i \partial x_j$ are calculated analytically for each body:
$$H_{ij} = G \cdot m \left(\frac{\delta_{ij}}{r^3} - \frac{3 x_i x_j}{r^5}\right)$$
The stress displayed is the difference between the two eigenvalues of the Hessian, $\sqrt{(\Phi_{xx} - \Phi_{yy})^2 + 4\Phi_{xy}^2}$ (proportional to the deviatoric part of the tensor): it measures the maximum shear stress, i.e., how much a body would be stretched in one direction and compressed in the orthogonal direction. It highlights areas of extreme tidal stress (for example, the orbit of Io around Jupiter). The coloring is on fixed physical scales: from blue (safe region) to red (structural disintegration) to white (near the singularity). The M key shows the legend with the thresholds, so you can see at a glance at what stress a body would be disintegrated.
[Newtonian] (L key with selected body)Co-rotating 2-body frame (selected body + dominant attractor) for identifying orbital equilibria. This mode does not render a continuous heatmap of the field, but identifies and highlights the L1–L5 Lagrange points as discrete bright points on a completely black background. The kernel calculates the gradient and the Hessian of the potential and uses a distance estimator based on the Newton-Raphson method in 2D ($r_{est} = |H^{-1} \nabla \Phi|$) to draw shaded "blobs" at the gradient zeros. The points are classified topologically using the Hessian: the unstable saddle points (L1, L2, L3) appear as red points, while the stable maxima of the co-rotating potential (L4, L5) appear as blue points. The local minima of the potential (gravitational wells at the center of the bodies, with $D > 0$ and $\text{Tr}(H) > 0$) are excluded from the filter on the Hessian trace, preventing the appearance of false blue blobs superimposed on the bodies. Pressing M overlays the analytical theoretical markers for an immediate comparison with the points that emerge numerically from the calculation.
[-8, 8], default 0.0): Adjusts the size of the displayed Lagrange points. By reducing the sensitivity, the points shrink to indicate the exact equilibrium coordinate precisely; by increasing it, the points expand to show the surrounding area of gravitational attraction. Thanks to automatic calibration, the default value of 0.0 clearly shows the points for any system (from Saturn to binary black holes).[Newtonian] (L key × 2)Map of the effective potential in the co-rotating frame of the selected pair (body + dominant attractor). The angular velocity $\omega$ is kinematically derived from the instantaneous specific angular momentum of the pair ($h = \vec{r} \times \vec{v}_{rel}$, $\omega = h / r^2$): the frame rotates like a rigid disk (each point co-rotates at the same $\omega$, with linear velocity $v = \omega r$). The effective potential adds the complete N-body gravity and the centrifugal term, net of the free-fall drag due to third bodies.
The map encodes two independent pieces of information, to be read separately:
The Roche lobe does not coincide with the boundary between red and blue (that is the line where the curvature $D$ changes sign, a different geometric locus): it is the equipotential of $\Phi_{eff}$ that passes through L1, and it can be visually read from the dark channels around the bodies. The low-force "eight" figure that closes right on the L1 saddle marks the maximum volume that a body can occupy before its matter overflows (Roche Lobe Overflow). The more extreme the mass ratio, the more the secondary's lobe shrinks into an elongated "drop" along the tidal axis.
[-8, 8]): raises or lowers the overall brightness to bring out the faintest details or darken the background.[0, 100]): controls the sharpness of the brightness transition; increasing it thins the dark channels around L1 and makes them sharper, making it easier to identify the overflow point.[Causal] (L key × 3)The most sophisticated visualization of the dynamic field: it maps the projected quadrupole gravitational strain of the selected pair. For each pixel, the kernel reads the position and velocity of each body at the retarded time of that pixel (double causal retrieval on the history buffers), subtracts the motion of the center of mass, and projects the retarded velocity along the pixel-source direction: the quadratic difference between the radial and tangential components reproduces the exact quadrupole angular symmetry ($\ell=2$) of the real gravitational radiation, with the characteristic four alternating cyan/red lobes and, for compact binaries in inspiral, the radiative macro-spirals propagating outward at speed $c$. It is the spatial counterpart of the point LIGO probe (same physics, same kinetic regularization). The complete mathematics, the effects of per-body causality, and the post-merger artifacts are documented in §7.6 of the physics guide.
The model is 2+1D with absolute time: all the dynamics exist in the two dimensions of the plane (a slice of the 3D universe, with the $1/r^2$ laws of three-dimensional physics) while a single universal clock marks the time for each body. It is the point of view of a distant observer, without time dilation or curved metric. Relativity enters from the side of the interactions, with causality at finite speed $c$ and the corrections described below (full framework in §1 of the physics guide).
The interaction between each pair of bodies follows Newton's law of universal gravitation:
F = G · M · m / r²
with the crucial difference that the position, velocity, and mass of the source body are taken from the history buffer at the retarded time $t_{ret} = t - r/c$, where $r$ is the distance and $c$ is the speed of light.
For sources in relativistic motion, the gravitational potential is corrected by inserting the classical Liénard-Wiechert denominator $(dist - \vec{v} \cdot \vec{r}/c)$ to describe the field contraction, concentrating the gravitational force orthogonally to the direction of motion (further details in §5 of the physics guide).
[!NOTE]
How the simulator avoids aberration instability
In discrete causal gravity, the aberration of force (due to the fact that gravity points towards the retarded position) introduces a fictitious torque that tends to rapidly widen the celestial orbits. To mitigate this numerical instability and preserve long-term Keplerian stability, the engine implements a Hybrid Dead Reckoning (the technique, borrowed from navigation, of estimating where a body is now based on where it was and its velocity) at the JIT kernel level:
- Quadratic Dead Reckoning (2nd order Taylor): for stable orbits and ordinary velocities, the position of the source is extrapolated by integrating historical velocity and acceleration at the instant of emission: $$\vec{x}_ {eff} = \vec{x}_ {ret} + \vec{v}_ {ret} \Delta t_ {flight} + \frac{1}{2}\vec{a}_ {ret} \Delta t_ {flight}^2$$
- Dead Reckoning bypass in the GW Regime: in an extreme relativistic regime (close to the merger, with relative velocity of the pair greater than 10% of $c$ and distance less than $1000 \cdot R_s$; for equal masses the criterion is equivalent to 5% of $c$ for a single body, but it also remains valid for asymmetric pairs where the heavy body moves slowly), the engine disables linear extrapolation and uses the exact present position of the source for both direction and distance in the calculation of forces. This fundamentally eliminates the accumulation of periodic radial error $O((v/c)^2)$ responsible for orbital instability.
In compact binary mergers, the orbit decays due to the emission of gravitational waves. The engine implements first-order non-conservative dissipative acceleration (2.5PN-order radiation reaction) according to the Damour-Deruelle real relativistic formulation for the relative acceleration $\vec{a}_{rel}$ (theoretical context and history of implementation in §6.2-6.5 of the physics guide):
$$\vec{a}_{rel} = \frac{8}{5}\frac{G^2 M \mu}{c^5 r^3}\Big[\dot{r}\big(18v^2 + \tfrac{2}{3}\tfrac{GM}{r} - 25\dot{r}^2\big)\hat{n} - \big(6v^2 - 2\tfrac{GM}{r} - 15\dot{r}^2\big)\vec{v}\Big] $$
where $M$ is the total mass of the pair, $\mu$ is the reduced mass, $\hat{n}$ is the separation unit vector, and $\vec{v}$ is the relative velocity. This acceleration is calculated and applied to each body based on its reciprocal mass contribution ($m_{src}/M$), ensuring the conservation of the overall linear momentum. The calculation operates in parameter-free mode, delegating the evolution of the orbit solely to the theoretical expression of order $2.5\text{PN}$.
To ensure the conservation of orbital energy and the long-term stability of complex gravitational systems, the engine adopts a Velocity Verlet integration scheme (implemented in the Numba JIT kernels in kernel_single.py, kernel_double.py, and kernel_triple.py; the analysis of the truncation error is in §4 of the physics guide). Each physics integration step follows this precise time sequence:
The DT parameter ($\Delta t$) is the fundamental constant that governs the temporal discretization of the model. Its choice is the most determining factor in the balance between physical accuracy, sampling capacity, and system resources, due to three competing dynamics:
As the time step of the Velocity Verlet algorithm, $\Delta t$ defines the local truncation error of the trajectory ($O(\Delta t^4)$ for the positions).
Since gravitational forces propagate at the finite speed $c$, each body must calculate the interactions by going up its light cone to the maximum flight time: $$t_{flight_max} = \frac{D_{max}}{c}$$ where $D_{max}$ is the maximum operational causal distance set for the scenario. The logical depth of the memory ring buffers for each body must cover at least $t_{flight_max}$. The number of elements $N_{elements}$ to be allocated for each buffer of each body therefore scales as: $$N_{elements} = \frac{t_{flight_max}}{\Delta t} \propto O\left(\frac{1}{\Delta t}\right)$$
This relationship shows how the RAM requirement is inversely proportional to $\Delta t$. However, the constraint is managed upstream: each preset chooses its own causal cone radius ($D_{max}$), and the SimulationManager reads it, sizing the buffers accordingly to optimize memory. For this reason, the predefined scenarios have "ideal" values of $D_{max}$, chosen on a case-by-case basis:
SimulationManager sets the maximum causal radius $D_{max}$ to only 3 AU (Astronomical Units), a small distance but amply sufficient to describe the entire final inspiral and coalescence phase of the pair.With the same hardware performance (TPS - Ticks Per Second), a smaller $\Delta t$ slows the progression of the simulated real time with respect to the user's real clock time. The engine compensates for this effect by multiplying the calculations per frame (via the in-game speed multiplier 1-5), but at the cost of an additional linear calculation load on the CPU.
The Simulation Radius (or Sim Radius) defines the maximum extent of the causal interaction. It works as a sort of "radar" or causal horizon centered on each celestial body:
For an ideal simulation, the simulation radius must be set to a value large enough to allow each body to easily reach any other active coordinate in the scenario. This causes the causal horizons to overlap entirely, ensuring reciprocal and consistent causality throughout the simulation.
AstroCausal_Engine/
├── launcher.py # Tkinter launch GUI (preset, DT, resolution)
├── main_gui.py # Main Pygame loop (events, physics, rendering)
├── ligo_analyzer.py # LIGO post-processing (spectrograms, chirp mass)
├── astro_settings.ini # User configuration file (editable)
├── config.py # Internal settings loader (do not modify)
├── core/
│ ├── data.py # Global state: NumPy arrays, physical constants
│ ├── engine.py # Physics engine (JIT kernel orchestrator)
│ ├── bodies.py # CelestialBody class
│ ├── presets.py # Scenario definitions (Solar System, GW, etc.)
│ ├── simulation_manager.py # Dynamic reconstruction (rebuild, alloc, snapshot)
│ ├── space_probe.py # LIGO probe controller
│ ├── global_state.py # UI/simulation state (pause, view mode, etc.)
│ ├── event_handler.py # Pygame event dispatcher
│ └── jit_kernels/ # Numba JIT kernels
│ ├── kernel_single.py # Integration with single buffer (L0)
│ ├── kernel_double.py # Integration with double buffer (L0 + L1)
│ ├── kernel_triple.py # Integration with triple buffer (L0 + L1 + L2)
│ ├── graphics_kernel.py # Field rendering (Φ, dΦ, Roche, Tidal)
│ └── kernel_helper_inline.py # Core of the computation: causal forces, dead reckoning, collisions, LIGO probe
├── ui/
│ ├── camera.py # 2D camera (pan, zoom, lock)
│ ├── gravity_renderer.py # GPU-like heatmap renderer on CPU
│ ├── master_renderer.py # Final layer composition
│ ├── overlay_renderer.py # HUD, telemetry, legends
│ ├── input_controller.py # Input → action mapping
│ ├── orbital_spawner.py # Interactive spawner with Lagrange points
│ ├── hud_components.py # Vertical faders for sensitivity
│ ├── game_console.py # In-game console with timestamped logs
│ └── tutorial_popup.py # Tutorial popup system
└── utils/
├── loading_splash.py # Loading splash screen
├── formatting.py # Unit formatting (km, AU, dt)
├── performance_manager.py # Heatmap resolution auto-tuner
├── event_logger.py # Impact and death tracker
└── gc_worker.py # Asynchronous garbage collector
launcher.py ──(subprocess)──► main_gui.py
│
├─ show_splash_and_load()
│ ├─ presets.get_preset()
│ └─ rebuild_simulation() ← allocates buffers, computes memory
│
├─ Engine(bodies) ← compiles JIT kernels
│
└─ MAIN LOOP
├─ EventHandler.handle_events()
├─ Engine.tick(speed_mult)
│ └─ kernel_single / kernel_double / kernel_triple
└─ MasterRenderer.render_all()
└─ GravityRenderer → graphics_kernel
For the full account of the engineering choices (DOD, branchless dispatch, asynchronous GC, adaptive LOD buffers), see the document ARCHITECTURE_DEEP_DIVE.md.
AstroCausal Engine is an educational and numerical exploration tool. It has the following physics limitations with respect to formal general relativity:
Running an interactive physics simulation at 60 FPS (the frames generated per second) means having a maximum budget of 16.6 ms per frame. In this time interval, the CPU must sequentially run both the physics engine and the rendering of the gravitational field heatmaps in the background.
The real load is largely in the hands of the user. The engine offers a balance but allows it to be adjusted in real time: you can deliberately throttle either the graphics side (high resolution with many bodies in the scene) or the physics side (many bodies with a high multiplier).
The two extremes: with more than twenty bodies at multiplier 5 (10,000×: the engine aims for 10,000 physics ticks per frame), it is normal to drop below 10 FPS. It is a user choice, because it is needed when you want the maximum advancement of the simulated time (more TPS, at a potentially massive cost of FPS). On the other hand, in compact mergers at $\Delta t = 1 \mu\text{s}$, few bodies are needed: there, the engine can handle 600,000 TPS and 60 FPS stably even with the dΦ/dt heatmap at native 2K resolution, and the high multipliers run without bottlenecks. In fact, several presets use $\Delta t = 1 \mu\text{s}$ with the expected event at 15 simulated seconds or more: at 600,000 TPS, the simulation runs at about 0.6 simulated seconds for each real second, enough to quickly reach the vicinity of the event, before lowering the multiplier and slowing down by orders of magnitude until the individual microseconds can be observed in super slow-motion. In short, the engine finds the balance but it remains interactive: the strategies below are used to consciously govern it, not to survive everyday use.
The asymptotic analysis of the cost per frame (physics versus rendering per pixel, with the worst-case formula) is documented in §2.3 and §3 of ARCHITECTURE_DEEP_DIVE.md. For practical purposes, one relationship is sufficient: the TPS (Ticks Per Second), the actual physics advance rate, are the product of the multiplier's ticks per frame (keys 1-5: 1, 10, 100, 1000, or 10000) times the real FPS. The engine adopts a predefined target of 60 FPS (which can be unlocked or modified in the .ini file): at 60 FPS with a multiplier of 5, the theoretical ceiling is 600,000 TPS (as in the presets of compact 2-body mergers). In very crowded scenarios (e.g., galactic clashes with ~200 bodies), the weight of the physics lowers the real FPS, proportionally dragging the TPS down as well.
Concrete example (complete Solar System, 36 bodies, on the reference hardware). Physics alone supports a ceiling of about 75,000 TPS. Since the multiplier does not require TPS but ticks per frame, the effect is as follows:
5 (10,000 ticks/frame): to stay at 60 FPS, 600,000 TPS would be needed, far beyond the limit. The engine still executes the 10,000 ticks required per frame, so the FPS drops to $75{,}000 / 10{,}000 = 7.5$ FPS.4 (1,000 ticks/frame): $1,000 \times 60 = 60,000$ TPS is enough for 60 FPS, below the ceiling. The scenario returns to being fluid at 60 FPS.The rule of thumb is $\text{FPS} = \min(60,\ \text{TPS ceiling} / \text{ticks per frame})$: when a scenario is populated, simply lower the multiplier to bring the FPS back to the maximum, choosing each time how much simulated time to sacrifice for smoothness.
There is also a lever orthogonal to the multiplier: if you need to accelerate the simulated time without losing FPS, just double the DT (Y key). Each tick advances twice the simulated time with the same calculation cost, so the speed of time doubles without sacrificing a single frame. Here the price is not FPS but numerical precision, so it is only worthwhile where the physical context allows it (stable orbits, far from relativistic regimes that require a microscopic DT).
The pixel-by-pixel calculation of heatmaps is the dominant graphics load on the CPU. To contain it, the engine uses three levers:
Summary: how to recover FPS and speed up the simulation
| Action | Effect | Cost |
|---|---|---|
Lower the speed multiplier (keys 1–5) | fewer physics ticks per frame, therefore FPS recovered (raising it towards 5 can cause them to drop, but not always) | less simulated time per frame |
Double the DT time step (Y key) | more simulated time speed with the same FPS, and even less RAM | only numerical precision (where the physical context allows it) |
Reduce the resolution of the heatmap (G key, cycles through the scale factors) | lighter field rendering, FPS recovered | visual detail of the heatmap |
Turn off the heatmap (H key, cycle through the modes until OFF) | resets the graphics cost: the entire frame budget goes to physics, maximum FPS gain | no heatmap on screen |
Causal propagation requires access to past states that are arbitrarily deep in time. To avoid RAM explosion and protect the CPU cache, the history is a ring buffer system with three hierarchical levels of resolution: L0 samples every single tick for close interactions, L1 and L2 sample the remote past at gradually lower resolution. Not all levels are always allocated: at each startup, the engine chooses the combination (only L0, L0+L1, or all three) by comparing the estimated footprint with the L3 cache of the CPU detected on the machine, so the same simulation can allocate different buffers on different PCs. If the requested memory exceeds the available memory, an OOM (Out Of Memory) protection intercepts the error and displays it with a graphic dialog instead of closing the program.
The fine mechanics of the buffers are fully documented in §2 of ARCHITECTURE_DEEP_DIVE.md, where you can find: the 3D data structure [body, slot, 5 parameters] with dimensions to the power of 2 and indices managed via AND bitmask (to eliminate the cost of modulo division in hot loops); the sampling strides of L1 (32 ticks) and L2 (256 ticks) with the mode selection criteria; the ultra-ECO placeholder allocation with OOM protection; the L0 → L1 → L2 causal cascade query with the complete Earth-Sun numerical example; the double causal retrieval (two cascade readings to solve the implicit time-of-flight equation); and the reconstruction of historical accelerations by finite differences (the buffers do not store them, to save memory).
The LIGO analyzer is an independent pipeline accessible from the launcher, intended for the spectral post-processing of the .npy binary dumps generated by the simulation probe.
During a simulation, the probe is placed with the P key on a point in space (ideally near a binary in inspiral; the system suggests when and where through the RADAR alerts). From that moment, at each tick the probe accumulates in a ring buffer the strain $(v_x^2 - v_y^2)\cdot m/r$ relative to the center of mass, always reading the L0 buffer at high resolution. The signal is saved as .npy in ligo_output/ upon exiting the simulation (or when changing DT), ready to be loaded into the analyzer.
$$f(\tau) = \frac{1}{\pi} \left(\frac{5}{256}\right)^{3/8} \left(\frac{c^3}{G M_{chirp}}\right)^{5/8} \tau^{-3/8}$$
$$M_{chirp} = \frac{c^3}{G} \left[\frac{5}{96 \pi^{8/3}} \frac{\dot{f}}{f^{11/3}} \right] ^{3/5}$$
where $f$ is the instantaneous frequency detected and $\dot{f} = df/dt$ is its time derivative.
[!WARNING]
Validation Disclaimer
The author is neither a physicist nor a mathematician by trade. The engine, its architecture, the numerical choices and the validation work are the author's own. The simulator explicitly computes a small set of standard formulas (retarded-time gravity, Velocity Verlet, Liénard-Wiechert, Paczyński-Wiita, Damour-Deruelle $2.5\text{PN}$ reaction), taken from standard references; no free calibration coefficients remain and the engine runs parameter-free. Language models were used as writing assistance for the documentation, and for a few implementation details that are flagged in the relevant sections. The formal treatment would still benefit a great deal from a look by professionals in the field, an aspect already outlined in the Roadmap below.
float64) on the CPU to avoid drift and numerical errors. Relevant side effect: once the rendering kernels (currently parallelized with prange on the width axis, see core/jit_kernels/graphics_kernel.py) leave the CPU, the freed cores return milliseconds per tick to the physics loop O(N²), opening up a currently non-existent calculation budget that could allow the calculation of conservative post-Newtonian terms (1PN, 2PN).jinja2 or string.Template) that produces the specific single/double/triple and parallel/sequential files from a single abstract kernel, with the buffer constants statically expanded. It would keep the loop hot without if (the template pastes the right code for each variant before compilation), eliminating manual duplication of the scaffolding.This project is distributed under the GNU GPL v3.0 license. In summary: anyone can study, use, and modify the code, but any redistribution, even modified, must remain open source under the same license and acknowledge the origin. The full text is in the LICENSE file.
Copyright © 2026 Alessandro Pioli
Developed by Alessandro Pioli. Independent project for the simulation and visualization of emergent causal gravitational physics.
45 commits
Python
100.0%