A chaotic double pendulum from nested VizGroups¶
Keywords: animation · double pendulum · VizGroup · chaos
Builds a two-link double pendulum out of nested scene-graph groups, exactly
like nested_groups.py::
arm1 (top-level group — pivot at the world origin, the "ceiling")
├── rod1 segment hanging down from the pivot
└── arm2 (group *nested inside* arm1, offset to the end of rod1)
├── rod2 segment hanging down from the end of rod1
└── bob point mass at the end of rod2
Each link rotates about its own pivot:
arm1rotatesrod1(and everything nested under it) about the ceiling pivot by the absolute angletheta1.arm2rotatesrod2about the joint at the end of rod1 by the relative angletheta2 - theta1, so rod2's absolute angle istheta2.
The pendulum state (theta1, theta2 and their angular velocities) is
integrated each frame with the standard double-pendulum equations of motion;
only the two group transforms are re-serialized, so no rod geometry is ever
recomputed or re-sent.
Each rod's label is anchored to the center of the line via
LabelStyle(along=0.5) — along is the fraction along the line's extent
(0 = start, 0.5 = midpoint, 1 = end).
Run¶
Source¶
viz/animation/double_pendulum.py
Code¶
# SPDX-License-Identifier: Apache-2.0
# Copyright 2021 Christian Perwass
"""double_pendulum.py — A chaotic double pendulum from nested ``VizGroup``s.
Builds a two-link double pendulum out of *nested* scene-graph groups, exactly
like ``nested_groups.py``::
arm1 (top-level group — pivot at the world origin, the "ceiling")
├── rod1 segment hanging down from the pivot
└── arm2 (group *nested inside* arm1, offset to the end of rod1)
├── rod2 segment hanging down from the end of rod1
└── bob point mass at the end of rod2
Each link rotates about its *own* pivot:
* ``arm1`` rotates ``rod1`` (and everything nested under it) about the ceiling
pivot by the absolute angle ``theta1``.
* ``arm2`` rotates ``rod2`` about the joint at the end of rod1 by the relative
angle ``theta2 - theta1``, so rod2's absolute angle is ``theta2``.
The pendulum state (``theta1``, ``theta2`` and their angular velocities) is
integrated each frame with the standard double-pendulum equations of motion;
only the two group transforms are re-serialized, so no rod geometry is ever
recomputed or re-sent.
Each rod's label is anchored to the *center* of the line via
``LabelStyle(along=0.5)`` — ``along`` is the fraction along the line's extent
(``0`` = start, ``0.5`` = midpoint, ``1`` = end).
Run with: uv run python py/examples/viz/animation/double_pendulum.py
Keywords: animation, double pendulum, VizGroup, chaos
"""
import math
from pytanga.geometry import Line, Point
from pytanga.viz import LabelStyle, LineStyle, PointStyle, Visualizer
# ── Pendulum parameters ──────────────────────────────────────
M1 = 1.0 # mass at the end of rod1 (kg)
M2 = 1.0 # mass at the end of rod2 (kg)
L1 = 2.0 # length of rod1 (world units)
L2 = 1.5 # length of rod2 (world units)
G = 9.81 # gravity (world units / s^2)
# ── Integration / animation timing ───────────────────────────
FPS = 50 # animation frame rate (passed to viz.animate)
STEPS = 4 # physics substeps per frame (for stability)
H = 1.0 / (FPS * STEPS) # integration step size (seconds)
DAMPING = 0.0 # per-second angular-velocity damping (0.0 = ideal / chaotic)
# ── Initial state: both rods released from horizontal, pointing at +x ──
THETA1_0 = math.pi / 2
THETA2_0 = math.pi / 2
OMEGA1_0 = 0.0
OMEGA2_0 = 0.0
def _accel(
theta1: float, theta2: float, omega1: float, omega2: float
) -> tuple[float, float]:
"""Angular accelerations of the two links (standard double-pendulum EOM).
``theta1`` / ``theta2`` are measured from the downward vertical (positive
toward +x). Returns ``(alpha1, alpha2)``.
"""
dtheta = theta1 - theta2
sin_d = math.sin(dtheta)
cos_d = math.cos(dtheta)
denom = 2.0 * M1 + M2 - M2 * math.cos(2.0 * dtheta)
alpha1 = (
-G * (2.0 * M1 + M2) * math.sin(theta1)
- M2 * G * math.sin(theta1 - 2.0 * theta2)
- 2.0 * sin_d * M2 * (omega2**2 * L2 + omega1**2 * L1 * cos_d)
) / (L1 * denom)
alpha2 = (
2.0
* sin_d
* (
omega1**2 * L1 * (M1 + M2)
+ G * (M1 + M2) * math.cos(theta1)
+ omega2**2 * L2 * M2 * cos_d
)
) / (L2 * denom)
return alpha1, alpha2
viz = Visualizer(title="Tanga — Double Pendulum")
viz.show()
# ── arm1: pivot at the world origin (the ceiling anchor) ─────
# Both rods are drawn hanging straight *down* (the −y direction); rotation is
# about the z axis, so a positive angle swings the rod toward +x.
arm1 = viz.add_group("arm1")
arm1.new(Point(0, 0, 0), color="#ffaa00", label="pivot1", style=PointStyle(size=0.10))
arm1.new(
Line.from_points(Point(0, 0, 0), Point(0, -L1, 0)),
color="#ff5555",
label="rod1",
label_style=LabelStyle(along=0.5), # anchor label at the line's midpoint
style=LineStyle(thickness=3.0),
)
# ── arm2: nested inside arm1, offset to the end of rod1 ──────
arm2 = arm1.add_group("arm2")
arm2.set_transform(position=(0.0, -L1, 0.0))
arm2.new(Point(0, 0, 0), color="#ffaa00", label="pivot2", style=PointStyle(size=0.09))
arm2.new(
Line.from_points(Point(0, 0, 0), Point(0, -L2, 0)),
color="#5599ff",
label="rod2",
label_style=LabelStyle(along=0.5), # anchor label at the line's midpoint
style=LineStyle(thickness=3.0),
)
arm2.new(Point(0, -L2, 0), color="#44ff44", label="bob", style=PointStyle(size=0.14))
viz.flush()
theta1, theta2 = THETA1_0, THETA2_0
omega1, omega2 = OMEGA1_0, OMEGA2_0
print("Simulating the double pendulum until Ctrl+C...")
for _ in viz.animate(fps=FPS):
for _ in range(STEPS):
alpha1, alpha2 = _accel(theta1, theta2, omega1, omega2)
omega1 += alpha1 * H
omega2 += alpha2 * H
if DAMPING:
omega1 *= 1.0 - DAMPING * H
omega2 *= 1.0 - DAMPING * H
theta1 += omega1 * H
theta2 += omega2 * H
# arm1 rotates rod1 + arm2 + rod2 about the ceiling pivot.
arm1.set_transform(rotation=(0.0, 0.0, theta1))
# arm2 rotates rod2 about the joint by the *relative* angle theta2 - theta1,
# compounding with arm1 so rod2's absolute angle is theta2.
arm2.set_transform(rotation=(0.0, 0.0, theta2 - theta1))
viz.flush()
print("Animation stopped.")