dschechter27 commited on
Commit
f123e58
·
verified ·
1 Parent(s): 43a5c55

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +396 -0
app.py ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ from dataclasses import dataclass, field
4
+
5
+ import gradio as gr
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ from matplotlib.animation import FuncAnimation
9
+
10
+
11
+ # -----------------------------
12
+ # Core physics data structures
13
+ # -----------------------------
14
+ @dataclass
15
+ class Body:
16
+ name: str
17
+ mass: float
18
+ position: np.ndarray
19
+ velocity: np.ndarray
20
+ color: str = "white"
21
+ size: float = 8.0
22
+ acceleration: np.ndarray = field(default_factory=lambda: np.zeros(2, dtype=float))
23
+ trail: list = field(default_factory=list)
24
+
25
+ def __post_init__(self):
26
+ self.position = np.array(self.position, dtype=float)
27
+ self.velocity = np.array(self.velocity, dtype=float)
28
+ self.acceleration = np.array(self.acceleration, dtype=float)
29
+
30
+ def record_position(self):
31
+ self.trail.append(self.position.copy())
32
+
33
+
34
+ # -----------------------------
35
+ # Physics engine
36
+ # -----------------------------
37
+ def compute_accelerations(bodies, G=1.0, softening=1e-3):
38
+ n = len(bodies)
39
+ accelerations = [np.zeros(2, dtype=float) for _ in range(n)]
40
+
41
+ for i in range(n):
42
+ for j in range(n):
43
+ if i == j:
44
+ continue
45
+
46
+ displacement = bodies[j].position - bodies[i].position
47
+ distance_sq = np.dot(displacement, displacement) + softening**2
48
+ distance = np.sqrt(distance_sq)
49
+
50
+ accelerations[i] += G * bodies[j].mass * displacement / (distance_sq * distance)
51
+
52
+ return accelerations
53
+
54
+
55
+ def step_velocity_verlet(bodies, dt, G=1.0, softening=1e-3):
56
+ old_accelerations = compute_accelerations(bodies, G=G, softening=softening)
57
+
58
+ for body, acc in zip(bodies, old_accelerations):
59
+ body.acceleration = acc
60
+ body.position = body.position + body.velocity * dt + 0.5 * acc * dt**2
61
+
62
+ new_accelerations = compute_accelerations(bodies, G=G, softening=softening)
63
+
64
+ for body, old_acc, new_acc in zip(bodies, old_accelerations, new_accelerations):
65
+ body.velocity = body.velocity + 0.5 * (old_acc + new_acc) * dt
66
+ body.acceleration = new_acc
67
+ body.record_position()
68
+
69
+
70
+ class SimulationVV:
71
+ def __init__(self, bodies, dt=0.001, G=1.0, softening=1e-3):
72
+ self.bodies = bodies
73
+ self.dt = dt
74
+ self.G = G
75
+ self.softening = softening
76
+ self.time = 0.0
77
+
78
+ for body in self.bodies:
79
+ if len(body.trail) == 0:
80
+ body.record_position()
81
+
82
+ def step(self):
83
+ step_velocity_verlet(
84
+ self.bodies,
85
+ dt=self.dt,
86
+ G=self.G,
87
+ softening=self.softening,
88
+ )
89
+ self.time += self.dt
90
+
91
+
92
+ # -----------------------------
93
+ # Diagnostics
94
+ # -----------------------------
95
+ def compute_kinetic_energy(bodies):
96
+ total_ke = 0.0
97
+ for body in bodies:
98
+ total_ke += 0.5 * body.mass * np.dot(body.velocity, body.velocity)
99
+ return total_ke
100
+
101
+
102
+ def compute_potential_energy(bodies, G=1.0, softening=1e-3):
103
+ total_pe = 0.0
104
+ n = len(bodies)
105
+
106
+ for i in range(n):
107
+ for j in range(i + 1, n):
108
+ displacement = bodies[j].position - bodies[i].position
109
+ distance = np.sqrt(np.dot(displacement, displacement) + softening**2)
110
+ total_pe += -G * bodies[i].mass * bodies[j].mass / distance
111
+
112
+ return total_pe
113
+
114
+
115
+ # -----------------------------
116
+ # Presets
117
+ # -----------------------------
118
+ def make_two_body_system(G=1.0):
119
+ M = 1000.0
120
+ r = 1.0
121
+ v = np.sqrt(G * M / r)
122
+
123
+ sun = Body(
124
+ name="Sun",
125
+ mass=M,
126
+ position=[0.0, 0.0],
127
+ velocity=[0.0, 0.0],
128
+ color="gold",
129
+ size=18,
130
+ )
131
+
132
+ earth = Body(
133
+ name="Earth",
134
+ mass=1.0,
135
+ position=[r, 0.0],
136
+ velocity=[0.0, v],
137
+ color="deepskyblue",
138
+ size=8,
139
+ )
140
+
141
+ return [sun, earth]
142
+
143
+
144
+ def make_three_body_system():
145
+ body1 = Body(
146
+ name="Body 1",
147
+ mass=500.0,
148
+ position=[-0.8, 0.0],
149
+ velocity=[0.0, -10.0],
150
+ color="orange",
151
+ size=14,
152
+ )
153
+
154
+ body2 = Body(
155
+ name="Body 2",
156
+ mass=500.0,
157
+ position=[0.8, 0.0],
158
+ velocity=[0.0, 10.0],
159
+ color="cyan",
160
+ size=14,
161
+ )
162
+
163
+ body3 = Body(
164
+ name="Body 3",
165
+ mass=5.0,
166
+ position=[0.0, 1.2],
167
+ velocity=[18.0, 0.0],
168
+ color="magenta",
169
+ size=8,
170
+ )
171
+
172
+ return [body1, body2, body3]
173
+
174
+
175
+ def make_custom_two_body(central_mass, orbit_radius, planet_mass, planet_speed, tangential_direction):
176
+ sun = Body(
177
+ name="Central Body",
178
+ mass=central_mass,
179
+ position=[0.0, 0.0],
180
+ velocity=[0.0, 0.0],
181
+ color="gold",
182
+ size=18,
183
+ )
184
+
185
+ vy = planet_speed if tangential_direction == "Counterclockwise" else -planet_speed
186
+
187
+ planet = Body(
188
+ name="Planet",
189
+ mass=planet_mass,
190
+ position=[orbit_radius, 0.0],
191
+ velocity=[0.0, vy],
192
+ color="deepskyblue",
193
+ size=8,
194
+ )
195
+
196
+ return [sun, planet]
197
+
198
+
199
+ # -----------------------------
200
+ # Rendering
201
+ # -----------------------------
202
+ def simulate_system(
203
+ preset,
204
+ dt,
205
+ frames,
206
+ steps_per_frame,
207
+ G,
208
+ softening,
209
+ trail_length,
210
+ central_mass,
211
+ orbit_radius,
212
+ planet_mass,
213
+ planet_speed,
214
+ tangential_direction,
215
+ ):
216
+ if preset == "Two-Body Circular Orbit":
217
+ bodies = make_two_body_system(G=G)
218
+ elif preset == "Three-Body Chaotic System":
219
+ bodies = make_three_body_system()
220
+ else:
221
+ bodies = make_custom_two_body(
222
+ central_mass=central_mass,
223
+ orbit_radius=orbit_radius,
224
+ planet_mass=planet_mass,
225
+ planet_speed=planet_speed,
226
+ tangential_direction=tangential_direction,
227
+ )
228
+
229
+ sim = SimulationVV(bodies, dt=dt, G=G, softening=softening)
230
+
231
+ kinetic_history = []
232
+ potential_history = []
233
+ total_history = []
234
+
235
+ fig, (ax_orbit, ax_energy) = plt.subplots(1, 2, figsize=(14, 6))
236
+ fig.patch.set_facecolor("#0f1117")
237
+ ax_orbit.set_facecolor("black")
238
+ ax_energy.set_facecolor("white")
239
+
240
+ max_range = max(2.5, orbit_radius * 1.8)
241
+ ax_orbit.set_xlim(-max_range, max_range)
242
+ ax_orbit.set_ylim(-max_range, max_range)
243
+ ax_orbit.set_aspect("equal")
244
+ ax_orbit.set_title("Orbital Dynamics", color="white", fontsize=14)
245
+ ax_orbit.tick_params(colors="white")
246
+ for spine in ax_orbit.spines.values():
247
+ spine.set_color("white")
248
+ ax_orbit.grid(alpha=0.15, color="white")
249
+
250
+ ax_energy.set_title("Energy Diagnostics", fontsize=14)
251
+ ax_energy.set_xlabel("Frame")
252
+ ax_energy.set_ylabel("Energy")
253
+ ax_energy.grid(alpha=0.3)
254
+
255
+ scatters = []
256
+ trail_lines = []
257
+
258
+ for body in sim.bodies:
259
+ scatter = ax_orbit.scatter([], [], color=body.color, s=body.size * 18, edgecolors="white", linewidths=0.5)
260
+ line, = ax_orbit.plot([], [], color=body.color, alpha=0.7, linewidth=1.5)
261
+ scatters.append(scatter)
262
+ trail_lines.append(line)
263
+
264
+ ke_line, = ax_energy.plot([], [], label="Kinetic Energy", linewidth=2)
265
+ pe_line, = ax_energy.plot([], [], label="Potential Energy", linewidth=2)
266
+ te_line, = ax_energy.plot([], [], label="Total Energy", linewidth=2)
267
+ ax_energy.legend()
268
+
269
+ def init():
270
+ for scatter, line in zip(scatters, trail_lines):
271
+ scatter.set_offsets(np.array([[np.nan, np.nan]]))
272
+ line.set_data([], [])
273
+ ke_line.set_data([], [])
274
+ pe_line.set_data([], [])
275
+ te_line.set_data([], [])
276
+ return scatters + trail_lines + [ke_line, pe_line, te_line]
277
+
278
+ def update(_frame):
279
+ for _ in range(steps_per_frame):
280
+ sim.step()
281
+
282
+ ke = compute_kinetic_energy(sim.bodies)
283
+ pe = compute_potential_energy(sim.bodies, G=G, softening=softening)
284
+ kinetic_history.append(ke)
285
+ potential_history.append(pe)
286
+ total_history.append(ke + pe)
287
+
288
+ for i, body in enumerate(sim.bodies):
289
+ scatters[i].set_offsets(body.position.reshape(1, 2))
290
+ trail = np.array(body.trail[-trail_length:])
291
+ if len(trail) > 1:
292
+ trail_lines[i].set_data(trail[:, 0], trail[:, 1])
293
+
294
+ x = np.arange(len(total_history))
295
+ ke_line.set_data(x, kinetic_history)
296
+ pe_line.set_data(x, potential_history)
297
+ te_line.set_data(x, total_history)
298
+
299
+ ax_energy.relim()
300
+ ax_energy.autoscale_view()
301
+
302
+ return scatters + trail_lines + [ke_line, pe_line, te_line]
303
+
304
+ anim = FuncAnimation(
305
+ fig,
306
+ update,
307
+ frames=frames,
308
+ init_func=init,
309
+ interval=40,
310
+ blit=False,
311
+ )
312
+
313
+ temp_dir = tempfile.mkdtemp()
314
+ output_path = os.path.join(temp_dir, "nbody_simulation.gif")
315
+ anim.save(output_path, writer="pillow", fps=20)
316
+ plt.close(fig)
317
+
318
+ return output_path
319
+
320
+
321
+ # -----------------------------
322
+ # Gradio UI
323
+ # -----------------------------
324
+ DESCRIPTION = """
325
+ ## N-Body Orbital Physics Lab
326
+
327
+ Explore Newtonian gravity with an interactive **Velocity Verlet** simulation.
328
+
329
+ ### Included
330
+ - Two-body circular orbit
331
+ - Three-body chaotic dynamics
332
+ - Custom two-body experiment mode
333
+ - Energy diagnostics
334
+ - Adjustable timestep, softening, and trail length
335
+ """
336
+
337
+ with gr.Blocks(title="N-Body Orbital Physics Lab", theme=gr.themes.Soft()) as demo:
338
+ gr.Markdown("# 🪐 N-Body Orbital Physics Lab")
339
+ gr.Markdown(DESCRIPTION)
340
+
341
+ with gr.Row():
342
+ with gr.Column():
343
+ preset = gr.Dropdown(
344
+ choices=[
345
+ "Two-Body Circular Orbit",
346
+ "Three-Body Chaotic System",
347
+ "Custom Two-Body Experiment",
348
+ ],
349
+ value="Two-Body Circular Orbit",
350
+ label="Preset",
351
+ )
352
+
353
+ dt = gr.Slider(0.0001, 0.005, value=0.0005, step=0.0001, label="Time Step (dt)")
354
+ frames = gr.Slider(100, 400, value=250, step=50, label="Rendered Frames")
355
+ steps_per_frame = gr.Slider(1, 8, value=4, step=1, label="Physics Steps per Frame")
356
+ G = gr.Slider(0.1, 5.0, value=1.0, step=0.1, label="Gravitational Constant (G)")
357
+ softening = gr.Slider(0.0001, 0.05, value=0.001, step=0.0001, label="Softening")
358
+ trail_length = gr.Slider(20, 300, value=100, step=10, label="Trail Length")
359
+
360
+ gr.Markdown("### Custom Two-Body Controls")
361
+ central_mass = gr.Slider(100, 5000, value=1000, step=50, label="Central Mass")
362
+ orbit_radius = gr.Slider(0.5, 3.0, value=1.0, step=0.1, label="Orbit Radius")
363
+ planet_mass = gr.Slider(0.1, 20.0, value=1.0, step=0.1, label="Planet Mass")
364
+ planet_speed = gr.Slider(1.0, 80.0, value=31.6, step=0.1, label="Planet Tangential Speed")
365
+ tangential_direction = gr.Radio(
366
+ choices=["Counterclockwise", "Clockwise"],
367
+ value="Counterclockwise",
368
+ label="Orbit Direction",
369
+ )
370
+
371
+ run_button = gr.Button("Run Simulation", variant="primary")
372
+
373
+ with gr.Column():
374
+ output_gif = gr.Image(label="Simulation Output", type="filepath")
375
+
376
+ run_button.click(
377
+ fn=simulate_system,
378
+ inputs=[
379
+ preset,
380
+ dt,
381
+ frames,
382
+ steps_per_frame,
383
+ G,
384
+ softening,
385
+ trail_length,
386
+ central_mass,
387
+ orbit_radius,
388
+ planet_mass,
389
+ planet_speed,
390
+ tangential_direction,
391
+ ],
392
+ outputs=output_gif,
393
+ )
394
+
395
+ if __name__ == "__main__":
396
+ demo.launch()