NavyDevilDoc commited on
Commit
3059793
·
verified ·
1 Parent(s): 1ac3c60

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +550 -32
src/streamlit_app.py CHANGED
@@ -1,40 +1,558 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
 
12
 
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from kinematics_visualizer import Motion1D, Motion2D, KinematicsVisualizer
5
 
6
+ # Configure Streamlit page
7
+ st.set_page_config(
8
+ page_title="Physics Tutorial: Kinematics",
9
+ page_icon="🚀",
10
+ layout="wide",
11
+ initial_sidebar_state="expanded"
12
+ )
13
 
14
+ # Custom CSS for better styling
15
+ st.markdown("""
16
+ <style>
17
+ .main-header {
18
+ font-size: 2.5rem;
19
+ font-weight: bold;
20
+ color: #1f77b4;
21
+ text-align: center;
22
+ margin-bottom: 2rem;
23
+ }
24
+ .section-header {
25
+ font-size: 1.5rem;
26
+ font-weight: bold;
27
+ color: #2e7d32;
28
+ margin-top: 2rem;
29
+ margin-bottom: 1rem;
30
+ }
31
+ .physics-equation {
32
+ background-color: #2e2e2e;
33
+ color: white;
34
+ padding: 1rem;
35
+ border-radius: 10px;
36
+ border-left: 5px solid #1f77b4;
37
+ margin: 1rem 0;
38
+ }
39
+ .physics-equation h4 {
40
+ color: #87ceeb;
41
+ margin-bottom: 1rem;
42
+ }
43
+ .physics-equation ul {
44
+ color: white;
45
+ }
46
+ .physics-equation li {
47
+ margin-bottom: 0.5rem;
48
+ }
49
+ .physics-equation strong {
50
+ color: #ffd700;
51
+ }
52
+ .parameter-box {
53
+ background-color: #fff3e0;
54
+ padding: 1rem;
55
+ border-radius: 10px;
56
+ margin: 1rem 0;
57
+ }
58
+ </style>
59
+ """, unsafe_allow_html=True)
60
 
61
+ def main():
62
+ st.markdown('<div class="main-header">🚀 Physics Tutorial: Kinematics</div>', unsafe_allow_html=True)
63
+
64
+ # Sidebar for navigation
65
+ st.sidebar.title("📚 Select Tutorial")
66
+ tutorial_type = st.sidebar.radio(
67
+ "Choose a physics concept:",
68
+ ["1D Motion", "2D Projectile Motion", "Compare Motions"]
69
+ )
70
+
71
+ if tutorial_type == "1D Motion":
72
+ show_1d_motion()
73
+ elif tutorial_type == "2D Projectile Motion":
74
+ show_2d_motion()
75
+ elif tutorial_type == "Compare Motions":
76
+ show_motion_comparison()
77
 
78
+ def show_1d_motion():
79
+ st.markdown('<div class="section-header">📏 One-Dimensional Motion</div>', unsafe_allow_html=True)
80
+
81
+ # Physics equations display
82
+ with st.expander("📖 Physics Equations (Click to expand)", expanded=False):
83
+ st.markdown("""
84
+ <div class="physics-equation">
85
+ <h4>Kinematic Equations for Constant Acceleration:</h4>
86
+ <ul>
87
+ <li><strong>Position:</strong> x(t) = x₀ + v₀t + ½at²</li>
88
+ <li><strong>Velocity:</strong> v(t) = v₀ + at</li>
89
+ <li><strong>Acceleration:</strong> a(t) = constant</li>
90
+ </ul>
91
+ <p><strong>Where:</strong></p>
92
+ <ul>
93
+ <li>x₀ = initial position (m)</li>
94
+ <li>v₀ = initial velocity (m/s)</li>
95
+ <li>a = acceleration (m/s²)</li>
96
+ <li>t = time (s)</li>
97
+ </ul>
98
+ </div>
99
+ """, unsafe_allow_html=True)
100
+
101
+ # Create two columns for controls and results
102
+ col1, col2 = st.columns([1, 2])
103
+
104
+ with col1:
105
+ st.markdown('<div class="section-header">🎛️ Control Parameters</div>', unsafe_allow_html=True)
106
+
107
+ # Parameter sliders
108
+ initial_pos = st.slider(
109
+ "Initial Position (m)",
110
+ min_value=-50.0, max_value=50.0, value=0.0, step=1.0,
111
+ help="Starting position of the object"
112
+ )
113
+
114
+ initial_vel = st.slider(
115
+ "Initial Velocity (m/s)",
116
+ min_value=-30.0, max_value=30.0, value=5.0, step=1.0,
117
+ help="Starting velocity of the object"
118
+ )
119
+
120
+ acceleration = st.slider(
121
+ "Acceleration (m/s²)",
122
+ min_value=-15.0, max_value=15.0, value=2.0, step=0.5,
123
+ help="Constant acceleration (positive = speeding up in positive direction)"
124
+ )
125
+
126
+ duration = st.slider(
127
+ "Simulation Duration (s)",
128
+ min_value=1.0, max_value=20.0, value=10.0, step=0.5,
129
+ help="How long to run the simulation"
130
+ )
131
+
132
+ # Display current parameters
133
+ st.markdown('<div class="parameter-box">', unsafe_allow_html=True)
134
+ st.markdown("**Current Parameters:**")
135
+ st.write(f"• Initial Position: {initial_pos:.1f} m")
136
+ st.write(f"• Initial Velocity: {initial_vel:.1f} m/s")
137
+ st.write(f"• Acceleration: {acceleration:.1f} m/s²")
138
+ st.write(f"• Duration: {duration:.1f} s")
139
+
140
+ # Calculate final values
141
+ final_pos = initial_pos + initial_vel * duration + 0.5 * acceleration * duration**2
142
+ final_vel = initial_vel + acceleration * duration
143
+
144
+ st.markdown("**Final Values:**")
145
+ st.write(f"• Final Position: {final_pos:.1f} m")
146
+ st.write(f"• Final Velocity: {final_vel:.1f} m/s")
147
+ st.markdown('</div>', unsafe_allow_html=True)
148
+
149
+ with col2:
150
+ # Create and display motion
151
+ motion = Motion1D(
152
+ initial_position=initial_pos,
153
+ initial_velocity=initial_vel,
154
+ acceleration=acceleration
155
+ )
156
+
157
+ # Generate title based on motion type
158
+ if acceleration > 0:
159
+ motion_type = "Accelerating Motion"
160
+ elif acceleration < 0:
161
+ motion_type = "Decelerating Motion"
162
+ else:
163
+ motion_type = "Constant Velocity Motion"
164
+
165
+ visualizer = KinematicsVisualizer()
166
+ fig = visualizer.plot_1d_motion(motion, duration, motion_type)
167
+
168
+ st.pyplot(fig)
169
+ plt.close()
170
 
171
+ def show_2d_motion():
172
+ st.markdown('<div class="section-header">🎯 Two-Dimensional Projectile Motion</div>', unsafe_allow_html=True)
173
+
174
+ # Physics equations display (updated to include sphere physics)
175
+ with st.expander("📖 Physics Equations (Click to expand)", expanded=False):
176
+ st.markdown("""
177
+ <div class="physics-equation">
178
+ <h4>Projectile Motion Equations:</h4>
179
+ <p><strong>Point Mass Model:</strong></p>
180
+ <ul>
181
+ <li><strong>No Air Resistance:</strong> Standard kinematic equations</li>
182
+ <li><strong>With Air Resistance:</strong> Linear drag model (drag ∝ velocity)</li>
183
+ </ul>
184
+ <p><strong>Sphere Model (more realistic):</strong></p>
185
+ <ul>
186
+ <li><strong>Mass:</strong> m = ρ × (4/3)π × r³</li>
187
+ <li><strong>Cross-sectional Area:</strong> A = π × r²</li>
188
+ <li><strong>Drag Force:</strong> F<sub>drag</sub> = ½ρ<sub>air</sub>C<sub>d</sub>Av²</li>
189
+ <li><strong>Terminal Velocity:</strong> v<sub>t</sub> = √(2mg / ρ<sub>air</sub>C<sub>d</sub>A)</li>
190
+ </ul>
191
+ <p><strong>Where:</strong></p>
192
+ <ul>
193
+ <li>ρ = sphere density (kg/m³)</li>
194
+ <li>r = sphere radius (m)</li>
195
+ <li>C<sub>d</sub> = aerodynamic drag coefficient</li>
196
+ <li>ρ<sub>air</sub> = air density (~1.225 kg/m³)</li>
197
+ </ul>
198
+ </div>
199
+ """, unsafe_allow_html=True)
200
+
201
+ # Create two columns
202
+ col1, col2 = st.columns([1, 2])
203
+
204
+ with col1:
205
+ st.markdown('<div class="section-header">🎛️ Launch Parameters</div>', unsafe_allow_html=True)
206
+
207
+ # Projectile Model Toggle
208
+ st.markdown("### 🎯 Projectile Model")
209
+ is_sphere = st.toggle(
210
+ "Model as Sphere",
211
+ value=False,
212
+ help="Toggle between point mass and realistic sphere with physical dimensions"
213
+ )
214
+
215
+ # Sphere properties (only show when sphere model is enabled)
216
+ sphere_radius = 0.037 # Default baseball
217
+ sphere_density = 700
218
+ sphere_drag_coeff = 0.47
219
+
220
+ if is_sphere:
221
+ # Sphere presets
222
+ st.markdown("**Quick Sphere Presets:**")
223
+ presets = Motion2D.get_sphere_presets()
224
+ preset_cols = st.columns(3)
225
+
226
+ selected_preset = None
227
+ for i, (key, preset) in enumerate(presets.items()):
228
+ with preset_cols[i % 3]:
229
+ if st.button(preset['name'], key=f"sphere_{key}"):
230
+ selected_preset = preset
231
+
232
+ # Apply preset if selected
233
+ if selected_preset:
234
+ sphere_radius = selected_preset['radius']
235
+ sphere_density = selected_preset['density']
236
+ sphere_drag_coeff = selected_preset['drag_coeff']
237
+
238
+ st.markdown("**Custom Sphere Properties:**")
239
+
240
+ # Sphere radius slider
241
+ sphere_radius = st.slider(
242
+ "Sphere Radius (mm)",
243
+ min_value=10.0, max_value=150.0,
244
+ value=sphere_radius*1000, step=1.0,
245
+ help="Radius of the sphere in millimeters"
246
+ ) / 1000 # Convert back to meters
247
+
248
+ # Sphere density slider
249
+ sphere_density = st.slider(
250
+ "Sphere Density (kg/m³)",
251
+ min_value=50, max_value=2000,
252
+ value=int(sphere_density), step=10,
253
+ help="Material density - affects mass and terminal velocity"
254
+ )
255
+
256
+ # Drag coefficient slider
257
+ sphere_drag_coeff = st.slider(
258
+ "Aerodynamic Drag Coefficient",
259
+ min_value=0.1, max_value=1.0,
260
+ value=sphere_drag_coeff, step=0.01,
261
+ help="0.24 (golf ball), 0.35 (baseball), 0.47 (smooth sphere), 0.51 (tennis ball)"
262
+ )
263
+
264
+ # Display calculated properties
265
+ temp_motion = Motion2D(
266
+ launch_speed=25, launch_angle=45,
267
+ is_sphere=True, sphere_radius=sphere_radius,
268
+ sphere_density=sphere_density, sphere_drag_coeff=sphere_drag_coeff
269
+ )
270
+ sphere_info = temp_motion.get_sphere_info()
271
+
272
+ st.info(f"""
273
+ **Calculated Properties:**
274
+ • Diameter: {sphere_info['diameter_mm']:.1f} mm
275
+ • Mass: {sphere_info['mass_g']:.1f} g
276
+ • Cross-section: {sphere_info['cross_section_cm2']:.1f} cm²
277
+ • Volume: {sphere_info['volume_cm3']:.1f} cm³
278
+ """)
279
+
280
+ # Air Resistance Toggle
281
+ st.markdown("### 🌬️ Air Resistance")
282
+ air_resistance_enabled = st.toggle(
283
+ "Enable Air Resistance",
284
+ value=False,
285
+ help="Toggle air resistance on/off to see the difference in trajectory"
286
+ )
287
+
288
+ # Show drag info based on model
289
+ if air_resistance_enabled and not is_sphere:
290
+ drag_coeff = st.slider(
291
+ "Point Mass Drag Coefficient",
292
+ min_value=0.01, max_value=0.5, value=0.1, step=0.01,
293
+ help="Simple linear drag coefficient for point mass model"
294
+ )
295
+ elif air_resistance_enabled and is_sphere:
296
+ st.info("🌬️ **Sphere Model**: Air resistance calculated from physical properties!")
297
+ drag_coeff = 0.1 # Not used for sphere model
298
+ else:
299
+ drag_coeff = 0.1
300
+
301
+ st.markdown("---")
302
+
303
+ # Launch parameter presets and sliders (same as before)
304
+ # ... [include all the existing preset and slider code] ...
305
+
306
+ # Initialize default values
307
+ default_speed = 25.0
308
+ default_angle = 45.0
309
+ default_height = 0.0
310
+ default_gravity = 9.81
311
+
312
+ # Handle preset button clicks
313
+ st.markdown("### 🎯 Launch Presets")
314
+ preset_buttons_col1, preset_buttons_col2 = st.columns(2)
315
+ with preset_buttons_col1:
316
+ if st.button("🏀 Basketball Shot"):
317
+ st.session_state.speed_preset = 15.0
318
+ st.session_state.angle_preset = 50.0
319
+ st.session_state.height_preset = 2.0
320
+ st.session_state.gravity_preset = 9.81
321
+
322
+ with preset_buttons_col2:
323
+ if st.button("🚀 Rocket Launch"):
324
+ st.session_state.speed_preset = 40.0
325
+ st.session_state.angle_preset = 75.0
326
+ st.session_state.height_preset = 0.0
327
+ st.session_state.gravity_preset = 9.81
328
+
329
+ # Use preset values if they exist, otherwise use defaults
330
+ speed_value = st.session_state.get('speed_preset', default_speed)
331
+ angle_value = st.session_state.get('angle_preset', default_angle)
332
+ height_value = st.session_state.get('height_preset', default_height)
333
+ gravity_value = st.session_state.get('gravity_preset', default_gravity)
334
+
335
+ st.markdown("### ⚙️ Physics Parameters")
336
+
337
+ # Parameter sliders
338
+ launch_speed = st.slider(
339
+ "Launch Speed (m/s)",
340
+ min_value=5.0, max_value=50.0, value=speed_value, step=1.0,
341
+ help="Initial speed of the projectile"
342
+ )
343
+
344
+ launch_angle = st.slider(
345
+ "Launch Angle (degrees)",
346
+ min_value=0.0, max_value=90.0, value=angle_value, step=5.0,
347
+ help="Angle above horizontal"
348
+ )
349
+
350
+ launch_height = st.slider(
351
+ "Launch Height (m)",
352
+ min_value=0.0, max_value=50.0, value=height_value, step=1.0,
353
+ help="Height above ground level"
354
+ )
355
+
356
+ gravity = st.slider(
357
+ "Gravity (m/s²)",
358
+ min_value=1.0, max_value=20.0, value=gravity_value, step=0.1,
359
+ help="Acceleration due to gravity (Earth = 9.81 m/s²)"
360
+ )
361
+
362
+ # Clear preset values when sliders are moved
363
+ if (launch_speed != speed_value or launch_angle != angle_value or
364
+ launch_height != height_value or gravity != gravity_value):
365
+ for key in ['speed_preset', 'angle_preset', 'height_preset', 'gravity_preset']:
366
+ if key in st.session_state:
367
+ del st.session_state[key]
368
+
369
+ # Create motion object with all parameters
370
+ motion = Motion2D(
371
+ launch_speed=launch_speed,
372
+ launch_angle=launch_angle,
373
+ launch_height=launch_height,
374
+ gravity=gravity,
375
+ air_resistance=air_resistance_enabled,
376
+ drag_coefficient=drag_coeff,
377
+ is_sphere=is_sphere,
378
+ sphere_radius=sphere_radius,
379
+ sphere_density=sphere_density,
380
+ sphere_drag_coeff=sphere_drag_coeff
381
+ )
382
+
383
+ info = motion.get_launch_info()
384
+
385
+ # Display comprehensive analysis
386
+ st.markdown('<div class="parameter-box">', unsafe_allow_html=True)
387
+ st.markdown("**Launch Analysis:**")
388
+ st.write(f"• Model: {'Sphere' if is_sphere else 'Point Mass'}")
389
+ st.write(f"• Launch Speed: {info['launch_speed']:.1f} m/s")
390
+ st.write(f"• Launch Angle: {info['launch_angle']:.1f}°")
391
+ st.write(f"• Initial Vₓ: {info['initial_velocity_x']:.1f} m/s")
392
+ st.write(f"• Initial Vᵧ: {info['initial_velocity_y']:.1f} m/s")
393
+
394
+ if is_sphere and 'mass_g' in info:
395
+ st.write(f"• Mass: {info['mass_g']:.1f} g")
396
+ st.write(f"• Diameter: {info['diameter_mm']:.1f} mm")
397
+ if air_resistance_enabled and 'terminal_velocity' in info:
398
+ st.write(f"• Terminal Velocity: {info['terminal_velocity']:.1f} m/s")
399
+
400
+ st.markdown("**Trajectory Results:**")
401
+ st.write(f"• Flight Time: {info['flight_time']:.2f} s")
402
+ st.write(f"• Range: {info['range']:.1f} m")
403
+ st.write(f"• Max Height: {info['max_height']:.1f} m")
404
+
405
+ # Physics insights
406
+ if is_sphere and air_resistance_enabled and 'mass_g' in info:
407
+ st.markdown("**Physics Insights:**")
408
+ if info['mass_g'] > 200:
409
+ st.write("🔹 Heavy object: Less affected by air resistance")
410
+ elif info['mass_g'] < 50:
411
+ st.write("🔹 Light object: Significantly affected by air resistance")
412
+
413
+ if info['diameter_mm'] > 100:
414
+ st.write("🔹 Large cross-section: More air resistance")
415
+ elif info['diameter_mm'] < 50:
416
+ st.write("🔹 Small cross-section: Less air resistance")
417
+
418
+ st.markdown('</div>', unsafe_allow_html=True)
419
+
420
+ # Reset button
421
+ if st.button("🔄 Reset to Defaults"):
422
+ for key in ['speed_preset', 'angle_preset', 'height_preset', 'gravity_preset']:
423
+ if key in st.session_state:
424
+ del st.session_state[key]
425
+ st.rerun()
426
+
427
+ with col2:
428
+ # Create and display trajectory with model info
429
+ visualizer = KinematicsVisualizer()
430
+
431
+ model_info = f"({'Sphere' if is_sphere else 'Point Mass'})"
432
+ air_info = " (with Air Resistance)" if air_resistance_enabled else " (No Air Resistance)"
433
+ trajectory_title = f"Projectile Motion - {launch_angle:.0f}° Launch {model_info}{air_info}"
434
+
435
+ fig = visualizer.plot_2d_trajectory(motion, title=trajectory_title)
436
+ st.pyplot(fig)
437
+ plt.close()
438
+
439
+ # Show model comparison if using sphere
440
+ if is_sphere and 'mass_g' in info:
441
+ st.markdown("### 📊 Sphere vs Point Mass Comparison")
442
+
443
+ fig_comp, ax = plt.subplots(figsize=(12, 6))
444
+
445
+ # Plot sphere model
446
+ data_sphere = motion.trajectory_data(motion.calculate_flight_time())
447
+ ax.plot(data_sphere['x'], data_sphere['y'],
448
+ 'r-', linewidth=3, label=f'Sphere Model ({info["mass_g"]:.0f}g)', alpha=0.8)
449
+
450
+ # Plot equivalent point mass
451
+ motion_point = Motion2D(
452
+ launch_speed=launch_speed, launch_angle=launch_angle,
453
+ launch_height=launch_height, gravity=gravity,
454
+ air_resistance=air_resistance_enabled, drag_coefficient=drag_coeff,
455
+ is_sphere=False
456
+ )
457
+ data_point = motion_point.trajectory_data(motion_point.calculate_flight_time())
458
+ ax.plot(data_point['x'], data_point['y'],
459
+ 'b--', linewidth=2, label='Point Mass Model', alpha=0.7)
460
+
461
+ ax.set_xlabel('Horizontal Position (m)')
462
+ ax.set_ylabel('Vertical Position (m)')
463
+ ax.set_title('Sphere Model vs Point Mass Model')
464
+ ax.grid(True, alpha=0.3)
465
+ ax.legend()
466
+ ax.set_ylim(bottom=0)
467
+
468
+ st.pyplot(fig_comp)
469
+ plt.close()
470
 
471
+ def show_motion_comparison():
472
+ st.markdown('<div class="section-header">📊 Compare Different Trajectories</div>', unsafe_allow_html=True)
473
+
474
+ st.markdown("**Compare up to 3 different projectile motions side by side**")
475
+
476
+ # Create tabs for different trajectories
477
+ tab1, tab2, tab3 = st.tabs(["🚀 Trajectory 1", "🎯 Trajectory 2", "⚽ Trajectory 3"])
478
+
479
+ trajectories = []
480
+
481
+ with tab1:
482
+ col1, col2 = st.columns(2)
483
+ with col1:
484
+ speed1 = st.slider("Speed 1 (m/s)", 5.0, 50.0, 20.0, key="speed1")
485
+ angle1 = st.slider("Angle 1 (°)", 0.0, 90.0, 30.0, key="angle1")
486
+ with col2:
487
+ height1 = st.slider("Height 1 (m)", 0.0, 30.0, 0.0, key="height1")
488
+
489
+ motion1 = Motion2D(launch_speed=speed1, launch_angle=angle1, launch_height=height1)
490
+ trajectories.append(("Trajectory 1", motion1, 'blue'))
491
+
492
+ with tab2:
493
+ col1, col2 = st.columns(2)
494
+ with col1:
495
+ speed2 = st.slider("Speed 2 (m/s)", 5.0, 50.0, 25.0, key="speed2")
496
+ angle2 = st.slider("Angle 2 (°)", 0.0, 90.0, 45.0, key="angle2")
497
+ with col2:
498
+ height2 = st.slider("Height 2 (m)", 0.0, 30.0, 0.0, key="height2")
499
+
500
+ motion2 = Motion2D(launch_speed=speed2, launch_angle=angle2, launch_height=height2)
501
+ trajectories.append(("Trajectory 2", motion2, 'red'))
502
+
503
+ with tab3:
504
+ col1, col2 = st.columns(2)
505
+ with col1:
506
+ speed3 = st.slider("Speed 3 (m/s)", 5.0, 50.0, 30.0, key="speed3")
507
+ angle3 = st.slider("Angle 3 (°)", 0.0, 90.0, 60.0, key="angle3")
508
+ with col2:
509
+ height3 = st.slider("Height 3 (m)", 0.0, 30.0, 5.0, key="height3")
510
+
511
+ motion3 = Motion2D(launch_speed=speed3, launch_angle=angle3, launch_height=height3)
512
+ trajectories.append(("Trajectory 3", motion3, 'green'))
513
+
514
+ # Create comparison plot
515
+ fig, ax = plt.subplots(figsize=(12, 8))
516
+
517
+ max_range = 0
518
+ for name, motion, color in trajectories:
519
+ data = motion.trajectory_data(motion.calculate_flight_time())
520
+ ax.plot(data['x'], data['y'], linewidth=3, label=name, color=color)
521
+
522
+ # Mark launch and landing points
523
+ ax.plot(motion.launch_x, motion.launch_height, 'o', markersize=8, color=color, alpha=0.7)
524
+ if len(data['x']) > 0:
525
+ ax.plot(data['x'][-1], data['y'][-1], 's', markersize=8, color=color, alpha=0.7)
526
+ max_range = max(max_range, data['x'][-1])
527
+
528
+ ax.set_xlabel('Horizontal Position (m)', fontsize=12)
529
+ ax.set_ylabel('Vertical Position (m)', fontsize=12)
530
+ ax.set_title('Trajectory Comparison', fontsize=16, fontweight='bold')
531
+ ax.grid(True, alpha=0.3)
532
+ ax.legend(fontsize=12)
533
+ ax.set_ylim(bottom=0)
534
+ ax.set_xlim(0, max_range * 1.1)
535
+
536
+ st.pyplot(fig)
537
+ plt.close()
538
+
539
+ # Comparison table
540
+ st.markdown("### 📋 Trajectory Comparison Table")
541
+
542
+ comparison_data = []
543
+ for name, motion, _ in trajectories:
544
+ info = motion.get_launch_info()
545
+ comparison_data.append({
546
+ "Trajectory": name,
547
+ "Speed (m/s)": f"{info['launch_speed']:.1f}",
548
+ "Angle (°)": f"{info['launch_angle']:.1f}",
549
+ "Height (m)": f"{info['launch_height']:.1f}",
550
+ "Flight Time (s)": f"{info['flight_time']:.2f}",
551
+ "Range (m)": f"{info['range']:.1f}",
552
+ "Max Height (m)": f"{info['max_height']:.1f}"
553
+ })
554
+
555
+ st.table(comparison_data)
556
 
557
+ if __name__ == "__main__":
558
+ main()