Gravitational two-body simulation using only¶
Keywords: animation · two-body · gravity · Point · Direction · simulation
Point and Direction for vector arithmetic.
Two massive bodies orbit each other under Newtonian gravity.
All calculations use Point and Direction operators
(+, -, *, /) and methods (dot, cross, mag, norm) — no raw
floats for vector components except at initialisation.
Run¶
Source¶
viz/animation/two_body_gravity.py
Code¶
# SPDX-License-Identifier: Apache-2.0
# Copyright 2021 Christian Perwass
"""two_body_gravity.py — Gravitational two-body simulation using only
Point and Direction for vector arithmetic.
Two massive bodies orbit each other under Newtonian gravity.
All calculations use ``Point`` and ``Direction`` operators
(+, -, *, /) and methods (dot, cross, mag, norm) — no raw
floats for vector components except at initialisation.
Run with: uv run python py/examples/viz/animation/two_body_gravity.py
Keywords: animation, two-body, gravity, Point, Direction, simulation
"""
from pytanga.geometry import Direction, Point
from pytanga.viz import PointStyle, Visualizer
# ═══════════════════════════════════════════════════════════════
# Simulation parameters
# ═══════════════════════════════════════════════════════════════
G = 2.0 # gravitational constant (tuned for visual appeal)
DT = 0.02 # physics time step (s) — matches the ~50 FPS animation rate
# Body 1 — heavy, starts near origin
mass_1 = 5.0
pos_1 = Point(2.0, 0.0, 0.0)
vel_1 = Direction(0.0, 0.5, 0.0)
# Body 2 — lighter, opposite side
mass_2 = 1.0
pos_2 = Point(-2.0, 0.0, 0.0)
vel_2 = Direction(0.5, -0.5, 0.0)
# ═══════════════════════════════════════════════════════════════
# Visualisation setup
# ═══════════════════════════════════════════════════════════════
viz = Visualizer(title="Tanga — Two-Body Gravitational Simulation")
viz.show()
# Coordinate axes for reference
viz.new(
Point(0, 0, 0),
label="origin",
style=PointStyle(
size=0.12,
color="#333333",
),
)
# Body entities — `new()` returns a VizObjectRef; update via `.entity`.
body_1 = viz.new(
pos_1,
label=f"$m_1 = {mass_1}$",
style=PointStyle(
size=0.25,
color="#ff4444",
),
)
body_2 = viz.new(
pos_2,
label=f"$m_2 ={mass_2}$",
style=PointStyle(
size=0.18,
color="#4488ff",
),
)
viz.flush()
# ═══════════════════════════════════════════════════════════════
# Simulation loop
# ═══════════════════════════════════════════════════════════════
print("Simulating two-body gravity until Ctrl+C...")
def _as_direction(value: "Direction | Point") -> Direction:
"""Narrow a point/direction expression back to a direction."""
assert isinstance(value, Direction), "expected a direction"
return value
for _ in viz.animate(fps=50):
# ── Gravitational force ──────────────────────────────────
# Vector from body 1 to body 2: r = pos_2 - pos_1
r_12: "Direction | Point" = pos_2 - pos_1
dist_12 = r_12.mag()
# Unit direction from 1 toward 2
r_hat_12 = _as_direction(r_12.normalized())
# Force magnitude: F = G * m1 * m2 / r²
force_mag = G * mass_1 * mass_2 / (dist_12 * dist_12)
# Force on body 1: toward body 2 (+r_hat direction)
f_1 = _as_direction(r_hat_12 * force_mag)
# Force on body 2: toward body 1 (−r_hat direction)
f_2: Direction = -f_1
# ── Update velocities (a = F/m, v += a * dt) ─────────────
acc_1: Direction = f_1 / mass_1
acc_2: Direction = f_2 / mass_2
vel_1 = vel_1 + acc_1 * DT
vel_2 = vel_2 + acc_2 * DT
# ── Update positions (p += v * dt) ───────────────────────
pos_1 = pos_1 + vel_1 * DT
pos_2 = pos_2 + vel_2 * DT
# ── Render ───────────────────────────────────────────────
body_1.entity = pos_1
body_2.entity = pos_2
viz.flush()
print("Simulation stopped.")