Spaces:
Runtime error
Runtime error
File size: 5,179 Bytes
72ae1e2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | """
pendulum_physics.py -- self-contained nonlinear pendulum physics core for
the interactive simulation. Deliberately has ZERO dependency on the
vision_lab/ package (no OpenCV) so this folder can be deployed alone as a
minimal Hugging Face Space / Gradio app that works for anyone, anywhere,
with no camera or physical hardware at all.
Equation of motion (damped, driven-free nonlinear pendulum):
theta'' + (b/m) * theta' + (g/L) * sin(theta) = 0
Solved with a fixed-step classical RK4 integrator (no SciPy ODE solver
dependency required, though SciPy is used for the exact elliptic-integral
period as an independent cross-check of the numerical integration).
"""
from __future__ import annotations
from dataclasses import dataclass
from math import pi, sin, sqrt
from typing import Optional, Tuple
import numpy as np
from scipy.special import ellipk
def _derivatives(theta: float, omega: float, g: float, L: float, damping: float) -> Tuple[float, float]:
theta_dot = omega
omega_dot = -damping * omega - (g / L) * sin(theta)
return theta_dot, omega_dot
def simulate(
theta0_rad: float,
g: float,
L: float,
damping: float = 0.0,
omega0: float = 0.0,
t_max: float = 10.0,
dt: float = 0.001,
) -> dict:
"""Integrate the exact nonlinear pendulum ODE with RK4.
Returns a dict of numpy arrays: t, theta (rad), omega (rad/s),
theta_small_angle (the SHM analytic comparison at the same theta0),
energy (per unit mass, J/kg).
"""
n = int(t_max / dt) + 1
t = np.linspace(0.0, t_max, n)
theta = np.empty(n)
omega = np.empty(n)
theta[0], omega[0] = theta0_rad, omega0
for i in range(1, n):
th, om = theta[i - 1], omega[i - 1]
k1_th, k1_om = _derivatives(th, om, g, L, damping)
k2_th, k2_om = _derivatives(th + 0.5 * dt * k1_th, om + 0.5 * dt * k1_om, g, L, damping)
k3_th, k3_om = _derivatives(th + 0.5 * dt * k2_th, om + 0.5 * dt * k2_om, g, L, damping)
k4_th, k4_om = _derivatives(th + dt * k3_th, om + dt * k3_om, g, L, damping)
theta[i] = th + (dt / 6.0) * (k1_th + 2 * k2_th + 2 * k3_th + k4_th)
omega[i] = om + (dt / 6.0) * (k1_om + 2 * k2_om + 2 * k3_om + k4_om)
omega_0_shm = sqrt(g / L)
theta_shm = theta0_rad * np.cos(omega_0_shm * t)
# Energy per unit mass: (1/2) L^2 omega^2 + g*L*(1 - cos(theta))
energy = 0.5 * (L ** 2) * omega ** 2 + g * L * (1.0 - np.cos(theta))
return {
"t": t,
"theta": theta,
"omega": omega,
"theta_small_angle": theta_shm,
"energy": energy,
}
def period_small_angle(g: float, L: float) -> float:
return 2.0 * pi * sqrt(L / g)
def period_exact(theta0_rad: float, g: float, L: float) -> float:
"""Exact nonlinear pendulum period via the complete elliptic integral
of the first kind: T = 4*sqrt(L/g) * K(sin^2(theta0/2))
(SciPy's ``ellipk(m)`` uses the parameter convention m = k^2.)
"""
m = sin(theta0_rad / 2.0) ** 2
return 4.0 * sqrt(L / g) * ellipk(m)
def period_series_correction(theta0_rad: float) -> float:
"""Fractional correction T_exact/T0 - 1, leading-order series
(matches vision_lab/physics.py::small_angle_period_correction, and
both are cross-checked against each other and against the exact
elliptic-integral result in the test suite)."""
theta = theta0_rad
return (theta ** 2) / 16.0 + (11.0 * theta ** 4) / 3072.0
@dataclass(frozen=True)
class VirtualTrial:
length_m: float
g_true: float
theta0_rad: float
n_oscillations: int
g_measured: float
sigma_g: float
def virtual_experiment(
length_m: float,
g_true: float,
theta0_rad: float,
n_oscillations: int = 20,
timing_noise_s: float = 0.02,
rng: Optional[np.random.Generator] = None,
) -> VirtualTrial:
"""Numerically reproduce the *camera experiment's* measurement method
(time N oscillations, divide, invert g=4pi^2L/T^2) on a numerically
exact nonlinear pendulum, with injected Gaussian timing noise -- i.e.
a digital twin of vision_lab/pendulum_tracker.py that needs no camera.
"""
if rng is None:
rng = np.random.default_rng()
t_true = period_exact(theta0_rad, g_true, length_m)
total_time_true = t_true * n_oscillations
# Two independent noisy timing reads (start & end crossing detection),
# combined -- mirrors the real tracker's start/end crossing timestamps.
noisy_total_time = total_time_true + rng.normal(0.0, timing_noise_s) - rng.normal(0.0, timing_noise_s)
T_measured = noisy_total_time / n_oscillations
g_measured = 4.0 * pi ** 2 * length_m / T_measured ** 2
sigma_T = timing_noise_s * sqrt(2.0) / n_oscillations
sigma_g = g_measured * sqrt((2 * sigma_T / T_measured) ** 2)
return VirtualTrial(
length_m=length_m,
g_true=g_true,
theta0_rad=theta0_rad,
n_oscillations=n_oscillations,
g_measured=g_measured,
sigma_g=sigma_g,
)
PLANETS = {
"Earth / الأرض": 9.80665,
"Moon / القمر": 1.62,
"Mars / المريخ": 3.71,
"Jupiter / المشتري": 24.79,
"Saturn / زحل": 10.44,
}
|