NavyDevilDoc commited on
Commit
17ebf4b
·
verified ·
1 Parent(s): 26fddb4

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +162 -356
src/streamlit_app.py CHANGED
@@ -1,7 +1,6 @@
1
  import streamlit as st
2
  import numpy as np
3
  import plotly.graph_objects as go
4
- import plotly.express as px
5
  from plotly.subplots import make_subplots
6
  from kinematics_visualizer import Motion1D, Motion2D, KinematicsVisualizer
7
 
@@ -63,25 +62,19 @@ st.markdown("""
63
  def main():
64
  st.markdown('<div class="main-header">🚀 Physics Tutorial: Kinematics</div>', unsafe_allow_html=True)
65
 
66
- # Use selectbox instead of radio (forces full page refresh)
67
  st.sidebar.title("📚 Select Tutorial")
68
- tutorial_type = st.sidebar.selectbox(
69
  "Choose a physics concept:",
70
- ["1D Motion", "2D Projectile Motion", "Compare Motions"]
71
  )
72
 
73
- # Force complete refresh when changing tutorials
74
  if tutorial_type == "1D Motion":
75
- st.cache_data.clear() # Clear all caches
76
  show_1d_motion()
77
  elif tutorial_type == "2D Projectile Motion":
78
- st.cache_data.clear() # Clear all caches
79
  show_2d_motion()
80
- elif tutorial_type == "Compare Motions":
81
- st.cache_data.clear() # Clear all caches
82
- show_motion_comparison()
83
 
84
- def create_1d_motion_plot(motion: Motion1D, duration: float, title: str):
85
  """Create 1D motion plots using Plotly"""
86
  # Generate time arrays
87
  t, x, v, a = motion.time_arrays(duration, dt=0.01)
@@ -138,7 +131,7 @@ def create_1d_motion_plot(motion: Motion1D, duration: float, title: str):
138
 
139
  return fig
140
 
141
- def create_2d_trajectory_plot(motion: Motion2D, title: str):
142
  """Create 2D trajectory plot using Plotly"""
143
  flight_time = motion.calculate_flight_time()
144
  data = motion.trajectory_data(flight_time)
@@ -177,16 +170,17 @@ def create_2d_trajectory_plot(motion: Motion2D, title: str):
177
  ))
178
 
179
  # Add maximum height point
180
- max_height_idx = np.argmax(data['y'])
181
- if max_height_idx > 0:
182
- fig.add_trace(go.Scatter(
183
- x=[data['x'][max_height_idx]],
184
- y=[data['y'][max_height_idx]],
185
- mode='markers',
186
- name='Max Height',
187
- marker=dict(color='orange', size=10, symbol='triangle-up'),
188
- hovertemplate='Max Height<br>X: %{x:.1f} m<br>Y: %{y:.1f} m<extra></extra>'
189
- ))
 
190
 
191
  # Update layout
192
  fig.update_layout(
@@ -201,8 +195,12 @@ def create_2d_trajectory_plot(motion: Motion2D, title: str):
201
  )
202
 
203
  # Set axis ranges
204
- fig.update_xaxes(range=[0, max(data['x']) * 1.1 if len(data['x']) > 0 else 10])
205
- fig.update_yaxes(range=[0, max(data['y']) * 1.2 if len(data['y']) > 0 else 10])
 
 
 
 
206
 
207
  # Add grid
208
  fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.3)')
@@ -213,105 +211,93 @@ def create_2d_trajectory_plot(motion: Motion2D, title: str):
213
  def show_1d_motion():
214
  st.markdown('<div class="section-header">📏 One-Dimensional Motion</div>', unsafe_allow_html=True)
215
 
216
- # Add unique container key
217
- with st.container(key="1d_motion_container"):
218
- # Physics equations display
219
- with st.expander("📖 Physics Equations (Click to expand)", expanded=False):
220
- st.markdown("""
221
- <div class="physics-equation">
222
- <h4>Kinematic Equations for Constant Acceleration:</h4>
223
- <ul>
224
- <li><strong>Position:</strong> x(t) = x₀ + v₀t + ½at²</li>
225
- <li><strong>Velocity:</strong> v(t) = v₀ + at</li>
226
- <li><strong>Acceleration:</strong> a(t) = constant</li>
227
- </ul>
228
- <p><strong>Where:</strong></p>
229
- <ul>
230
- <li>x₀ = initial position (m)</li>
231
- <li>v₀ = initial velocity (m/s)</li>
232
- <li>a = acceleration (m/s²)</li>
233
- <li>t = time (s)</li>
234
- </ul>
235
- </div>
236
- """, unsafe_allow_html=True)
237
-
238
- # Create two columns for controls and results
239
- col1, col2 = st.columns([1, 2])
240
-
241
- with col1:
242
- st.markdown('<div class="section-header">🎛️ Control Parameters</div>', unsafe_allow_html=True)
243
-
244
- # Parameter sliders with unique keys for 1D motion
245
- initial_pos = st.slider(
246
- "Initial Position (m)",
247
- min_value=-50.0, max_value=50.0, value=0.0, step=1.0,
248
- help="Starting position of the object",
249
- key="1d_initial_pos"
250
- )
251
-
252
- initial_vel = st.slider(
253
- "Initial Velocity (m/s)",
254
- min_value=-30.0, max_value=30.0, value=5.0, step=1.0,
255
- help="Starting velocity of the object",
256
- key="1d_initial_vel"
257
- )
258
-
259
- acceleration = st.slider(
260
- "Acceleration (m/s²)",
261
- min_value=-15.0, max_value=15.0, value=2.0, step=0.5,
262
- help="Constant acceleration (positive = speeding up in positive direction)",
263
- key="1d_acceleration"
264
- )
265
-
266
- duration = st.slider(
267
- "Simulation Duration (s)",
268
- min_value=1.0, max_value=20.0, value=10.0, step=0.5,
269
- help="How long to run the simulation",
270
- key="1d_duration"
271
- )
272
-
273
- # Display current parameters
274
- st.markdown('<div class="parameter-box">', unsafe_allow_html=True)
275
- st.markdown("**Current Parameters:**")
276
- st.write(f"• Initial Position: {initial_pos:.1f} m")
277
- st.write(f"• Initial Velocity: {initial_vel:.1f} m/s")
278
- st.write(f"• Acceleration: {acceleration:.1f} m/s²")
279
- st.write(f" Duration: {duration:.1f} s")
280
-
281
- # Calculate final values
282
- final_pos = initial_pos + initial_vel * duration + 0.5 * acceleration * duration**2
283
- final_vel = initial_vel + acceleration * duration
284
-
285
- st.markdown("**Final Values:**")
286
- st.write(f"• Final Position: {final_pos:.1f} m")
287
- st.write(f"• Final Velocity: {final_vel:.1f} m/s")
288
- st.markdown('</div>', unsafe_allow_html=True)
289
-
290
- with col2:
291
- # Create unique cache function for 1D motion ONLY
292
- @st.cache_data(show_spinner=False)
293
- def create_1d_motion_plot_unique(tab_id, initial_pos, initial_vel, acceleration, duration):
294
- """Tab-specific 1D motion plot - includes tab_id to prevent cross-contamination"""
295
- motion = Motion1D(
296
- initial_position=initial_pos,
297
- initial_velocity=initial_vel,
298
- acceleration=acceleration
299
- )
300
-
301
- # Generate title based on motion type
302
- if acceleration > 0:
303
- motion_type = "Accelerating Motion"
304
- elif acceleration < 0:
305
- motion_type = "Decelerating Motion"
306
- else:
307
- motion_type = "Constant Velocity Motion"
308
-
309
- return create_1d_motion_plot(motion, duration, motion_type)
310
-
311
- # Create and display Plotly chart with tab-specific cache
312
- fig = create_1d_motion_plot_unique("1d_tab", initial_pos, initial_vel, acceleration, duration)
313
- st.plotly_chart(fig, use_container_width=True, key="1d_motion_plot_unique")
314
-
315
 
316
  def show_2d_motion():
317
  st.markdown('<div class="section-header">🎯 Two-Dimensional Projectile Motion</div>', unsafe_allow_html=True)
@@ -566,246 +552,66 @@ def show_2d_motion():
566
  del st.session_state[key]
567
  st.rerun()
568
 
569
- with col2:
570
- # Create and display trajectory with model info
571
- model_info = f"({'Sphere' if is_sphere else 'Point Mass'})"
572
- air_info = " (with Air Resistance)" if air_resistance_enabled else " (No Air Resistance)"
573
- trajectory_title = f"Projectile Motion - {launch_angle:.0f}° Launch {model_info}{air_info}"
574
-
575
- # Create unique cache function for 2D motion ONLY
576
- @st.cache_data(show_spinner=False)
577
- def create_2d_motion_plot_unique(tab_id, launch_speed, launch_angle, launch_height, gravity, air_resistance_enabled, drag_coeff, is_sphere, sphere_radius, sphere_density, sphere_drag_coeff):
578
- """Tab-specific 2D motion plot - includes tab_id to prevent cross-contamination"""
579
- motion = Motion2D(
580
- launch_speed=launch_speed,
581
- launch_angle=launch_angle,
582
- launch_height=launch_height,
583
- gravity=gravity,
584
- air_resistance=air_resistance_enabled,
585
- drag_coefficient=drag_coeff,
586
- is_sphere=is_sphere,
587
- sphere_radius=sphere_radius,
588
- sphere_density=sphere_density,
589
- sphere_drag_coeff=sphere_drag_coeff
590
- )
591
-
592
- model_info = f"({'Sphere' if is_sphere else 'Point Mass'})"
593
- air_info = " (with Air Resistance)" if air_resistance_enabled else " (No Air Resistance)"
594
- trajectory_title = f"Projectile Motion - {launch_angle:.0f}° Launch {model_info}{air_info}"
595
-
596
- return create_2d_trajectory_plot(motion, trajectory_title)
597
-
598
- # Create and display main trajectory plot with tab-specific cache
599
- fig = create_2d_motion_plot_unique(
600
- "2d_tab", launch_speed, launch_angle, launch_height, gravity,
601
- air_resistance_enabled, drag_coeff, is_sphere, sphere_radius,
602
- sphere_density, sphere_drag_coeff
603
- )
604
- st.plotly_chart(fig, use_container_width=True, key="2d_motion_plot_unique")
605
 
606
- # Show model comparison if using sphere
607
- if is_sphere and 'mass_g' in info:
608
- st.markdown("### 📊 Sphere vs Point Mass Comparison")
609
-
610
- @st.cache_data(show_spinner=False)
611
- def create_2d_comparison_plot_unique(tab_id, launch_speed, launch_angle, launch_height, gravity, air_resistance_enabled, drag_coeff, sphere_radius, sphere_density, sphere_drag_coeff, mass_g):
612
- """Tab-specific 2D comparison plot"""
613
- # Create comparison plot
614
- fig_comp = go.Figure()
615
-
616
- # Plot sphere model
617
- motion_sphere = Motion2D(
618
- launch_speed=launch_speed, launch_angle=launch_angle,
619
- launch_height=launch_height, gravity=gravity,
620
- air_resistance=air_resistance_enabled, drag_coefficient=drag_coeff,
621
- is_sphere=True, sphere_radius=sphere_radius,
622
- sphere_density=sphere_density, sphere_drag_coeff=sphere_drag_coeff
623
- )
624
- data_sphere = motion_sphere.trajectory_data(motion_sphere.calculate_flight_time())
625
- fig_comp.add_trace(go.Scatter(
626
- x=data_sphere['x'],
627
- y=data_sphere['y'],
628
- mode='lines',
629
- name=f'Sphere Model ({mass_g:.0f}g)',
630
- line=dict(color='red', width=3),
631
- hovertemplate='<b>Sphere</b><br>X: %{x:.1f} m<br>Y: %{y:.1f} m<extra></extra>'
632
- ))
633
-
634
- # Plot equivalent point mass
635
- motion_point = Motion2D(
636
- launch_speed=launch_speed, launch_angle=launch_angle,
637
- launch_height=launch_height, gravity=gravity,
638
- air_resistance=air_resistance_enabled, drag_coefficient=drag_coeff,
639
- is_sphere=False
640
- )
641
- data_point = motion_point.trajectory_data(motion_point.calculate_flight_time())
642
- fig_comp.add_trace(go.Scatter(
643
- x=data_point['x'],
644
- y=data_point['y'],
645
- mode='lines',
646
- name='Point Mass Model',
647
- line=dict(color='blue', width=2, dash='dash'),
648
- hovertemplate='<b>Point Mass</b><br>X: %{x:.1f} m<br>Y: %{y:.1f} m<extra></extra>'
649
- ))
650
-
651
- # Update layout for comparison plot
652
- fig_comp.update_layout(
653
- title="Sphere Model vs Point Mass Model",
654
- xaxis_title="Horizontal Position (m)",
655
- yaxis_title="Vertical Position (m)",
656
- showlegend=True,
657
- template='plotly_white',
658
- height=400
659
- )
660
-
661
- fig_comp.update_yaxes(range=[0, None])
662
- fig_comp.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.3)')
663
- fig_comp.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.3)')
664
-
665
- return fig_comp
666
-
667
- fig_comp = create_2d_comparison_plot_unique(
668
- "2d_comparison", launch_speed, launch_angle, launch_height, gravity,
669
- air_resistance_enabled, drag_coeff, sphere_radius, sphere_density,
670
- sphere_drag_coeff, info['mass_g']
671
- )
672
- st.plotly_chart(fig_comp, use_container_width=True, key="2d_comparison_plot_unique")
673
-
674
-
675
- def show_motion_comparison():
676
- st.markdown('<div class="section-header">📊 Compare Different Trajectories</div>', unsafe_allow_html=True)
677
-
678
- st.markdown("**Compare up to 3 different projectile motions side by side**")
679
-
680
- # Use container with unique identification for comparison section
681
- with st.container(key="comparison_container"):
682
- # Use columns instead of tabs to avoid tab-related flickering
683
- col1, col2, col3 = st.columns(3)
684
-
685
- with col1:
686
- st.subheader("🚀 Trajectory 1")
687
- speed1 = st.slider("Speed 1", 5.0, 50.0, 20.0, key="comp_speed1")
688
- angle1 = st.slider("Angle 1", 0.0, 90.0, 30.0, key="comp_angle1")
689
- height1 = st.slider("Height 1", 0.0, 30.0, 0.0, key="comp_height1")
690
-
691
- with col2:
692
- st.subheader("🎯 Trajectory 2")
693
- speed2 = st.slider("Speed 2", 5.0, 50.0, 25.0, key="comp_speed2")
694
- angle2 = st.slider("Angle 2", 0.0, 90.0, 45.0, key="comp_angle2")
695
- height2 = st.slider("Height 2", 0.0, 30.0, 0.0, key="comp_height2")
696
-
697
- with col3:
698
- st.subheader("⚽ Trajectory 3")
699
- speed3 = st.slider("Speed 3", 5.0, 50.0, 30.0, key="comp_speed3")
700
- angle3 = st.slider("Angle 3", 0.0, 90.0, 60.0, key="comp_angle3")
701
- height3 = st.slider("Height 3", 0.0, 30.0, 5.0, key="comp_height3")
702
-
703
- # Single plot and table section
704
- st.markdown("---")
705
-
706
- # Create UNIQUE comparison plot function that can't conflict with 2D motion
707
- @st.cache_data(show_spinner=False)
708
- def create_trajectory_comparison_plot_unique(tab_id, speed1, angle1, height1, speed2, angle2, height2, speed3, angle3, height3):
709
- """UNIQUE comparison plot function - completely separate from 2D motion"""
710
- trajectories_data = [
711
- ("Trajectory 1", Motion2D(launch_speed=speed1, launch_angle=angle1, launch_height=height1), "blue"),
712
- ("Trajectory 2", Motion2D(launch_speed=speed2, launch_angle=angle2, launch_height=height2), "red"),
713
- ("Trajectory 3", Motion2D(launch_speed=speed3, launch_angle=angle3, launch_height=height3), "green")
714
- ]
715
-
716
- fig = go.Figure()
717
-
718
- for name, motion, color in trajectories_data:
719
- data = motion.trajectory_data(motion.calculate_flight_time())
720
 
721
- # Add trajectory line
722
- fig.add_trace(go.Scatter(
723
- x=data['x'],
724
- y=data['y'],
 
725
  mode='lines',
726
- name=name,
727
- line=dict(color=color, width=3),
728
- hovertemplate=f'<b>{name}</b><br>X: %{{x:.1f}} m<br>Y: %{{y:.1f}} m<extra></extra>'
729
  ))
730
 
731
- # Add launch point
732
- fig.add_trace(go.Scatter(
733
- x=[motion.launch_x],
734
- y=[motion.launch_height],
735
- mode='markers',
736
- name=f'{name} Launch',
737
- marker=dict(color=color, size=10, symbol='circle'),
738
- showlegend=False,
739
- hovertemplate=f'<b>{name} Launch</b><br>X: %{{x:.1f}} m<br>Y: %{{y:.1f}} m<extra></extra>'
 
 
 
 
 
 
740
  ))
741
 
742
- # Add landing point
743
- if len(data['x']) > 0:
744
- fig.add_trace(go.Scatter(
745
- x=[data['x'][-1]],
746
- y=[data['y'][-1]],
747
- mode='markers',
748
- name=f'{name} Landing',
749
- marker=dict(color=color, size=10, symbol='square'),
750
- showlegend=False,
751
- hovertemplate=f'<b>{name} Landing</b><br>X: %{{x:.1f}} m<br>Y: %{{y:.1f}} m<extra></extra>'
752
- ))
753
-
754
- # Update layout
755
- fig.update_layout(
756
- title=dict(text="Multi-Trajectory Comparison", font=dict(size=18, color="black")),
757
- xaxis_title="Horizontal Position (m)",
758
- yaxis_title="Vertical Position (m)",
759
- showlegend=True,
760
- hovermode='closest',
761
- template='plotly_white',
762
- height=600,
763
- margin=dict(l=50, r=50, t=80, b=50)
764
- )
765
-
766
- # Set axis ranges
767
- fig.update_xaxes(range=[0, None])
768
- fig.update_yaxes(range=[0, None])
769
-
770
- # Add grid
771
- fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.3)')
772
- fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.3)')
773
-
774
- return fig
775
-
776
- # Create cached comparison data with unique function name
777
- @st.cache_data(show_spinner=False)
778
- def create_trajectory_comparison_data_unique(tab_id, speed1, angle1, height1, speed2, angle2, height2, speed3, angle3, height3):
779
- """UNIQUE comparison data function"""
780
- trajectories_data = [
781
- ("Trajectory 1", Motion2D(launch_speed=speed1, launch_angle=angle1, launch_height=height1)),
782
- ("Trajectory 2", Motion2D(launch_speed=speed2, launch_angle=angle2, launch_height=height2)),
783
- ("Trajectory 3", Motion2D(launch_speed=speed3, launch_angle=angle3, launch_height=height3))
784
- ]
785
-
786
- comparison_data = []
787
- for name, motion in trajectories_data:
788
- info = motion.get_launch_info()
789
- comparison_data.append({
790
- "Trajectory": name,
791
- "Speed (m/s)": f"{info['launch_speed']:.1f}",
792
- "Angle (°)": f"{info['launch_angle']:.1f}",
793
- "Height (m)": f"{info['launch_height']:.1f}",
794
- "Flight Time (s)": f"{info['flight_time']:.2f}",
795
- "Range (m)": f"{info['range']:.1f}",
796
- "Max Height (m)": f"{info['max_height']:.1f}"
797
- })
798
-
799
- return comparison_data
800
-
801
- # Display Plotly chart with completely unique identifiers
802
- fig = create_trajectory_comparison_plot_unique("comparison_tab", speed1, angle1, height1, speed2, angle2, height2, speed3, angle3, height3)
803
- st.plotly_chart(fig, use_container_width=True, key="trajectory_comparison_plot_unique")
804
-
805
- # Display comparison table
806
- st.markdown("### 📋 Trajectory Comparison Table")
807
- comparison_data = create_trajectory_comparison_data_unique("comparison_tab", speed1, angle1, height1, speed2, angle2, height2, speed3, angle3, height3)
808
- st.table(comparison_data)
809
 
810
  if __name__ == "__main__":
811
  main()
 
1
  import streamlit as st
2
  import numpy as np
3
  import plotly.graph_objects as go
 
4
  from plotly.subplots import make_subplots
5
  from kinematics_visualizer import Motion1D, Motion2D, KinematicsVisualizer
6
 
 
62
  def main():
63
  st.markdown('<div class="main-header">🚀 Physics Tutorial: Kinematics</div>', unsafe_allow_html=True)
64
 
65
+ # Sidebar for navigation
66
  st.sidebar.title("📚 Select Tutorial")
67
+ tutorial_type = st.sidebar.radio(
68
  "Choose a physics concept:",
69
+ ["1D Motion", "2D Projectile Motion"]
70
  )
71
 
 
72
  if tutorial_type == "1D Motion":
 
73
  show_1d_motion()
74
  elif tutorial_type == "2D Projectile Motion":
 
75
  show_2d_motion()
 
 
 
76
 
77
+ def create_1d_motion_plot_plotly(motion: Motion1D, duration: float, title: str):
78
  """Create 1D motion plots using Plotly"""
79
  # Generate time arrays
80
  t, x, v, a = motion.time_arrays(duration, dt=0.01)
 
131
 
132
  return fig
133
 
134
+ def create_2d_trajectory_plot_plotly(motion: Motion2D, title: str):
135
  """Create 2D trajectory plot using Plotly"""
136
  flight_time = motion.calculate_flight_time()
137
  data = motion.trajectory_data(flight_time)
 
170
  ))
171
 
172
  # Add maximum height point
173
+ if len(data['y']) > 0:
174
+ max_height_idx = np.argmax(data['y'])
175
+ if max_height_idx > 0:
176
+ fig.add_trace(go.Scatter(
177
+ x=[data['x'][max_height_idx]],
178
+ y=[data['y'][max_height_idx]],
179
+ mode='markers',
180
+ name='Max Height',
181
+ marker=dict(color='orange', size=10, symbol='triangle-up'),
182
+ hovertemplate='Max Height<br>X: %{x:.1f} m<br>Y: %{y:.1f} m<extra></extra>'
183
+ ))
184
 
185
  # Update layout
186
  fig.update_layout(
 
195
  )
196
 
197
  # Set axis ranges
198
+ if len(data['x']) > 0 and len(data['y']) > 0:
199
+ fig.update_xaxes(range=[0, max(data['x']) * 1.1])
200
+ fig.update_yaxes(range=[0, max(data['y']) * 1.2])
201
+ else:
202
+ fig.update_xaxes(range=[0, 10])
203
+ fig.update_yaxes(range=[0, 10])
204
 
205
  # Add grid
206
  fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.3)')
 
211
  def show_1d_motion():
212
  st.markdown('<div class="section-header">📏 One-Dimensional Motion</div>', unsafe_allow_html=True)
213
 
214
+ # Physics equations display
215
+ with st.expander("📖 Physics Equations (Click to expand)", expanded=False):
216
+ st.markdown("""
217
+ <div class="physics-equation">
218
+ <h4>Kinematic Equations for Constant Acceleration:</h4>
219
+ <ul>
220
+ <li><strong>Position:</strong> x(t) = x₀ + v₀t + ½at²</li>
221
+ <li><strong>Velocity:</strong> v(t) = v₀ + at</li>
222
+ <li><strong>Acceleration:</strong> a(t) = constant</li>
223
+ </ul>
224
+ <p><strong>Where:</strong></p>
225
+ <ul>
226
+ <li>x₀ = initial position (m)</li>
227
+ <li>v₀ = initial velocity (m/s)</li>
228
+ <li>a = acceleration (m/s²)</li>
229
+ <li>t = time (s)</li>
230
+ </ul>
231
+ </div>
232
+ """, unsafe_allow_html=True)
233
+
234
+ # Create two columns for controls and results
235
+ col1, col2 = st.columns([1, 2])
236
+
237
+ with col1:
238
+ st.markdown('<div class="section-header">🎛️ Control Parameters</div>', unsafe_allow_html=True)
239
+
240
+ # Parameter sliders
241
+ initial_pos = st.slider(
242
+ "Initial Position (m)",
243
+ min_value=-50.0, max_value=50.0, value=0.0, step=1.0,
244
+ help="Starting position of the object"
245
+ )
246
+
247
+ initial_vel = st.slider(
248
+ "Initial Velocity (m/s)",
249
+ min_value=-30.0, max_value=30.0, value=5.0, step=1.0,
250
+ help="Starting velocity of the object"
251
+ )
252
+
253
+ acceleration = st.slider(
254
+ "Acceleration (m/s²)",
255
+ min_value=-15.0, max_value=15.0, value=2.0, step=0.5,
256
+ help="Constant acceleration (positive = speeding up in positive direction)"
257
+ )
258
+
259
+ duration = st.slider(
260
+ "Simulation Duration (s)",
261
+ min_value=1.0, max_value=20.0, value=10.0, step=0.5,
262
+ help="How long to run the simulation"
263
+ )
264
+
265
+ # Display current parameters
266
+ st.markdown('<div class="parameter-box">', unsafe_allow_html=True)
267
+ st.markdown("**Current Parameters:**")
268
+ st.write(f"• Initial Position: {initial_pos:.1f} m")
269
+ st.write(f"• Initial Velocity: {initial_vel:.1f} m/s")
270
+ st.write(f"• Acceleration: {acceleration:.1f} m/s²")
271
+ st.write(f"• Duration: {duration:.1f} s")
272
+
273
+ # Calculate final values
274
+ final_pos = initial_pos + initial_vel * duration + 0.5 * acceleration * duration**2
275
+ final_vel = initial_vel + acceleration * duration
276
+
277
+ st.markdown("**Final Values:**")
278
+ st.write(f"• Final Position: {final_pos:.1f} m")
279
+ st.write(f"• Final Velocity: {final_vel:.1f} m/s")
280
+ st.markdown('</div>', unsafe_allow_html=True)
281
+
282
+ with col2:
283
+ # Create and display motion
284
+ motion = Motion1D(
285
+ initial_position=initial_pos,
286
+ initial_velocity=initial_vel,
287
+ acceleration=acceleration
288
+ )
289
+
290
+ # Generate title based on motion type
291
+ if acceleration > 0:
292
+ motion_type = "Accelerating Motion"
293
+ elif acceleration < 0:
294
+ motion_type = "Decelerating Motion"
295
+ else:
296
+ motion_type = "Constant Velocity Motion"
297
+
298
+ # Create and display Plotly chart
299
+ fig = create_1d_motion_plot_plotly(motion, duration, motion_type)
300
+ st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  def show_2d_motion():
303
  st.markdown('<div class="section-header">🎯 Two-Dimensional Projectile Motion</div>', unsafe_allow_html=True)
 
552
  del st.session_state[key]
553
  st.rerun()
554
 
555
+ with col2:
556
+ # Create and display trajectory with model info
557
+ model_info = f"({'Sphere' if is_sphere else 'Point Mass'})"
558
+ air_info = " (with Air Resistance)" if air_resistance_enabled else " (No Air Resistance)"
559
+ trajectory_title = f"Projectile Motion - {launch_angle:.0f}° Launch {model_info}{air_info}"
560
+
561
+ # Create and display main trajectory plot
562
+ fig = create_2d_trajectory_plot_plotly(motion, trajectory_title)
563
+ st.plotly_chart(fig, use_container_width=True)
564
+
565
+ # Show model comparison if using sphere
566
+ if is_sphere and 'mass_g' in info:
567
+ st.markdown("### 📊 Sphere vs Point Mass Comparison")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
 
569
+ # Create comparison plot using Plotly
570
+ fig_comp = go.Figure()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
+ # Plot sphere model
573
+ data_sphere = motion.trajectory_data(motion.calculate_flight_time())
574
+ fig_comp.add_trace(go.Scatter(
575
+ x=data_sphere['x'],
576
+ y=data_sphere['y'],
577
  mode='lines',
578
+ name=f'Sphere Model ({info["mass_g"]:.0f}g)',
579
+ line=dict(color='red', width=3),
580
+ hovertemplate='<b>Sphere</b><br>X: %{x:.1f} m<br>Y: %{y:.1f} m<extra></extra>'
581
  ))
582
 
583
+ # Plot equivalent point mass
584
+ motion_point = Motion2D(
585
+ launch_speed=launch_speed, launch_angle=launch_angle,
586
+ launch_height=launch_height, gravity=gravity,
587
+ air_resistance=air_resistance_enabled, drag_coefficient=drag_coeff,
588
+ is_sphere=False
589
+ )
590
+ data_point = motion_point.trajectory_data(motion_point.calculate_flight_time())
591
+ fig_comp.add_trace(go.Scatter(
592
+ x=data_point['x'],
593
+ y=data_point['y'],
594
+ mode='lines',
595
+ name='Point Mass Model',
596
+ line=dict(color='blue', width=2, dash='dash'),
597
+ hovertemplate='<b>Point Mass</b><br>X: %{x:.1f} m<br>Y: %{y:.1f} m<extra></extra>'
598
  ))
599
 
600
+ # Update layout for comparison plot
601
+ fig_comp.update_layout(
602
+ title="Sphere Model vs Point Mass Model",
603
+ xaxis_title="Horizontal Position (m)",
604
+ yaxis_title="Vertical Position (m)",
605
+ showlegend=True,
606
+ template='plotly_white',
607
+ height=400
608
+ )
609
+
610
+ fig_comp.update_yaxes(range=[0, None])
611
+ fig_comp.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.3)')
612
+ fig_comp.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.3)')
613
+
614
+ st.plotly_chart(fig_comp, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
 
616
  if __name__ == "__main__":
617
  main()