Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from numpy.random import normal
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
# ---------------------------
|
| 6 |
+
# Physical constants
|
| 7 |
+
# ---------------------------
|
| 8 |
+
g = 9.81
|
| 9 |
+
rho = 1.2
|
| 10 |
+
Cd = 0.47
|
| 11 |
+
r = 0.02
|
| 12 |
+
A = np.pi * r**2
|
| 13 |
+
m = 0.0027
|
| 14 |
+
|
| 15 |
+
k_drag = 0.5 * rho * Cd * A / m
|
| 16 |
+
k_mag = 4e-4 # Magnus coefficient (tuned)
|
| 17 |
+
|
| 18 |
+
# Table + net
|
| 19 |
+
table_length = 2.74
|
| 20 |
+
x_net = table_length / 2
|
| 21 |
+
h_net = 0.1525
|
| 22 |
+
y_thresh = h_net + r # clearance threshold
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ---------------------------
|
| 26 |
+
# 2D flight ODE
|
| 27 |
+
# ---------------------------
|
| 28 |
+
def f(state, omega):
|
| 29 |
+
x, y, vx, vy = state
|
| 30 |
+
v = np.sqrt(vx**2 + vy**2)
|
| 31 |
+
|
| 32 |
+
ax_drag = -k_drag * v * vx
|
| 33 |
+
ay_drag = -k_drag * v * vy
|
| 34 |
+
|
| 35 |
+
ax_mag = k_mag * omega * vy
|
| 36 |
+
ay_mag = -k_mag * omega * vx
|
| 37 |
+
|
| 38 |
+
return np.array([
|
| 39 |
+
vx,
|
| 40 |
+
vy,
|
| 41 |
+
ax_drag + ax_mag,
|
| 42 |
+
-g + ay_drag + ay_mag
|
| 43 |
+
])
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# ---------------------------
|
| 47 |
+
# Single trajectory (NO BOUNCE)
|
| 48 |
+
# ---------------------------
|
| 49 |
+
def simulate_trajectory(S, theta_deg, omega, dt=0.001, t_max=2.0):
|
| 50 |
+
theta = np.deg2rad(theta_deg)
|
| 51 |
+
|
| 52 |
+
# initial conditions
|
| 53 |
+
vx = S * np.cos(theta)
|
| 54 |
+
vy = S * np.sin(theta)
|
| 55 |
+
x, y = 0.0, 0.3 # contact height ~30 cm
|
| 56 |
+
|
| 57 |
+
prev_x, prev_y = x, y
|
| 58 |
+
cleared_net = False
|
| 59 |
+
hit_net = False
|
| 60 |
+
|
| 61 |
+
for _ in np.arange(0, t_max, dt):
|
| 62 |
+
|
| 63 |
+
# ------------- NET CHECK -------------
|
| 64 |
+
if (prev_x - x_net) * (x - x_net) <= 0 and (x != prev_x):
|
| 65 |
+
tau = (x_net - prev_x) / (x - prev_x)
|
| 66 |
+
y_cross = prev_y + tau * (y - prev_y)
|
| 67 |
+
|
| 68 |
+
if y_cross <= y_thresh:
|
| 69 |
+
return x_net, "net" # hit net
|
| 70 |
+
else:
|
| 71 |
+
cleared_net = True
|
| 72 |
+
|
| 73 |
+
# ------------- GROUND (TABLE) IMPACT -------------
|
| 74 |
+
if y <= 0:
|
| 75 |
+
# Ball lands
|
| 76 |
+
if x < x_net:
|
| 77 |
+
return x, "undershoot" # lands on own side
|
| 78 |
+
elif x <= table_length:
|
| 79 |
+
return x, "valid" # lands on opponent side
|
| 80 |
+
else:
|
| 81 |
+
return x, "overshoot" # lands beyond table
|
| 82 |
+
|
| 83 |
+
# ------------- INTEGRATE (RK4) -------------
|
| 84 |
+
state = np.array([x, y, vx, vy])
|
| 85 |
+
k1 = f(state, omega)
|
| 86 |
+
k2 = f(state + 0.5*dt*k1, omega)
|
| 87 |
+
k3 = f(state + 0.5*dt*k2, omega)
|
| 88 |
+
k4 = f(state + dt*k3, omega)
|
| 89 |
+
state += (dt/6)*(k1 + 2*k2 + 2*k3 + k4)
|
| 90 |
+
|
| 91 |
+
prev_x, prev_y = x, y
|
| 92 |
+
x, y, vx, vy = state
|
| 93 |
+
|
| 94 |
+
# If ball goes far past table without landing → overshoot
|
| 95 |
+
if x > table_length + 0.5:
|
| 96 |
+
return x, "overshoot"
|
| 97 |
+
|
| 98 |
+
return x, "unknown"
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# ---------------------------
|
| 102 |
+
# Monte Carlo
|
| 103 |
+
# ---------------------------
|
| 104 |
+
def monte_carlo(n=2000):
|
| 105 |
+
landings = []
|
| 106 |
+
outcomes = {"valid": 0, "net": 0, "undershoot": 0, "overshoot": 0}
|
| 107 |
+
|
| 108 |
+
for _ in range(n):
|
| 109 |
+
# Random shot parameters
|
| 110 |
+
S = normal(8.0, 0.4)
|
| 111 |
+
ang = normal(12.0, 2.0)
|
| 112 |
+
spin = normal(150, 20)
|
| 113 |
+
|
| 114 |
+
x_land, outcome = simulate_trajectory(S, ang, spin)
|
| 115 |
+
|
| 116 |
+
landings.append(x_land)
|
| 117 |
+
outcomes[outcome] += 1
|
| 118 |
+
|
| 119 |
+
return np.array(landings), outcomes
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# ---------------------------
|
| 123 |
+
# Run + Plot
|
| 124 |
+
# ---------------------------
|
| 125 |
+
landings, outcomes = monte_carlo(2000)
|
| 126 |
+
|
| 127 |
+
print(outcomes)
|
| 128 |
+
print("Valid shot probability:", outcomes["valid"] / 2000)
|
| 129 |
+
|
| 130 |
+
plt.hist(landings, bins=80, density=True)
|
| 131 |
+
plt.axvline(x_net, color='r', linestyle='--', label='Net')
|
| 132 |
+
plt.axvline(table_length, color='k', linestyle='--', label='End of table")
|
| 133 |
+
plt.title("Landing distribution (PDF approx)")
|
| 134 |
+
plt.xlabel("Landing x-position (m)")
|
| 135 |
+
plt.ylabel("Probability density")
|
| 136 |
+
plt.legend()
|
| 137 |
+
plt.show()
|