File size: 3,914 Bytes
bcf0876 a9a30fb bcf0876 7559e31 |
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 157 158 159 |
import numpy as np
from numpy.random import normal
import matplotlib.pyplot as plt
# ---------------------------
# Physical constants
# ---------------------------
g = 9.81
rho = 1.2
Cd = 0.47
r = 0.02
A = np.pi * r**2
m = 0.0027
k_drag = 0.5 * rho * Cd * A / m
k_mag = 4e-4 # Magnus coefficient (tuned)
# Table + net
table_length = 2.74
x_net = table_length / 2
h_net = 0.1525
y_thresh = h_net + r # clearance threshold
# ---------------------------
# 2D flight ODE
# ---------------------------
def f(state, omega):
x, y, vx, vy = state
v = np.sqrt(vx**2 + vy**2)
ax_drag = -k_drag * v * vx
ay_drag = -k_drag * v * vy
ax_mag = k_mag * omega * vy
ay_mag = -k_mag * omega * vx
return np.array([
vx,
vy,
ax_drag + ax_mag,
-g + ay_drag + ay_mag
])
# ---------------------------
# Single trajectory (NO BOUNCE)
# ---------------------------
def simulate_trajectory(S, theta_deg, omega, dt=0.001, t_max=2.0):
theta = np.deg2rad(theta_deg)
# initial conditions
vx = S * np.cos(theta)
vy = S * np.sin(theta)
x, y = 0.0, 0.3 # contact height ~30 cm
prev_x, prev_y = x, y
cleared_net = False
hit_net = False
for _ in np.arange(0, t_max, dt):
# ------------- NET CHECK -------------
if (prev_x - x_net) * (x - x_net) <= 0 and (x != prev_x):
tau = (x_net - prev_x) / (x - prev_x)
y_cross = prev_y + tau * (y - prev_y)
if y_cross <= y_thresh:
return x_net, "net" # hit net
else:
cleared_net = True
# ------------- GROUND (TABLE) IMPACT -------------
if y <= 0:
# Ball lands
if x < x_net:
return x, "undershoot" # lands on own side
elif x <= table_length:
return x, "valid" # lands on opponent side
else:
return x, "overshoot" # lands beyond table
# ------------- INTEGRATE (RK4) -------------
state = np.array([x, y, vx, vy])
k1 = f(state, omega)
k2 = f(state + 0.5*dt*k1, omega)
k3 = f(state + 0.5*dt*k2, omega)
k4 = f(state + dt*k3, omega)
state += (dt/6)*(k1 + 2*k2 + 2*k3 + k4)
prev_x, prev_y = x, y
x, y, vx, vy = state
# If ball goes far past table without landing → overshoot
if x > table_length + 0.5:
return x, "overshoot"
return x, "unknown"
# ---------------------------
# Monte Carlo
# ---------------------------
def monte_carlo(n=2000):
landings = []
outcomes = {"valid": 0, "net": 0, "undershoot": 0, "overshoot": 0}
for _ in range(n):
# Random shot parameters
S = normal(8.0, 0.4)
ang = normal(12.0, 2.0)
spin = normal(150, 20)
x_land, outcome = simulate_trajectory(S, ang, spin)
landings.append(x_land)
outcomes[outcome] += 1
return np.array(landings), outcomes
# ---------------------------
# Run + Plot
# ---------------------------
landings, outcomes = monte_carlo(2000)
print(outcomes)
print("Valid shot probability:", outcomes["valid"] / 2000)
plt.hist(landings, bins=80, density=True)
plt.axvline(x_net, color='r', linestyle='--', label='Net')
plt.axvline(table_length, color='k', linestyle='--', label="End of table")
plt.title("Landing distribution (PDF approx)")
plt.xlabel("Landing x-position (m)")
plt.ylabel("Probability density")
plt.legend()
plt.show()
import gradio as gr
def run_sim():
landings, outcomes = monte_carlo(2000)
result_str = f"""
Valid: {outcomes['valid']}
Net: {outcomes['net']}
Undershoot: {outcomes['undershoot']}
Overshoot: {outcomes['overshoot']}
"""
return result_str
demo = gr.Interface(
fn=run_sim,
inputs=[],
outputs="text",
title="Table Tennis Monte Carlo Simulator"
)
demo.launch() |