Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +118 -68
src/streamlit_app.py
CHANGED
|
@@ -3,6 +3,13 @@ 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",
|
|
@@ -68,6 +75,11 @@ def main():
|
|
| 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":
|
|
@@ -473,86 +485,124 @@ def show_motion_comparison():
|
|
| 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 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
|
|
|
| 491 |
|
| 492 |
with tab2:
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
|
|
|
| 502 |
|
| 503 |
with tab3:
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 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 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
|
| 555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
|
| 557 |
if __name__ == "__main__":
|
| 558 |
main()
|
|
|
|
| 3 |
import matplotlib.pyplot as plt
|
| 4 |
from kinematics_visualizer import Motion1D, Motion2D, KinematicsVisualizer
|
| 5 |
|
| 6 |
+
def clear_comparison_state():
|
| 7 |
+
"""Clear any lingering comparison state"""
|
| 8 |
+
keys_to_clear = [key for key in st.session_state.keys() if key.startswith('comp_')]
|
| 9 |
+
for key in keys_to_clear:
|
| 10 |
+
if key in st.session_state:
|
| 11 |
+
del st.session_state[key]
|
| 12 |
+
|
| 13 |
# Configure Streamlit page
|
| 14 |
st.set_page_config(
|
| 15 |
page_title="Physics Tutorial: Kinematics",
|
|
|
|
| 75 |
["1D Motion", "2D Projectile Motion", "Compare Motions"]
|
| 76 |
)
|
| 77 |
|
| 78 |
+
# Clear comparison state when not on comparison tab
|
| 79 |
+
if tutorial_type != "Compare Motions":
|
| 80 |
+
clear_comparison_state()
|
| 81 |
+
plt.close('all') # Close any lingering plots
|
| 82 |
+
|
| 83 |
if tutorial_type == "1D Motion":
|
| 84 |
show_1d_motion()
|
| 85 |
elif tutorial_type == "2D Projectile Motion":
|
|
|
|
| 485 |
|
| 486 |
st.markdown("**Compare up to 3 different projectile motions side by side**")
|
| 487 |
|
| 488 |
+
# Clear any previous matplotlib figures explicitly
|
| 489 |
+
plt.clf()
|
| 490 |
+
plt.close('all')
|
| 491 |
+
|
| 492 |
# Create tabs for different trajectories
|
| 493 |
tab1, tab2, tab3 = st.tabs(["🚀 Trajectory 1", "🎯 Trajectory 2", "⚽ Trajectory 3"])
|
| 494 |
|
| 495 |
trajectories = []
|
| 496 |
|
| 497 |
+
# Use containers to isolate each tab's content
|
| 498 |
with tab1:
|
| 499 |
+
with st.container():
|
| 500 |
+
col1, col2 = st.columns(2)
|
| 501 |
+
with col1:
|
| 502 |
+
speed1 = st.slider("Speed 1 (m/s)", 5.0, 50.0, 20.0, key="comp_speed1")
|
| 503 |
+
angle1 = st.slider("Angle 1 (°)", 0.0, 90.0, 30.0, key="comp_angle1")
|
| 504 |
+
with col2:
|
| 505 |
+
height1 = st.slider("Height 1 (m)", 0.0, 30.0, 0.0, key="comp_height1")
|
| 506 |
+
|
| 507 |
+
motion1 = Motion2D(launch_speed=speed1, launch_angle=angle1, launch_height=height1)
|
| 508 |
+
trajectories.append(("Trajectory 1", motion1, 'blue'))
|
| 509 |
|
| 510 |
with tab2:
|
| 511 |
+
with st.container():
|
| 512 |
+
col1, col2 = st.columns(2)
|
| 513 |
+
with col1:
|
| 514 |
+
speed2 = st.slider("Speed 2 (m/s)", 5.0, 50.0, 25.0, key="comp_speed2")
|
| 515 |
+
angle2 = st.slider("Angle 2 (°)", 0.0, 90.0, 45.0, key="comp_angle2")
|
| 516 |
+
with col2:
|
| 517 |
+
height2 = st.slider("Height 2 (m)", 0.0, 30.0, 0.0, key="comp_height2")
|
| 518 |
+
|
| 519 |
+
motion2 = Motion2D(launch_speed=speed2, launch_angle=angle2, launch_height=height2)
|
| 520 |
+
trajectories.append(("Trajectory 2", motion2, 'red'))
|
| 521 |
|
| 522 |
with tab3:
|
| 523 |
+
with st.container():
|
| 524 |
+
col1, col2 = st.columns(2)
|
| 525 |
+
with col1:
|
| 526 |
+
speed3 = st.slider("Speed 3 (m/s)", 5.0, 50.0, 30.0, key="comp_speed3")
|
| 527 |
+
angle3 = st.slider("Angle 3 (°)", 0.0, 90.0, 60.0, key="comp_angle3")
|
| 528 |
+
with col2:
|
| 529 |
+
height3 = st.slider("Height 3 (m)", 0.0, 30.0, 5.0, key="comp_height3")
|
| 530 |
+
|
| 531 |
+
motion3 = Motion2D(launch_speed=speed3, launch_angle=angle3, launch_height=height3)
|
| 532 |
+
trajectories.append(("Trajectory 3", motion3, 'green'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
|
| 534 |
+
# Create comparison plot in its own container
|
| 535 |
+
with st.container():
|
| 536 |
+
try:
|
| 537 |
+
# Ensure we have a clean figure
|
| 538 |
+
plt.close('all')
|
| 539 |
+
fig, ax = plt.subplots(figsize=(12, 8))
|
| 540 |
+
|
| 541 |
+
max_range = 0
|
| 542 |
+
for name, motion, color in trajectories:
|
| 543 |
+
try:
|
| 544 |
+
data = motion.trajectory_data(motion.calculate_flight_time())
|
| 545 |
+
if len(data['x']) > 0: # Check data exists
|
| 546 |
+
ax.plot(data['x'], data['y'], linewidth=3, label=name, color=color)
|
| 547 |
+
|
| 548 |
+
# Mark launch and landing points
|
| 549 |
+
ax.plot(motion.launch_x, motion.launch_height, 'o', markersize=8, color=color, alpha=0.7)
|
| 550 |
+
ax.plot(data['x'][-1], data['y'][-1], 's', markersize=8, color=color, alpha=0.7)
|
| 551 |
+
max_range = max(max_range, data['x'][-1] if data['x'][-1] > 0 else 0)
|
| 552 |
+
except Exception as e:
|
| 553 |
+
st.error(f"Error plotting {name}: {str(e)}")
|
| 554 |
+
continue
|
| 555 |
+
|
| 556 |
+
# Only proceed if we have valid data
|
| 557 |
+
if max_range > 0:
|
| 558 |
+
ax.set_xlabel('Horizontal Position (m)', fontsize=12)
|
| 559 |
+
ax.set_ylabel('Vertical Position (m)', fontsize=12)
|
| 560 |
+
ax.set_title('Trajectory Comparison', fontsize=16, fontweight='bold')
|
| 561 |
+
ax.grid(True, alpha=0.3)
|
| 562 |
+
ax.legend(fontsize=12)
|
| 563 |
+
ax.set_ylim(bottom=0)
|
| 564 |
+
ax.set_xlim(0, max_range * 1.1)
|
| 565 |
+
|
| 566 |
+
# Display plot
|
| 567 |
+
st.pyplot(fig, clear_figure=True) # Important: clear_figure=True
|
| 568 |
+
else:
|
| 569 |
+
st.warning("No valid trajectory data to display")
|
| 570 |
+
|
| 571 |
+
except Exception as e:
|
| 572 |
+
st.error(f"Error creating comparison plot: {str(e)}")
|
| 573 |
+
finally:
|
| 574 |
+
plt.close(fig) # Ensure figure is always closed
|
| 575 |
+
plt.close('all')
|
| 576 |
|
| 577 |
+
# Comparison table in its own container
|
| 578 |
+
with st.container():
|
| 579 |
+
st.markdown("### 📋 Trajectory Comparison Table")
|
| 580 |
+
|
| 581 |
+
try:
|
| 582 |
+
comparison_data = []
|
| 583 |
+
for name, motion, _ in trajectories:
|
| 584 |
+
try:
|
| 585 |
+
info = motion.get_launch_info()
|
| 586 |
+
comparison_data.append({
|
| 587 |
+
"Trajectory": name,
|
| 588 |
+
"Speed (m/s)": f"{info['launch_speed']:.1f}",
|
| 589 |
+
"Angle (°)": f"{info['launch_angle']:.1f}",
|
| 590 |
+
"Height (m)": f"{info['launch_height']:.1f}",
|
| 591 |
+
"Flight Time (s)": f"{info['flight_time']:.2f}",
|
| 592 |
+
"Range (m)": f"{info['range']:.1f}",
|
| 593 |
+
"Max Height (m)": f"{info['max_height']:.1f}"
|
| 594 |
+
})
|
| 595 |
+
except Exception as e:
|
| 596 |
+
st.error(f"Error getting info for {name}: {str(e)}")
|
| 597 |
+
continue
|
| 598 |
+
|
| 599 |
+
if comparison_data:
|
| 600 |
+
st.table(comparison_data)
|
| 601 |
+
else:
|
| 602 |
+
st.warning("No trajectory data available for comparison table")
|
| 603 |
+
|
| 604 |
+
except Exception as e:
|
| 605 |
+
st.error(f"Error creating comparison table: {str(e)}")
|
| 606 |
|
| 607 |
if __name__ == "__main__":
|
| 608 |
main()
|