Commit
·
dfc428f
1
Parent(s):
cc90294
Added new folder
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- pages/ac-vs-dc.py +369 -0
- pages/air-resistance.py +159 -0
- pages/angular-momentum.py +114 -0
- pages/atomic-structure.py +148 -0
- pages/basic-circuit-analysis.py +388 -0
- pages/blackbody-radiation.py +85 -0
- pages/capacitance.py +65 -0
- pages/center-of-gravity.py +83 -0
- pages/center-of-mass.py +75 -0
- pages/centripetal-force.py +59 -0
- pages/chaos-theory.py +50 -0
- pages/conservation-of-angular-momentum.py +66 -0
- pages/conservation-of-energy.py +137 -0
- pages/conservation-of-momentum.py +110 -0
- pages/coulombs-law.py +61 -0
- pages/diffraction.py +56 -0
- pages/dimensional-analysis.py +110 -0
- pages/distance.py +142 -0
- pages/electic-charge.py +86 -0
- pages/electic-field.py +102 -0
- pages/electomagnetic-induction.py +298 -0
- pages/electric-potential.py +49 -0
- pages/electromagnetc-waves.py +42 -0
- pages/electromagnetic-spectrum.py +89 -0
- pages/energy.py +235 -0
- pages/farady-law.py +116 -0
- pages/field-concepts.py +131 -0
- pages/first-law-of-thermodynamics.py +52 -0
- pages/fluid-dynamics.py +46 -0
- pages/fluid-statics.py +45 -0
- pages/force.py +131 -0
- pages/free-fall.py +140 -0
- pages/friction.py +115 -0
- pages/general-relativity.py +56 -0
- pages/gravity.py +161 -0
- pages/heat.py +47 -0
- pages/ideal-gas-law.py +40 -0
- pages/impulse.py +122 -0
- pages/inertia.py +111 -0
- pages/interference.py +68 -0
- pages/kinetic-theory-of-gas.py +91 -0
- pages/lenz-law.py +112 -0
- pages/light-wave.py +70 -0
- pages/magnetic-field.py +258 -0
- pages/mass-energy-equivalence.py +42 -0
- pages/mass.py +122 -0
- pages/maxwells-equations.py +145 -0
- pages/mechanical-advantage.py +63 -0
- pages/moment-of-inertia.py +78 -0
- pages/momentum.py +151 -0
pages/ac-vs-dc.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from matplotlib.animation import FuncAnimation
|
| 5 |
+
import io
|
| 6 |
+
from PIL import Image
|
| 7 |
+
|
| 8 |
+
# Set page config
|
| 9 |
+
st.set_page_config(page_title="AC vs DC Electricity Visualization", layout="wide")
|
| 10 |
+
|
| 11 |
+
# Title and description
|
| 12 |
+
st.title("AC vs DC Electricity Visualization")
|
| 13 |
+
st.write("""
|
| 14 |
+
This simulation demonstrates the key differences between Alternating Current (AC) and Direct Current (DC).
|
| 15 |
+
- **AC**: Current periodically changes direction, voltage oscillates between positive and negative values
|
| 16 |
+
- **DC**: Current flows in a single direction, voltage remains constant
|
| 17 |
+
""")
|
| 18 |
+
|
| 19 |
+
# Sidebar controls
|
| 20 |
+
st.sidebar.header("Simulation Controls")
|
| 21 |
+
|
| 22 |
+
# AC parameters
|
| 23 |
+
st.sidebar.subheader("AC Parameters")
|
| 24 |
+
ac_amplitude = st.sidebar.slider("AC Amplitude (Volts)", 1.0, 10.0, 5.0, 0.1)
|
| 25 |
+
ac_frequency = st.sidebar.slider("AC Frequency (Hz)", 0.1, 2.0, 1.0, 0.1)
|
| 26 |
+
|
| 27 |
+
# DC parameters
|
| 28 |
+
st.sidebar.subheader("DC Parameters")
|
| 29 |
+
dc_voltage = st.sidebar.slider("DC Voltage (Volts)", 0.0, 10.0, 5.0, 0.1)
|
| 30 |
+
|
| 31 |
+
# Animation speed
|
| 32 |
+
animation_speed = st.sidebar.slider("Animation Speed", 0.1, 2.0, 1.0, 0.1)
|
| 33 |
+
|
| 34 |
+
# Time parameters
|
| 35 |
+
duration = 5 # seconds to simulate
|
| 36 |
+
fps = 30 # frames per second
|
| 37 |
+
|
| 38 |
+
# Function to create the simulation animation
|
| 39 |
+
def create_animation(ac_amplitude, ac_frequency, dc_voltage, duration, fps, animation_speed):
|
| 40 |
+
# Create figure and axes
|
| 41 |
+
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12))
|
| 42 |
+
plt.tight_layout(pad=4.0)
|
| 43 |
+
|
| 44 |
+
# Time array
|
| 45 |
+
t = np.linspace(0, duration, int(duration * fps))
|
| 46 |
+
dt = t[1] - t[0]
|
| 47 |
+
|
| 48 |
+
# Calculate the AC and DC signals
|
| 49 |
+
ac_signal = ac_amplitude * np.sin(2 * np.pi * ac_frequency * t)
|
| 50 |
+
dc_signal = np.ones_like(t) * dc_voltage
|
| 51 |
+
|
| 52 |
+
# Set up plots
|
| 53 |
+
# Voltage plots
|
| 54 |
+
ax1.set_xlim(0, duration)
|
| 55 |
+
ax1.set_ylim(-max(ac_amplitude, dc_voltage) * 1.2, max(ac_amplitude, dc_voltage) * 1.2)
|
| 56 |
+
ax1.set_ylabel('Voltage (V)')
|
| 57 |
+
ax1.set_title('Voltage vs Time')
|
| 58 |
+
ax1.grid(True)
|
| 59 |
+
|
| 60 |
+
ac_line, = ax1.plot([], [], 'r-', label='AC')
|
| 61 |
+
dc_line, = ax1.plot([], [], 'b-', label='DC')
|
| 62 |
+
ax1.legend(loc='upper right')
|
| 63 |
+
|
| 64 |
+
# AC electron flow
|
| 65 |
+
ax2.set_xlim(-1, 1)
|
| 66 |
+
ax2.set_ylim(-1, 1)
|
| 67 |
+
ax2.set_aspect('equal')
|
| 68 |
+
ax2.set_title('AC Electron Flow')
|
| 69 |
+
ax2.grid(True)
|
| 70 |
+
ax2.set_xticks([])
|
| 71 |
+
ax2.set_yticks([])
|
| 72 |
+
|
| 73 |
+
# Draw a wire for AC
|
| 74 |
+
wire_length = 1.8
|
| 75 |
+
wire_thickness = 0.1
|
| 76 |
+
wire_rect = plt.Rectangle((-wire_length/2, -wire_thickness/2), wire_length, wire_thickness,
|
| 77 |
+
fc='gray', ec='black')
|
| 78 |
+
ax2.add_patch(wire_rect)
|
| 79 |
+
|
| 80 |
+
# Electrons for AC
|
| 81 |
+
num_electrons = 20
|
| 82 |
+
ac_electrons = []
|
| 83 |
+
for i in range(num_electrons):
|
| 84 |
+
electron = plt.Circle((0, 0), 0.03, fc='blue', ec='black')
|
| 85 |
+
ax2.add_patch(electron)
|
| 86 |
+
ac_electrons.append(electron)
|
| 87 |
+
|
| 88 |
+
# DC electron flow
|
| 89 |
+
ax3.set_xlim(-1, 1)
|
| 90 |
+
ax3.set_ylim(-1, 1)
|
| 91 |
+
ax3.set_aspect('equal')
|
| 92 |
+
ax3.set_title('DC Electron Flow')
|
| 93 |
+
ax3.grid(True)
|
| 94 |
+
ax3.set_xticks([])
|
| 95 |
+
ax3.set_yticks([])
|
| 96 |
+
|
| 97 |
+
# Draw a wire for DC
|
| 98 |
+
wire_rect = plt.Rectangle((-wire_length/2, -wire_thickness/2), wire_length, wire_thickness,
|
| 99 |
+
fc='gray', ec='black')
|
| 100 |
+
ax3.add_patch(wire_rect)
|
| 101 |
+
|
| 102 |
+
# Electrons for DC
|
| 103 |
+
dc_electrons = []
|
| 104 |
+
for i in range(num_electrons):
|
| 105 |
+
electron = plt.Circle((0, 0), 0.03, fc='blue', ec='black')
|
| 106 |
+
ax3.add_patch(electron)
|
| 107 |
+
dc_electrons.append(electron)
|
| 108 |
+
|
| 109 |
+
# Initialization function for animation
|
| 110 |
+
def init():
|
| 111 |
+
ac_line.set_data([], [])
|
| 112 |
+
dc_line.set_data([], [])
|
| 113 |
+
|
| 114 |
+
# Initialize AC electrons positions
|
| 115 |
+
for i, electron in enumerate(ac_electrons):
|
| 116 |
+
pos = -wire_length/2 + i * wire_length / (num_electrons - 1)
|
| 117 |
+
electron.center = (pos, 0)
|
| 118 |
+
|
| 119 |
+
# Initialize DC electrons positions
|
| 120 |
+
for i, electron in enumerate(dc_electrons):
|
| 121 |
+
pos = -wire_length/2 + i * wire_length / (num_electrons - 1)
|
| 122 |
+
electron.center = (pos, 0)
|
| 123 |
+
|
| 124 |
+
return ac_line, dc_line, *ac_electrons, *dc_electrons
|
| 125 |
+
|
| 126 |
+
# Animation function
|
| 127 |
+
def animate(frame):
|
| 128 |
+
frame = int(frame * animation_speed) % len(t)
|
| 129 |
+
current_time = t[:frame+1]
|
| 130 |
+
|
| 131 |
+
# Update voltage plots
|
| 132 |
+
ac_line.set_data(current_time, ac_signal[:frame+1])
|
| 133 |
+
dc_line.set_data(current_time, dc_signal[:frame+1])
|
| 134 |
+
|
| 135 |
+
# Update AC electron positions
|
| 136 |
+
ac_current_value = ac_signal[frame]
|
| 137 |
+
ac_speed = ac_current_value / ac_amplitude # Normalized speed factor
|
| 138 |
+
|
| 139 |
+
for i, electron in enumerate(ac_electrons):
|
| 140 |
+
# Base position
|
| 141 |
+
base_pos = -wire_length/2 + i * wire_length / (num_electrons - 1)
|
| 142 |
+
# Add oscillation based on AC signal
|
| 143 |
+
oscillation = 0.05 * np.sin(2 * np.pi * ac_frequency * t[frame] - i * np.pi / 10)
|
| 144 |
+
electron.center = (base_pos + oscillation, 0)
|
| 145 |
+
|
| 146 |
+
# Change electron color based on direction
|
| 147 |
+
if ac_current_value > 0:
|
| 148 |
+
electron.set_facecolor('blue')
|
| 149 |
+
else:
|
| 150 |
+
electron.set_facecolor('red')
|
| 151 |
+
|
| 152 |
+
# Update DC electron positions
|
| 153 |
+
for i, electron in enumerate(dc_electrons):
|
| 154 |
+
# Base position plus movement based on time
|
| 155 |
+
pos = (-wire_length/2 + (i + frame/20 * animation_speed) % num_electrons * wire_length / num_electrons)
|
| 156 |
+
if pos > wire_length/2:
|
| 157 |
+
pos = -wire_length/2
|
| 158 |
+
electron.center = (pos, 0)
|
| 159 |
+
|
| 160 |
+
return ac_line, dc_line, *ac_electrons, *dc_electrons
|
| 161 |
+
|
| 162 |
+
# Create animation
|
| 163 |
+
anim = FuncAnimation(fig, animate, frames=len(t), init_func=init, blit=True)
|
| 164 |
+
|
| 165 |
+
plt.tight_layout()
|
| 166 |
+
return anim, fig
|
| 167 |
+
|
| 168 |
+
# Create separate plots for static visualization instead of animation
|
| 169 |
+
def create_static_plots():
|
| 170 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
|
| 171 |
+
|
| 172 |
+
# Time array
|
| 173 |
+
t = np.linspace(0, duration, 1000)
|
| 174 |
+
|
| 175 |
+
# Calculate the AC and DC signals
|
| 176 |
+
ac_signal = ac_amplitude * np.sin(2 * np.pi * ac_frequency * t)
|
| 177 |
+
dc_signal = np.ones_like(t) * dc_voltage
|
| 178 |
+
|
| 179 |
+
# AC plot
|
| 180 |
+
ax1.plot(t, ac_signal, 'r-', label=f'AC ({ac_frequency} Hz, {ac_amplitude}V)')
|
| 181 |
+
ax1.set_ylabel('Voltage (V)')
|
| 182 |
+
ax1.set_title('Alternating Current (AC)')
|
| 183 |
+
ax1.grid(True)
|
| 184 |
+
ax1.legend()
|
| 185 |
+
|
| 186 |
+
# DC plot
|
| 187 |
+
ax2.plot(t, dc_signal, 'b-', label=f'DC ({dc_voltage}V)')
|
| 188 |
+
ax2.set_xlabel('Time (s)')
|
| 189 |
+
ax2.set_ylabel('Voltage (V)')
|
| 190 |
+
ax2.set_title('Direct Current (DC)')
|
| 191 |
+
ax2.grid(True)
|
| 192 |
+
ax2.legend()
|
| 193 |
+
|
| 194 |
+
plt.tight_layout()
|
| 195 |
+
return fig
|
| 196 |
+
|
| 197 |
+
# Display static plots
|
| 198 |
+
st.header("Voltage Over Time")
|
| 199 |
+
static_fig = create_static_plots()
|
| 200 |
+
st.pyplot(static_fig)
|
| 201 |
+
|
| 202 |
+
# Visualize electron flow differences
|
| 203 |
+
st.header("Electron Flow Visualization")
|
| 204 |
+
st.write("""
|
| 205 |
+
The visualization below shows how electrons move in AC and DC circuits:
|
| 206 |
+
- In the AC circuit (top), electrons oscillate back and forth, changing direction periodically
|
| 207 |
+
- In the DC circuit (bottom), electrons flow continuously in one direction
|
| 208 |
+
""")
|
| 209 |
+
|
| 210 |
+
# Instead of animation, create a visual representation with multiple frames
|
| 211 |
+
st.write("**Animation visualization replaced with static representation**")
|
| 212 |
+
|
| 213 |
+
# Create frames to show electron movement
|
| 214 |
+
def create_electron_flow_images():
|
| 215 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6))
|
| 216 |
+
|
| 217 |
+
# Set up axes
|
| 218 |
+
for ax, title in zip([ax1, ax2], ['AC Electron Flow', 'DC Electron Flow']):
|
| 219 |
+
ax.set_xlim(-1, 1)
|
| 220 |
+
ax.set_ylim(-1, 1)
|
| 221 |
+
ax.set_aspect('equal')
|
| 222 |
+
ax.set_title(title)
|
| 223 |
+
ax.grid(True)
|
| 224 |
+
ax.set_xticks([])
|
| 225 |
+
ax.set_yticks([])
|
| 226 |
+
|
| 227 |
+
# Draw wires
|
| 228 |
+
wire_length = 1.8
|
| 229 |
+
wire_thickness = 0.1
|
| 230 |
+
wire_rect = plt.Rectangle((-wire_length/2, -wire_thickness/2), wire_length, wire_thickness,
|
| 231 |
+
fc='gray', ec='black')
|
| 232 |
+
ax.add_patch(wire_rect)
|
| 233 |
+
|
| 234 |
+
plt.tight_layout()
|
| 235 |
+
return fig
|
| 236 |
+
|
| 237 |
+
# Show different phases of electron movement
|
| 238 |
+
phases = ['Phase 1: Initial', 'Phase 2: Movement', 'Phase 3: Direction Change (AC only)']
|
| 239 |
+
|
| 240 |
+
for i, phase in enumerate(phases):
|
| 241 |
+
st.subheader(phase)
|
| 242 |
+
|
| 243 |
+
fig = create_electron_flow_images()
|
| 244 |
+
num_electrons = 10
|
| 245 |
+
|
| 246 |
+
# Get the axes from the figure
|
| 247 |
+
ax1, ax2 = fig.axes
|
| 248 |
+
|
| 249 |
+
# Draw electrons for AC
|
| 250 |
+
for j in range(num_electrons):
|
| 251 |
+
# Base position
|
| 252 |
+
base_pos = -0.9 + j * 1.8 / (num_electrons - 1)
|
| 253 |
+
|
| 254 |
+
# Different positions for different phases
|
| 255 |
+
if i == 0: # Initial phase
|
| 256 |
+
pos = base_pos
|
| 257 |
+
color = 'blue'
|
| 258 |
+
elif i == 1: # Movement phase
|
| 259 |
+
pos = base_pos + 0.1 # Slightly moved to the right
|
| 260 |
+
color = 'blue'
|
| 261 |
+
else: # Direction change (for AC)
|
| 262 |
+
pos = base_pos - 0.1 # Now moved to the left
|
| 263 |
+
color = 'red' # Changed color to indicate reversed direction
|
| 264 |
+
|
| 265 |
+
electron = plt.Circle((pos, 0), 0.03, fc=color, ec='black')
|
| 266 |
+
ax1.add_patch(electron)
|
| 267 |
+
|
| 268 |
+
# Draw electrons for DC (always moving in one direction)
|
| 269 |
+
for j in range(num_electrons):
|
| 270 |
+
base_pos = -0.9 + j * 1.8 / (num_electrons - 1)
|
| 271 |
+
|
| 272 |
+
# Different positions for different phases, but always moving right
|
| 273 |
+
if i == 0: # Initial phase
|
| 274 |
+
pos = base_pos
|
| 275 |
+
elif i == 1: # Movement phase
|
| 276 |
+
pos = base_pos + 0.1 # Moved to the right
|
| 277 |
+
else: # Continued movement
|
| 278 |
+
pos = base_pos + 0.2 # Moved further right
|
| 279 |
+
|
| 280 |
+
# If electron moves beyond wire, wrap around
|
| 281 |
+
if pos > 0.9:
|
| 282 |
+
pos = -0.9 + (pos - 0.9)
|
| 283 |
+
|
| 284 |
+
electron = plt.Circle((pos, 0), 0.03, fc='blue', ec='black')
|
| 285 |
+
ax2.add_patch(electron)
|
| 286 |
+
|
| 287 |
+
st.pyplot(fig)
|
| 288 |
+
|
| 289 |
+
# Explain each phase
|
| 290 |
+
if i == 0:
|
| 291 |
+
st.write("Initial state: Electrons are evenly distributed along both wires.")
|
| 292 |
+
elif i == 1:
|
| 293 |
+
st.write("Both AC and DC electrons begin moving to the right as current flows.")
|
| 294 |
+
else:
|
| 295 |
+
st.write("In AC, the current reverses direction periodically (red electrons now moving left). In DC, electrons continue flowing in the same direction (wrapping around when reaching the end).")
|
| 296 |
+
|
| 297 |
+
# Explanations
|
| 298 |
+
st.header("Key Differences Between AC and DC")
|
| 299 |
+
|
| 300 |
+
col1, col2 = st.columns(2)
|
| 301 |
+
|
| 302 |
+
with col1:
|
| 303 |
+
st.subheader("Alternating Current (AC)")
|
| 304 |
+
st.write("""
|
| 305 |
+
- **Direction**: Periodically changes direction
|
| 306 |
+
- **Waveform**: Typically sinusoidal
|
| 307 |
+
- **Voltage**: Oscillates between positive and negative values
|
| 308 |
+
- **Generation**: Produced by alternators and generators
|
| 309 |
+
- **Transmission**: Can be efficiently transmitted over long distances
|
| 310 |
+
- **Usage**: Power grid, household appliances, transformers
|
| 311 |
+
""")
|
| 312 |
+
|
| 313 |
+
with col2:
|
| 314 |
+
st.subheader("Direct Current (DC)")
|
| 315 |
+
st.write("""
|
| 316 |
+
- **Direction**: Flows in one direction only
|
| 317 |
+
- **Waveform**: Constant (or with minimal variation)
|
| 318 |
+
- **Voltage**: Maintains consistent polarity
|
| 319 |
+
- **Generation**: Produced by batteries, solar cells, and power supplies
|
| 320 |
+
- **Transmission**: Loses power over long distances
|
| 321 |
+
- **Usage**: Electronics, batteries, LED lighting, computers
|
| 322 |
+
""")
|
| 323 |
+
|
| 324 |
+
st.header("Real-World Applications")
|
| 325 |
+
st.write("""
|
| 326 |
+
- **AC** is used for power transmission and distribution systems because its voltage can be easily changed using transformers, allowing for efficient transmission over long distances.
|
| 327 |
+
- **DC** is used in electronic devices, mobile phones, computers, and electric vehicles because these devices require a stable, consistent current.
|
| 328 |
+
- Many devices convert AC to DC using adapters or power supplies. Look at your laptop charger - it converts the AC from the wall outlet to the DC your computer needs!
|
| 329 |
+
""")
|
| 330 |
+
|
| 331 |
+
# Advantages and disadvantages
|
| 332 |
+
st.header("Advantages and Disadvantages")
|
| 333 |
+
|
| 334 |
+
adv_col1, adv_col2 = st.columns(2)
|
| 335 |
+
|
| 336 |
+
with adv_col1:
|
| 337 |
+
st.subheader("AC Advantages")
|
| 338 |
+
st.write("""
|
| 339 |
+
- Easy to transform voltage levels
|
| 340 |
+
- Less energy loss in transmission
|
| 341 |
+
- Easy to generate with rotating machines
|
| 342 |
+
- Self-extinguishing arcs (helpful for circuit breakers)
|
| 343 |
+
""")
|
| 344 |
+
|
| 345 |
+
st.subheader("AC Disadvantages")
|
| 346 |
+
st.write("""
|
| 347 |
+
- More complex circuits needed
|
| 348 |
+
- Skin effect in conductors
|
| 349 |
+
- Inductive and capacitive losses
|
| 350 |
+
- Not suitable for sensitive electronics without conversion
|
| 351 |
+
""")
|
| 352 |
+
|
| 353 |
+
with adv_col2:
|
| 354 |
+
st.subheader("DC Advantages")
|
| 355 |
+
st.write("""
|
| 356 |
+
- Simple to understand and use
|
| 357 |
+
- Steady power delivery
|
| 358 |
+
- No frequency considerations
|
| 359 |
+
- Better for electronic components
|
| 360 |
+
- Less dangerous at equal voltages
|
| 361 |
+
""")
|
| 362 |
+
|
| 363 |
+
st.subheader("DC Disadvantages")
|
| 364 |
+
st.write("""
|
| 365 |
+
- Difficult to change voltage levels
|
| 366 |
+
- Higher transmission losses over distance
|
| 367 |
+
- Cannot use transformers directly
|
| 368 |
+
- Difficult to interrupt (arc issues)
|
| 369 |
+
""")
|
pages/air-resistance.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
class FallingObject:
|
| 6 |
+
def __init__(self, mass, drag_coefficient, cross_sectional_area,
|
| 7 |
+
initial_height, initial_velocity=0, air_density=1.225):
|
| 8 |
+
"""
|
| 9 |
+
Simulate a falling object with air resistance
|
| 10 |
+
|
| 11 |
+
Parameters:
|
| 12 |
+
- mass: mass of the object (kg)
|
| 13 |
+
- drag_coefficient: coefficient of drag
|
| 14 |
+
- cross_sectional_area: frontal area of the object (m²)
|
| 15 |
+
- initial_height: starting height of the object (m)
|
| 16 |
+
- initial_velocity: initial velocity (m/s), default is 0
|
| 17 |
+
- air_density: density of air (kg/m³), default is sea level air density
|
| 18 |
+
"""
|
| 19 |
+
self.mass = mass
|
| 20 |
+
self.drag_coefficient = drag_coefficient
|
| 21 |
+
self.cross_sectional_area = cross_sectional_area
|
| 22 |
+
self.height = initial_height
|
| 23 |
+
self.velocity = initial_velocity
|
| 24 |
+
self.air_density = air_density
|
| 25 |
+
self.gravity = 9.81 # m/s²
|
| 26 |
+
|
| 27 |
+
def calculate_trajectory(self, time_step=0.01, max_time=10):
|
| 28 |
+
"""
|
| 29 |
+
Calculate the trajectory of the falling object
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
- times: list of time points
|
| 33 |
+
- heights: list of corresponding heights
|
| 34 |
+
- velocities: list of corresponding velocities
|
| 35 |
+
"""
|
| 36 |
+
times = [0]
|
| 37 |
+
heights = [self.height]
|
| 38 |
+
velocities = [self.velocity]
|
| 39 |
+
|
| 40 |
+
current_time = 0
|
| 41 |
+
current_height = self.height
|
| 42 |
+
current_velocity = self.velocity
|
| 43 |
+
|
| 44 |
+
while current_height > 0 and current_time < max_time:
|
| 45 |
+
# Calculate drag force
|
| 46 |
+
drag_force = 0.5 * self.drag_coefficient * self.air_density * \
|
| 47 |
+
self.cross_sectional_area * abs(current_velocity)**2
|
| 48 |
+
|
| 49 |
+
# Determine drag direction (opposite to velocity)
|
| 50 |
+
drag_direction = 1 if current_velocity > 0 else -1
|
| 51 |
+
|
| 52 |
+
# Calculate net force
|
| 53 |
+
net_force = self.mass * self.gravity - drag_direction * drag_force
|
| 54 |
+
|
| 55 |
+
# Calculate acceleration
|
| 56 |
+
acceleration = net_force / self.mass
|
| 57 |
+
|
| 58 |
+
# Update velocity
|
| 59 |
+
current_velocity += acceleration * time_step
|
| 60 |
+
|
| 61 |
+
# Update height (now decreasing)
|
| 62 |
+
current_height -= current_velocity * time_step
|
| 63 |
+
|
| 64 |
+
# Update time
|
| 65 |
+
current_time += time_step
|
| 66 |
+
|
| 67 |
+
# Store results
|
| 68 |
+
times.append(current_time)
|
| 69 |
+
heights.append(max(0, current_height))
|
| 70 |
+
velocities.append(current_velocity)
|
| 71 |
+
|
| 72 |
+
return times, heights, velocities
|
| 73 |
+
|
| 74 |
+
def plot_trajectory(times, heights, velocities):
|
| 75 |
+
"""
|
| 76 |
+
Create a matplotlib figure with two subplots
|
| 77 |
+
"""
|
| 78 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
|
| 79 |
+
|
| 80 |
+
# Height vs Time plot
|
| 81 |
+
ax1.plot(times, heights, label='Height')
|
| 82 |
+
ax1.set_xlabel('Time (s)')
|
| 83 |
+
ax1.set_ylabel('Height (m)')
|
| 84 |
+
ax1.set_title('Object Height over Time')
|
| 85 |
+
ax1.invert_yaxis() # Invert y-axis to show falling
|
| 86 |
+
ax1.legend()
|
| 87 |
+
ax1.grid(True)
|
| 88 |
+
|
| 89 |
+
# Velocity vs Time plot
|
| 90 |
+
ax2.plot(times, velocities, label='Velocity', color='r')
|
| 91 |
+
ax2.set_xlabel('Time (s)')
|
| 92 |
+
ax2.set_ylabel('Velocity (m/s)')
|
| 93 |
+
ax2.set_title('Velocity over Time')
|
| 94 |
+
ax2.legend()
|
| 95 |
+
ax2.grid(True)
|
| 96 |
+
|
| 97 |
+
return fig
|
| 98 |
+
|
| 99 |
+
def main():
|
| 100 |
+
st.title('Air Resistance Simulation')
|
| 101 |
+
|
| 102 |
+
# Sidebar for input parameters
|
| 103 |
+
st.sidebar.header('Object Properties')
|
| 104 |
+
|
| 105 |
+
# Input sliders
|
| 106 |
+
mass = st.sidebar.slider('Mass (kg)', min_value=0.1, max_value=100.0, value=1.0, step=0.1)
|
| 107 |
+
initial_height = st.sidebar.slider('Initial Height (m)', min_value=1, max_value=500, value=100)
|
| 108 |
+
drag_coefficient = st.sidebar.slider('Drag Coefficient', min_value=0.1, max_value=2.0, value=0.47, step=0.01)
|
| 109 |
+
cross_sectional_area = st.sidebar.slider('Cross-Sectional Area (m²)',
|
| 110 |
+
min_value=0.01, max_value=10.0, value=0.1, step=0.01)
|
| 111 |
+
|
| 112 |
+
# Create falling object
|
| 113 |
+
falling_object = FallingObject(
|
| 114 |
+
mass=mass,
|
| 115 |
+
drag_coefficient=drag_coefficient,
|
| 116 |
+
cross_sectional_area=cross_sectional_area,
|
| 117 |
+
initial_height=initial_height
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# Calculate trajectory
|
| 121 |
+
times, heights, velocities = falling_object.calculate_trajectory()
|
| 122 |
+
|
| 123 |
+
# Plot results
|
| 124 |
+
fig = plot_trajectory(times, heights, velocities)
|
| 125 |
+
st.pyplot(fig)
|
| 126 |
+
|
| 127 |
+
# Display some key metrics
|
| 128 |
+
st.subheader('Simulation Results')
|
| 129 |
+
col1, col2, col3 = st.columns(3)
|
| 130 |
+
|
| 131 |
+
with col1:
|
| 132 |
+
st.metric('Fall Time', f'{times[-1]:.2f} s')
|
| 133 |
+
|
| 134 |
+
with col2:
|
| 135 |
+
st.metric('Max Velocity', f'{max(abs(v) for v in velocities):.2f} m/s')
|
| 136 |
+
|
| 137 |
+
with col3:
|
| 138 |
+
terminal_velocity = np.sqrt((2 * mass * 9.81) / (drag_coefficient * 1.225 * cross_sectional_area))
|
| 139 |
+
st.metric('Terminal Velocity', f'{terminal_velocity:.2f} m/s')
|
| 140 |
+
|
| 141 |
+
# Explanation
|
| 142 |
+
st.markdown("""
|
| 143 |
+
### How Air Resistance Works
|
| 144 |
+
|
| 145 |
+
This simulation demonstrates how air resistance affects a falling object:
|
| 146 |
+
|
| 147 |
+
- **Drag Force**: Opposes the motion of the object through the air
|
| 148 |
+
- **Factors Affecting Drag**:
|
| 149 |
+
* Object's mass
|
| 150 |
+
* Drag coefficient
|
| 151 |
+
* Cross-sectional area
|
| 152 |
+
* Air density
|
| 153 |
+
|
| 154 |
+
As the object falls, air resistance increases with velocity, eventually
|
| 155 |
+
leading to terminal velocity where gravitational force equals air resistance.
|
| 156 |
+
""")
|
| 157 |
+
|
| 158 |
+
if __name__ == '__main__':
|
| 159 |
+
main()
|
pages/angular-momentum.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
# Title of the app
|
| 6 |
+
st.title("Angular Momentum Visualization")
|
| 7 |
+
|
| 8 |
+
# Let the user choose the type of motion
|
| 9 |
+
motion_type = st.selectbox("Select Motion Type", ["Linear Motion", "Circular Motion"])
|
| 10 |
+
|
| 11 |
+
# Scenario 1: Linear Motion
|
| 12 |
+
if motion_type == "Linear Motion":
|
| 13 |
+
st.subheader("Linear Motion")
|
| 14 |
+
st.write("A particle moves in a straight line. Adjust the initial position and velocity to see how Angular Momentum behaves.")
|
| 15 |
+
|
| 16 |
+
# Input sliders for initial position and velocity
|
| 17 |
+
x0 = st.slider("Initial x-position (x₀)", -5.0, 5.0, 0.0)
|
| 18 |
+
y0 = st.slider("Initial y-position (y₀)", -5.0, 5.0, 0.0)
|
| 19 |
+
v_x = st.slider("Velocity x-component (vₓ)", -2.0, 2.0, 1.0)
|
| 20 |
+
v_y = st.slider("Velocity y-component (vᵧ)", -2.0, 2.0, 0.0)
|
| 21 |
+
t = st.slider("Time (t)", 0.0, 10.0, 0.0)
|
| 22 |
+
m = 1.0 # Mass set to 1 for simplicity
|
| 23 |
+
|
| 24 |
+
# Calculate position at time t
|
| 25 |
+
x = x0 + v_x * t
|
| 26 |
+
y = y0 + v_y * t
|
| 27 |
+
|
| 28 |
+
# Calculate Angular Momentum (z-component)
|
| 29 |
+
L_z = m * (x * v_y - y * v_x)
|
| 30 |
+
|
| 31 |
+
# Create the plot
|
| 32 |
+
fig, ax = plt.subplots()
|
| 33 |
+
ax.set_aspect('equal') # Equal scaling for x and y axes
|
| 34 |
+
ax.set_xlim(-10, 10)
|
| 35 |
+
ax.set_ylim(-10, 10)
|
| 36 |
+
ax.grid(True)
|
| 37 |
+
|
| 38 |
+
# Plot the origin
|
| 39 |
+
ax.plot(0, 0, 'ko', label='Origin')
|
| 40 |
+
|
| 41 |
+
# Plot the particle's path (straight line)
|
| 42 |
+
if v_x != 0 or v_y != 0:
|
| 43 |
+
# Define the line extending beyond plot limits
|
| 44 |
+
s = 100
|
| 45 |
+
x1 = x0 + v_x * (-s)
|
| 46 |
+
y1 = y0 + v_y * (-s)
|
| 47 |
+
x2 = x0 + v_x * s
|
| 48 |
+
y2 = y0 + v_y * s
|
| 49 |
+
ax.plot([x1, x2], [y1, y2], 'g--', label='Path')
|
| 50 |
+
|
| 51 |
+
# Plot position vector (blue)
|
| 52 |
+
ax.arrow(0, 0, x, y, head_width=0.5, head_length=0.5, fc='blue', ec='blue', label='Position Vector')
|
| 53 |
+
# Plot velocity vector (red)
|
| 54 |
+
ax.arrow(x, y, v_x, v_y, head_width=0.5, head_length=0.5, fc='red', ec='red', label='Velocity Vector')
|
| 55 |
+
# Plot particle position
|
| 56 |
+
ax.plot(x, y, 'ro', label='Particle')
|
| 57 |
+
|
| 58 |
+
# Display Angular Momentum
|
| 59 |
+
ax.text(-9, 9, f'L_z = {L_z:.2f}', fontsize=12)
|
| 60 |
+
|
| 61 |
+
# Add legend
|
| 62 |
+
ax.legend(loc='upper right')
|
| 63 |
+
st.pyplot(fig)
|
| 64 |
+
|
| 65 |
+
# Scenario 2: Circular Motion
|
| 66 |
+
elif motion_type == "Circular Motion":
|
| 67 |
+
st.subheader("Circular Motion")
|
| 68 |
+
st.write("A particle moves in a circle around the origin. Adjust the radius and angular velocity to observe Angular Momentum.")
|
| 69 |
+
|
| 70 |
+
# Input sliders for radius and angular velocity
|
| 71 |
+
r = st.slider("Radius (r)", 0.5, 5.0, 2.0)
|
| 72 |
+
omega = st.slider("Angular Velocity (ω)", 0.1, 2.0, 1.0)
|
| 73 |
+
t = st.slider("Time (t)", 0.0, 10.0, 0.0)
|
| 74 |
+
m = 1.0 # Mass set to 1
|
| 75 |
+
|
| 76 |
+
# Calculate position and velocity using circular motion equations
|
| 77 |
+
theta = omega * t
|
| 78 |
+
x = r * np.cos(theta)
|
| 79 |
+
y = r * np.sin(theta)
|
| 80 |
+
v_x = -r * omega * np.sin(theta)
|
| 81 |
+
v_y = r * omega * np.cos(theta)
|
| 82 |
+
|
| 83 |
+
# Calculate Angular Momentum (z-component)
|
| 84 |
+
L_z = m * (x * v_y - y * v_x)
|
| 85 |
+
|
| 86 |
+
# Create the plot
|
| 87 |
+
fig, ax = plt.subplots()
|
| 88 |
+
ax.set_aspect('equal')
|
| 89 |
+
ax.set_xlim(-10, 10)
|
| 90 |
+
ax.set_ylim(-10, 10)
|
| 91 |
+
ax.grid(True)
|
| 92 |
+
|
| 93 |
+
# Plot the origin
|
| 94 |
+
ax.plot(0, 0, 'ko', label='Origin')
|
| 95 |
+
|
| 96 |
+
# Plot the circular path
|
| 97 |
+
theta_vals = np.linspace(0, 2 * np.pi, 100)
|
| 98 |
+
x_path = r * np.cos(theta_vals)
|
| 99 |
+
y_path = r * np.sin(theta_vals)
|
| 100 |
+
ax.plot(x_path, y_path, 'g--', label='Path')
|
| 101 |
+
|
| 102 |
+
# Plot position vector (blue)
|
| 103 |
+
ax.arrow(0, 0, x, y, head_width=0.5, head_length=0.5, fc='blue', ec='blue', label='Position Vector')
|
| 104 |
+
# Plot velocity vector (red)
|
| 105 |
+
ax.arrow(x, y, v_x, v_y, head_width=0.5, head_length=0.5, fc='red', ec='red', label='Velocity Vector')
|
| 106 |
+
# Plot particle position
|
| 107 |
+
ax.plot(x, y, 'ro', label='Particle')
|
| 108 |
+
|
| 109 |
+
# Display Angular Momentum
|
| 110 |
+
ax.text(-9, 9, f'L_z = {L_z:.2f}', fontsize=12)
|
| 111 |
+
|
| 112 |
+
# Add legend
|
| 113 |
+
ax.legend(loc='upper right')
|
| 114 |
+
st.pyplot(fig)
|
pages/atomic-structure.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import plotly.graph_objects as go
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
# Dictionary of elements (atomic number: (name, symbol))
|
| 6 |
+
elements = {
|
| 7 |
+
1: ("Hydrogen", "H"),
|
| 8 |
+
2: ("Helium", "He"),
|
| 9 |
+
3: ("Lithium", "Li"),
|
| 10 |
+
4: ("Beryllium", "Be"),
|
| 11 |
+
5: ("Boron", "B"),
|
| 12 |
+
6: ("Carbon", "C"),
|
| 13 |
+
7: ("Nitrogen", "N"),
|
| 14 |
+
8: ("Oxygen", "O"),
|
| 15 |
+
9: ("Fluorine", "F"),
|
| 16 |
+
10: ("Neon", "Ne"),
|
| 17 |
+
# Add more elements as needed
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
# Function to calculate electron shells
|
| 21 |
+
def get_electron_shells(electrons):
|
| 22 |
+
shells = []
|
| 23 |
+
n = 1
|
| 24 |
+
while electrons > 0:
|
| 25 |
+
max_electrons = 2 * n**2
|
| 26 |
+
if electrons >= max_electrons:
|
| 27 |
+
shells.append(max_electrons)
|
| 28 |
+
electrons -= max_electrons
|
| 29 |
+
else:
|
| 30 |
+
shells.append(electrons)
|
| 31 |
+
electrons = 0
|
| 32 |
+
n += 1
|
| 33 |
+
return shells
|
| 34 |
+
|
| 35 |
+
# Streamlit app
|
| 36 |
+
st.title("Atomic Structure Visualizer")
|
| 37 |
+
|
| 38 |
+
# User inputs
|
| 39 |
+
col1, col2, col3 = st.columns(3)
|
| 40 |
+
with col1:
|
| 41 |
+
Z = st.number_input("Number of protons (Z)", min_value=1, max_value=118, value=6)
|
| 42 |
+
with col2:
|
| 43 |
+
N = st.number_input("Number of neutrons (N)", min_value=0, value=6)
|
| 44 |
+
with col3:
|
| 45 |
+
E = st.number_input("Number of electrons (E)", min_value=0, value=Z)
|
| 46 |
+
|
| 47 |
+
# Calculate atomic properties
|
| 48 |
+
A = Z + N # Mass number
|
| 49 |
+
charge = Z - E # Charge of the ion
|
| 50 |
+
name, symbol = elements.get(Z, ("Unknown", "?"))
|
| 51 |
+
|
| 52 |
+
# Display atomic information
|
| 53 |
+
st.subheader("Atomic Information")
|
| 54 |
+
st.write(f"**Element**: {name} ({symbol})")
|
| 55 |
+
st.write(f"**Atomic Number**: {Z}")
|
| 56 |
+
st.write(f"**Mass Number**: {A}")
|
| 57 |
+
st.write(f"**Number of Electrons**: {E}")
|
| 58 |
+
st.write(f"**Charge**: {'+' if charge > 0 else ''}{charge}" if charge != 0 else "Neutral atom")
|
| 59 |
+
|
| 60 |
+
# Calculate electron shells
|
| 61 |
+
shells = get_electron_shells(E)
|
| 62 |
+
|
| 63 |
+
# Nucleus parameters
|
| 64 |
+
nucleus_radius = 0.1
|
| 65 |
+
|
| 66 |
+
# Generate proton positions
|
| 67 |
+
proton_theta = np.random.uniform(0, 2 * np.pi, Z)
|
| 68 |
+
proton_r = np.random.uniform(0, nucleus_radius, Z)
|
| 69 |
+
proton_x = proton_r * np.cos(proton_theta)
|
| 70 |
+
proton_y = proton_r * np.sin(proton_theta)
|
| 71 |
+
|
| 72 |
+
# Generate neutron positions
|
| 73 |
+
neutron_theta = np.random.uniform(0, 2 * np.pi, N)
|
| 74 |
+
neutron_r = np.random.uniform(0, nucleus_radius, N)
|
| 75 |
+
neutron_x = neutron_r * np.cos(neutron_theta)
|
| 76 |
+
neutron_y = neutron_r * np.sin(neutron_theta)
|
| 77 |
+
|
| 78 |
+
# Generate electron positions
|
| 79 |
+
electron_x = []
|
| 80 |
+
electron_y = []
|
| 81 |
+
r_shell = 1.0 # Radius increment per shell
|
| 82 |
+
for n, k in enumerate(shells, start=1):
|
| 83 |
+
r = n * r_shell
|
| 84 |
+
angles = np.linspace(0, 2 * np.pi, k, endpoint=False)
|
| 85 |
+
for angle in angles:
|
| 86 |
+
electron_x.append(r * np.cos(angle))
|
| 87 |
+
electron_y.append(r * np.sin(angle))
|
| 88 |
+
|
| 89 |
+
# Create Plotly figure
|
| 90 |
+
fig = go.Figure()
|
| 91 |
+
|
| 92 |
+
# Add protons
|
| 93 |
+
fig.add_trace(go.Scatter(
|
| 94 |
+
x=proton_x, y=proton_y,
|
| 95 |
+
mode='markers',
|
| 96 |
+
name='Protons',
|
| 97 |
+
marker=dict(color='red', size=8)
|
| 98 |
+
))
|
| 99 |
+
|
| 100 |
+
# Add neutrons
|
| 101 |
+
fig.add_trace(go.Scatter(
|
| 102 |
+
x=neutron_x, y=neutron_y,
|
| 103 |
+
mode='markers',
|
| 104 |
+
name='Neutrons',
|
| 105 |
+
marker=dict(color='blue', size=8)
|
| 106 |
+
))
|
| 107 |
+
|
| 108 |
+
# Add electrons
|
| 109 |
+
fig.add_trace(go.Scatter(
|
| 110 |
+
x=electron_x, y=electron_y,
|
| 111 |
+
mode='markers',
|
| 112 |
+
name='Electrons',
|
| 113 |
+
marker=dict(color='green', size=10)
|
| 114 |
+
))
|
| 115 |
+
|
| 116 |
+
# Add shell circles
|
| 117 |
+
for n in range(1, len(shells) + 1):
|
| 118 |
+
fig.add_shape(
|
| 119 |
+
type="circle",
|
| 120 |
+
x0=-n * r_shell, y0=-n * r_shell,
|
| 121 |
+
x1=n * r_shell, y1=n * r_shell,
|
| 122 |
+
line=dict(color="gray", width=1, dash="dash")
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Set plot limits and aspect ratio
|
| 126 |
+
max_r = max(0.5, len(shells) * r_shell)
|
| 127 |
+
fig.update_xaxes(range=[-max_r, max_r])
|
| 128 |
+
fig.update_yaxes(range=[-max_r, max_r], scaleanchor="x", scaleratio=1)
|
| 129 |
+
|
| 130 |
+
# Update layout
|
| 131 |
+
fig.update_layout(
|
| 132 |
+
title="Atomic Structure",
|
| 133 |
+
width=600,
|
| 134 |
+
height=600,
|
| 135 |
+
showlegend=True
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Display plot in Streamlit
|
| 139 |
+
st.plotly_chart(fig)
|
| 140 |
+
|
| 141 |
+
# Instructions
|
| 142 |
+
st.markdown("""
|
| 143 |
+
### How to Use
|
| 144 |
+
- **Protons (Z)**: Defines the element (e.g., Z=6 for Carbon).
|
| 145 |
+
- **Neutrons (N)**: Affects the mass number (A = Z + N).
|
| 146 |
+
- **Electrons (E)**: Determines if it's an ion (Charge = Z - E).
|
| 147 |
+
- Adjust the inputs to visualize different atoms or ions!
|
| 148 |
+
""")
|
pages/basic-circuit-analysis.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from matplotlib.patches import Rectangle, Circle, FancyArrowPatch
|
| 5 |
+
|
| 6 |
+
st.set_page_config(page_title="Circuit Analysis Simulator", layout="wide")
|
| 7 |
+
|
| 8 |
+
st.title("Interactive Circuit Analysis Simulator")
|
| 9 |
+
st.write("Explore basic circuit concepts including Ohm's Law, series and parallel circuits.")
|
| 10 |
+
|
| 11 |
+
# Sidebar for navigation
|
| 12 |
+
page = st.sidebar.radio("Choose a Simulation",
|
| 13 |
+
["Ohm's Law", "Series Circuit", "Parallel Circuit"])
|
| 14 |
+
|
| 15 |
+
if page == "Ohm's Law":
|
| 16 |
+
st.header("Ohm's Law: V = I × R")
|
| 17 |
+
st.write("""
|
| 18 |
+
Ohm's Law describes the relationship between voltage (V), current (I), and resistance (R).
|
| 19 |
+
Adjust the sliders to see how changing one parameter affects the others.
|
| 20 |
+
""")
|
| 21 |
+
|
| 22 |
+
col1, col2 = st.columns(2)
|
| 23 |
+
|
| 24 |
+
with col1:
|
| 25 |
+
# Let user control two of the variables and calculate the third
|
| 26 |
+
calculation_mode = st.radio(
|
| 27 |
+
"What do you want to calculate?",
|
| 28 |
+
["Voltage", "Current", "Resistance"]
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
if calculation_mode == "Voltage":
|
| 32 |
+
current = st.slider("Current (A)", 0.1, 10.0, 1.0, 0.1)
|
| 33 |
+
resistance = st.slider("Resistance (Ω)", 1.0, 100.0, 10.0, 1.0)
|
| 34 |
+
voltage = current * resistance
|
| 35 |
+
st.success(f"Calculated Voltage: {voltage:.2f} V")
|
| 36 |
+
|
| 37 |
+
elif calculation_mode == "Current":
|
| 38 |
+
voltage = st.slider("Voltage (V)", 1.0, 220.0, 12.0, 1.0)
|
| 39 |
+
resistance = st.slider("Resistance (Ω)", 1.0, 100.0, 10.0, 1.0)
|
| 40 |
+
current = voltage / resistance if resistance != 0 else float('inf')
|
| 41 |
+
st.success(f"Calculated Current: {current:.2f} A")
|
| 42 |
+
|
| 43 |
+
else: # Resistance
|
| 44 |
+
voltage = st.slider("Voltage (V)", 1.0, 220.0, 12.0, 1.0)
|
| 45 |
+
current = st.slider("Current (A)", 0.1, 10.0, 1.0, 0.1)
|
| 46 |
+
resistance = voltage / current if current != 0 else float('inf')
|
| 47 |
+
st.success(f"Calculated Resistance: {resistance:.2f} Ω")
|
| 48 |
+
|
| 49 |
+
with col2:
|
| 50 |
+
# Create visualization
|
| 51 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 52 |
+
|
| 53 |
+
# Draw battery
|
| 54 |
+
battery_height = 1.5
|
| 55 |
+
battery_x = 1
|
| 56 |
+
battery_y = 3
|
| 57 |
+
|
| 58 |
+
# Long line
|
| 59 |
+
ax.plot([battery_x, battery_x + 6], [battery_y + battery_height, battery_y + battery_height], 'k-', lw=2)
|
| 60 |
+
# Bottom line
|
| 61 |
+
ax.plot([battery_x, battery_x + 6], [battery_y, battery_y], 'k-', lw=2)
|
| 62 |
+
|
| 63 |
+
# Battery
|
| 64 |
+
ax.plot([battery_x, battery_x], [battery_y, battery_y + battery_height], 'k-', lw=2)
|
| 65 |
+
ax.plot([battery_x + 0.2, battery_x + 0.2], [battery_y + 0.25, battery_y + battery_height - 0.25], 'k-', lw=4)
|
| 66 |
+
ax.plot([battery_x + 0.5, battery_x + 0.5], [battery_y + 0.5, battery_y + battery_height - 0.5], 'k-', lw=2)
|
| 67 |
+
|
| 68 |
+
# Resistor (zigzag)
|
| 69 |
+
res_x = battery_x + 3
|
| 70 |
+
res_y = battery_y + battery_height
|
| 71 |
+
zigzag_width = 1.5
|
| 72 |
+
zigzag_height = 0.5
|
| 73 |
+
zigzag_pts = 7
|
| 74 |
+
x_pts = np.linspace(res_x, res_x + zigzag_width, zigzag_pts)
|
| 75 |
+
y_pts = np.array([0, 1, 0, 1, 0, 1, 0]) * zigzag_height + res_y
|
| 76 |
+
ax.plot(x_pts, y_pts, 'k-', lw=2)
|
| 77 |
+
|
| 78 |
+
# Add current direction arrow
|
| 79 |
+
if calculation_mode == "Current" or calculation_mode == "Resistance":
|
| 80 |
+
current_value = current
|
| 81 |
+
else:
|
| 82 |
+
current_value = current
|
| 83 |
+
|
| 84 |
+
# Scale arrow size based on current
|
| 85 |
+
arrow_scale = min(1, current_value / 5)
|
| 86 |
+
arrow_width = 0.15 * (0.5 + arrow_scale)
|
| 87 |
+
arrow = FancyArrowPatch((battery_x + 4.5, battery_y + 0.5),
|
| 88 |
+
(battery_x + 3, battery_y + 0.5),
|
| 89 |
+
arrowstyle='->',
|
| 90 |
+
color='blue',
|
| 91 |
+
lw=2,
|
| 92 |
+
mutation_scale=20)
|
| 93 |
+
ax.add_patch(arrow)
|
| 94 |
+
ax.text(battery_x + 3.8, battery_y + 0.3, f"{current:.1f} A", color='blue')
|
| 95 |
+
|
| 96 |
+
# Add voltage label
|
| 97 |
+
if calculation_mode == "Voltage" or calculation_mode == "Resistance":
|
| 98 |
+
voltage_value = voltage
|
| 99 |
+
else:
|
| 100 |
+
voltage_value = voltage
|
| 101 |
+
|
| 102 |
+
ax.text(battery_x + 0.7, battery_y + 0.75, f"{voltage:.1f} V", color='red')
|
| 103 |
+
|
| 104 |
+
# Add resistance label
|
| 105 |
+
if calculation_mode == "Resistance" or calculation_mode == "Voltage":
|
| 106 |
+
resistance_value = resistance
|
| 107 |
+
else:
|
| 108 |
+
resistance_value = resistance
|
| 109 |
+
|
| 110 |
+
ax.text(res_x + 0.2, res_y + 0.75, f"{resistance:.1f} Ω", color='green')
|
| 111 |
+
|
| 112 |
+
ax.set_xlim(0, 8)
|
| 113 |
+
ax.set_ylim(1, 6)
|
| 114 |
+
ax.axis('equal')
|
| 115 |
+
ax.axis('off')
|
| 116 |
+
|
| 117 |
+
st.pyplot(fig)
|
| 118 |
+
|
| 119 |
+
# Show the formula and calculation
|
| 120 |
+
st.write("### Ohm's Law Formula")
|
| 121 |
+
if calculation_mode == "Voltage":
|
| 122 |
+
st.latex(r"V = I \times R")
|
| 123 |
+
st.latex(f"V = {current:.2f} \, A \times {resistance:.2f} \, \Omega = {voltage:.2f} \, V")
|
| 124 |
+
elif calculation_mode == "Current":
|
| 125 |
+
st.latex(r"I = \frac{V}{R}")
|
| 126 |
+
st.latex(f"I = \\frac{{{voltage:.2f} \, V}}{{{resistance:.2f} \, \Omega}} = {current:.2f} \, A")
|
| 127 |
+
else: # Resistance
|
| 128 |
+
st.latex(r"R = \frac{V}{I}")
|
| 129 |
+
st.latex(f"R = \\frac{{{voltage:.2f} \, V}}{{{current:.2f} \, A}} = {resistance:.2f} \, \Omega")
|
| 130 |
+
|
| 131 |
+
elif page == "Series Circuit":
|
| 132 |
+
st.header("Series Circuit Analysis")
|
| 133 |
+
st.write("""
|
| 134 |
+
In a series circuit, the same current flows through each component, and the total voltage is the sum of the voltages across each component.
|
| 135 |
+
""")
|
| 136 |
+
|
| 137 |
+
col1, col2 = st.columns(2)
|
| 138 |
+
|
| 139 |
+
with col1:
|
| 140 |
+
# User inputs
|
| 141 |
+
voltage_source = st.slider("Voltage Source (V)", 1.0, 24.0, 12.0, 0.1)
|
| 142 |
+
|
| 143 |
+
st.subheader("Resistors in Series")
|
| 144 |
+
num_resistors = st.slider("Number of Resistors", 1, 5, 3)
|
| 145 |
+
|
| 146 |
+
resistances = []
|
| 147 |
+
for i in range(num_resistors):
|
| 148 |
+
r = st.slider(f"R{i+1} (Ω)", 1.0, 100.0, 10.0 * (i+1), 1.0, key=f"series_r{i}")
|
| 149 |
+
resistances.append(r)
|
| 150 |
+
|
| 151 |
+
# Calculations
|
| 152 |
+
total_resistance = sum(resistances)
|
| 153 |
+
current = voltage_source / total_resistance if total_resistance > 0 else 0
|
| 154 |
+
voltage_drops = [current * r for r in resistances]
|
| 155 |
+
power_dissipations = [current**2 * r for r in resistances]
|
| 156 |
+
|
| 157 |
+
st.subheader("Circuit Analysis Results")
|
| 158 |
+
st.write(f"Total Resistance: {total_resistance:.2f} Ω")
|
| 159 |
+
st.write(f"Current: {current:.2f} A")
|
| 160 |
+
|
| 161 |
+
# Display voltage drops and power for each resistor
|
| 162 |
+
for i in range(num_resistors):
|
| 163 |
+
st.write(f"R{i+1}: Voltage Drop = {voltage_drops[i]:.2f} V, Power = {power_dissipations[i]:.2f} W")
|
| 164 |
+
|
| 165 |
+
with col2:
|
| 166 |
+
# Create visualization of series circuit
|
| 167 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 168 |
+
|
| 169 |
+
# Draw circuit
|
| 170 |
+
start_x, start_y = 1, 5
|
| 171 |
+
|
| 172 |
+
# Draw battery
|
| 173 |
+
battery_height = 1.5
|
| 174 |
+
battery_x = start_x
|
| 175 |
+
battery_y = start_y - battery_height/2
|
| 176 |
+
|
| 177 |
+
# Battery
|
| 178 |
+
ax.plot([battery_x, battery_x], [battery_y, battery_y + battery_height], 'k-', lw=2)
|
| 179 |
+
ax.plot([battery_x + 0.2, battery_x + 0.2], [battery_y + 0.25, battery_y + battery_height - 0.25], 'k-', lw=4)
|
| 180 |
+
ax.plot([battery_x + 0.5, battery_x + 0.5], [battery_y + 0.5, battery_y + battery_height - 0.5], 'k-', lw=2)
|
| 181 |
+
|
| 182 |
+
# Label voltage source
|
| 183 |
+
ax.text(battery_x + 0.7, battery_y + 0.75, f"{voltage_source:.1f} V", color='red')
|
| 184 |
+
|
| 185 |
+
# Draw wires and resistors in series
|
| 186 |
+
line_length = 10 / (num_resistors + 1)
|
| 187 |
+
|
| 188 |
+
# Top line from battery to first resistor
|
| 189 |
+
ax.plot([battery_x + 0.5, battery_x + line_length], [battery_y + battery_height - 0.5, battery_y + battery_height - 0.5], 'k-', lw=2)
|
| 190 |
+
|
| 191 |
+
# Draw resistors in series
|
| 192 |
+
for i in range(num_resistors):
|
| 193 |
+
res_x = battery_x + line_length * (i + 1)
|
| 194 |
+
res_y = battery_y + battery_height - 0.5
|
| 195 |
+
|
| 196 |
+
# Draw resistor (zigzag)
|
| 197 |
+
zigzag_width = 1.0
|
| 198 |
+
zigzag_height = 0.5
|
| 199 |
+
zigzag_pts = 7
|
| 200 |
+
x_pts = np.linspace(res_x, res_x + zigzag_width, zigzag_pts)
|
| 201 |
+
y_pts = np.array([0, 1, 0, 1, 0, 1, 0]) * zigzag_height + res_y
|
| 202 |
+
ax.plot(x_pts, y_pts, 'k-', lw=2)
|
| 203 |
+
|
| 204 |
+
# Label resistor
|
| 205 |
+
ax.text(res_x + 0.2, res_y + 0.8, f"R{i+1}\n{resistances[i]:.1f}Ω\n{voltage_drops[i]:.1f}V", fontsize=8)
|
| 206 |
+
|
| 207 |
+
# Connect to next resistor if not the last one
|
| 208 |
+
if i < num_resistors - 1:
|
| 209 |
+
ax.plot([res_x + zigzag_width, res_x + line_length], [res_y, res_y], 'k-', lw=2)
|
| 210 |
+
|
| 211 |
+
# Bottom line back to battery
|
| 212 |
+
ax.plot([battery_x + line_length * (num_resistors + 1) - line_length + zigzag_width, battery_x + 0.5],
|
| 213 |
+
[res_y, battery_y + 0.5], 'k-', lw=2)
|
| 214 |
+
|
| 215 |
+
# Add current direction arrow
|
| 216 |
+
arrow = FancyArrowPatch((battery_x + 0.5, battery_y + 0.5),
|
| 217 |
+
(battery_x + line_length/2, battery_y + 0.5),
|
| 218 |
+
arrowstyle='->',
|
| 219 |
+
color='blue',
|
| 220 |
+
lw=2,
|
| 221 |
+
mutation_scale=15)
|
| 222 |
+
ax.add_patch(arrow)
|
| 223 |
+
ax.text(battery_x + line_length/4, battery_y + 0.2, f"{current:.1f}A", color='blue', fontsize=9)
|
| 224 |
+
|
| 225 |
+
ax.set_xlim(0, 12)
|
| 226 |
+
ax.set_ylim(2, 8)
|
| 227 |
+
ax.axis('equal')
|
| 228 |
+
ax.axis('off')
|
| 229 |
+
|
| 230 |
+
st.pyplot(fig)
|
| 231 |
+
|
| 232 |
+
# Show formulas
|
| 233 |
+
st.write("### Series Circuit Formulas")
|
| 234 |
+
st.latex(r"R_{total} = R_1 + R_2 + ... + R_n")
|
| 235 |
+
st.latex(f"R_{{total}} = {' + '.join([f'{r:.1f}' for r in resistances])} = {total_resistance:.2f} \, \Omega")
|
| 236 |
+
st.latex(r"I = \frac{V_{source}}{R_{total}}")
|
| 237 |
+
st.latex(f"I = \\frac{{{voltage_source:.2f}}}{{{total_resistance:.2f}}} = {current:.2f} \, A")
|
| 238 |
+
st.latex(r"V_n = I \times R_n")
|
| 239 |
+
|
| 240 |
+
elif page == "Parallel Circuit":
|
| 241 |
+
st.header("Parallel Circuit Analysis")
|
| 242 |
+
st.write("""
|
| 243 |
+
In a parallel circuit, each component has the same voltage across it, and the total current is the sum of the currents through each path.
|
| 244 |
+
""")
|
| 245 |
+
|
| 246 |
+
col1, col2 = st.columns(2)
|
| 247 |
+
|
| 248 |
+
with col1:
|
| 249 |
+
# User inputs
|
| 250 |
+
voltage_source = st.slider("Voltage Source (V)", 1.0, 24.0, 12.0, 0.1)
|
| 251 |
+
|
| 252 |
+
st.subheader("Resistors in Parallel")
|
| 253 |
+
num_resistors = st.slider("Number of Resistors", 1, 5, 3)
|
| 254 |
+
|
| 255 |
+
resistances = []
|
| 256 |
+
for i in range(num_resistors):
|
| 257 |
+
r = st.slider(f"R{i+1} (Ω)", 1.0, 100.0, float(10.0 * (i+1)), 1.0, key=f"parallel_r{i}")
|
| 258 |
+
resistances.append(r)
|
| 259 |
+
|
| 260 |
+
# Calculations
|
| 261 |
+
inverse_resistance_sum = sum(1/r for r in resistances) if resistances else 0
|
| 262 |
+
total_resistance = 1/inverse_resistance_sum if inverse_resistance_sum > 0 else float('inf')
|
| 263 |
+
|
| 264 |
+
branch_currents = [voltage_source / r for r in resistances]
|
| 265 |
+
total_current = sum(branch_currents)
|
| 266 |
+
power_dissipations = [(voltage_source ** 2) / r for r in resistances]
|
| 267 |
+
total_power = voltage_source * total_current
|
| 268 |
+
|
| 269 |
+
st.subheader("Circuit Analysis Results")
|
| 270 |
+
st.write(f"Total Resistance: {total_resistance:.2f} Ω")
|
| 271 |
+
st.write(f"Total Current: {total_current:.2f} A")
|
| 272 |
+
|
| 273 |
+
# Display current and power for each resistor
|
| 274 |
+
for i in range(num_resistors):
|
| 275 |
+
st.write(f"R{i+1}: Current = {branch_currents[i]:.2f} A, Power = {power_dissipations[i]:.2f} W")
|
| 276 |
+
|
| 277 |
+
with col2:
|
| 278 |
+
# Create visualization of parallel circuit
|
| 279 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 280 |
+
|
| 281 |
+
# Draw circuit
|
| 282 |
+
start_x, start_y = 1, 5
|
| 283 |
+
end_x = 9
|
| 284 |
+
|
| 285 |
+
# Draw battery
|
| 286 |
+
battery_height = 1.5
|
| 287 |
+
battery_x = start_x
|
| 288 |
+
battery_y = start_y - battery_height/2
|
| 289 |
+
|
| 290 |
+
# Battery
|
| 291 |
+
ax.plot([battery_x, battery_x], [battery_y, battery_y + battery_height], 'k-', lw=2)
|
| 292 |
+
ax.plot([battery_x + 0.2, battery_x + 0.2], [battery_y + 0.25, battery_y + battery_height - 0.25], 'k-', lw=4)
|
| 293 |
+
ax.plot([battery_x + 0.5, battery_x + 0.5], [battery_y + 0.5, battery_y + battery_height - 0.5], 'k-', lw=2)
|
| 294 |
+
|
| 295 |
+
# Label voltage source
|
| 296 |
+
ax.text(battery_x + 0.7, battery_y + 0.75, f"{voltage_source:.1f} V", color='red')
|
| 297 |
+
|
| 298 |
+
# Top horizontal line
|
| 299 |
+
ax.plot([battery_x + 0.5, end_x], [battery_y + battery_height - 0.5, battery_y + battery_height - 0.5], 'k-', lw=2)
|
| 300 |
+
|
| 301 |
+
# Bottom horizontal line
|
| 302 |
+
ax.plot([battery_x + 0.5, end_x], [battery_y + 0.5, battery_y + 0.5], 'k-', lw=2)
|
| 303 |
+
|
| 304 |
+
# Add main current arrow
|
| 305 |
+
arrow = FancyArrowPatch((battery_x + 1.5, battery_y + battery_height - 0.5),
|
| 306 |
+
(battery_x + 2.5, battery_y + battery_height - 0.5),
|
| 307 |
+
arrowstyle='->',
|
| 308 |
+
color='blue',
|
| 309 |
+
lw=2,
|
| 310 |
+
mutation_scale=15)
|
| 311 |
+
ax.add_patch(arrow)
|
| 312 |
+
ax.text(battery_x + 1.5, battery_y + battery_height - 0.8, f"{total_current:.1f}A", color='blue')
|
| 313 |
+
|
| 314 |
+
# Draw resistors in parallel
|
| 315 |
+
spacing = 5.0 / (num_resistors + 1)
|
| 316 |
+
for i in range(num_resistors):
|
| 317 |
+
res_x = battery_x + 2.5 + i * spacing
|
| 318 |
+
res_y_top = battery_y + battery_height - 0.5
|
| 319 |
+
res_y_bottom = battery_y + 0.5
|
| 320 |
+
|
| 321 |
+
# Vertical lines to resistor
|
| 322 |
+
ax.plot([res_x, res_x], [res_y_top, res_y_top - 1], 'k-', lw=2)
|
| 323 |
+
ax.plot([res_x, res_x], [res_y_bottom, res_y_bottom + 1], 'k-', lw=2)
|
| 324 |
+
|
| 325 |
+
# Draw resistor (zigzag horizontal)
|
| 326 |
+
zigzag_height = 1.0
|
| 327 |
+
zigzag_width = 0.5
|
| 328 |
+
zigzag_pts = 7
|
| 329 |
+
y_pts = np.linspace(res_y_top - 1, res_y_bottom + 1, zigzag_pts)
|
| 330 |
+
x_pts = np.array([0, 1, 0, 1, 0, 1, 0]) * zigzag_width + res_x
|
| 331 |
+
ax.plot(x_pts, y_pts, 'k-', lw=2)
|
| 332 |
+
|
| 333 |
+
# Branch current arrow
|
| 334 |
+
branch_arrow = FancyArrowPatch((res_x - 0.3, res_y_top - 0.5),
|
| 335 |
+
(res_x - 0.3, res_y_top - 1.5),
|
| 336 |
+
arrowstyle='->',
|
| 337 |
+
color='blue',
|
| 338 |
+
lw=2,
|
| 339 |
+
mutation_scale=15)
|
| 340 |
+
ax.add_patch(branch_arrow)
|
| 341 |
+
|
| 342 |
+
# Label resistor and current
|
| 343 |
+
ax.text(res_x + 0.6, res_y_top - 1.5, f"R{i+1}\n{resistances[i]:.1f}Ω\n{branch_currents[i]:.2f}A", fontsize=8)
|
| 344 |
+
|
| 345 |
+
ax.set_xlim(0, 12)
|
| 346 |
+
ax.set_ylim(2, 8)
|
| 347 |
+
ax.axis('equal')
|
| 348 |
+
ax.axis('off')
|
| 349 |
+
|
| 350 |
+
st.pyplot(fig)
|
| 351 |
+
|
| 352 |
+
# Show formulas
|
| 353 |
+
st.write("### Parallel Circuit Formulas")
|
| 354 |
+
st.latex(r"\frac{1}{R_{total}} = \frac{1}{R_1} + \frac{1}{R_2} + ... + \frac{1}{R_n}")
|
| 355 |
+
|
| 356 |
+
# Create the fractions part without f-strings
|
| 357 |
+
fractions_parts = []
|
| 358 |
+
for r in resistances:
|
| 359 |
+
fractions_parts.append(r"\frac{1}{" + f"{r:.1f}" + "}")
|
| 360 |
+
fractions_text = " + ".join(fractions_parts)
|
| 361 |
+
|
| 362 |
+
st.latex(r"\frac{1}{R_{total}} = " + fractions_text + f" = {inverse_resistance_sum:.4f}")
|
| 363 |
+
st.latex(r"R_{total} = \frac{1}{" + f"{inverse_resistance_sum:.4f}" + r"} = " + f"{total_resistance:.2f} \, \Omega")
|
| 364 |
+
st.latex(r"I_{total} = I_1 + I_2 + ... + I_n")
|
| 365 |
+
st.latex(f"I_{{total}} = {' + '.join([f'{i:.2f}' for i in branch_currents])} = {total_current:.2f} \, A")
|
| 366 |
+
st.latex(r"I_n = \frac{V}{R_n}")
|
| 367 |
+
|
| 368 |
+
st.sidebar.markdown('''File "D:\projects\physics-simulations\pages\basic-circuit-analysis.py", line 359
|
| 369 |
+
fractions_parts.append(r""\frac{1}{""" + f"{r:.1f}" + r""}""")
|
| 370 |
+
^
|
| 371 |
+
SyntaxError: unexpected character after line continuation character
|
| 372 |
+
### Basic Circuit Concepts
|
| 373 |
+
|
| 374 |
+
**Ohm's Law:** V = IR
|
| 375 |
+
- V: Voltage (Volts)
|
| 376 |
+
- I: Current (Amperes)
|
| 377 |
+
- R: Resistance (Ohms)
|
| 378 |
+
|
| 379 |
+
**Series Circuit Properties:**
|
| 380 |
+
- Same current through all components
|
| 381 |
+
- Rtotal = R1 + R2 + ... + Rn
|
| 382 |
+
- Vtotal = V1 + V2 + ... + Vn
|
| 383 |
+
|
| 384 |
+
**Parallel Circuit Properties:**
|
| 385 |
+
- Same voltage across all components
|
| 386 |
+
- 1/Rtotal = 1/R1 + 1/R2 + ... + 1/Rn
|
| 387 |
+
- Itotal = I1 + I2 + ... + In
|
| 388 |
+
''')
|
pages/blackbody-radiation.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
# Physical constants
|
| 6 |
+
h = 6.62607015e-34 # Planck's constant (J·s)
|
| 7 |
+
c = 299792458 # Speed of light (m/s)
|
| 8 |
+
k_B = 1.380649e-23 # Boltzmann constant (J/K)
|
| 9 |
+
b = 2.897e-3 # Wien's displacement constant (m·K)
|
| 10 |
+
|
| 11 |
+
# Planck's law function: Spectral radiance as a function of wavelength (in meters) and temperature
|
| 12 |
+
def planck(lambda_m, T):
|
| 13 |
+
"""Calculate spectral radiance B(lambda, T) in W/m²/sr/m."""
|
| 14 |
+
return (2 * h * c**2 / lambda_m**5) / (np.exp(h * c / (lambda_m * k_B * T)) - 1)
|
| 15 |
+
|
| 16 |
+
# Streamlit app
|
| 17 |
+
st.title("Blackbody Radiation Simulation")
|
| 18 |
+
|
| 19 |
+
# Educational explanation
|
| 20 |
+
st.write("""
|
| 21 |
+
### Understanding Blackbody Radiation
|
| 22 |
+
A **blackbody** is an idealized object that absorbs all incoming radiation and re-emits it based solely on its temperature. The emitted radiation's intensity and wavelength distribution are described by **Planck's law**:
|
| 23 |
+
|
| 24 |
+
\[ B(\lambda, T) = \frac{2hc^2}{\lambda^5} \cdot \frac{1}{e^{\frac{hc}{\lambda k_B T}} - 1} \]
|
| 25 |
+
|
| 26 |
+
- \( \lambda \): Wavelength (m)
|
| 27 |
+
- \( T \): Temperature (K)
|
| 28 |
+
- \( h \): Planck's constant
|
| 29 |
+
- \( c \): Speed of light
|
| 30 |
+
- \( k_B \): Boltzmann constant
|
| 31 |
+
|
| 32 |
+
The **peak wavelength** (\( \lambda_{\text{peak}} \)) shifts with temperature according to **Wien's displacement law**:
|
| 33 |
+
|
| 34 |
+
\[ \lambda_{\text{peak}} = \frac{b}{T} \]
|
| 35 |
+
|
| 36 |
+
where \( b \approx 2.897 \times 10^{-3} \, \text{m·K} \). This simulation plots the spectrum and marks the peak.
|
| 37 |
+
""")
|
| 38 |
+
|
| 39 |
+
# User input
|
| 40 |
+
T = st.slider("Temperature (K)", min_value=300, max_value=10000, value=5000, step=100,
|
| 41 |
+
help="Adjust the temperature to see how the radiation spectrum changes.")
|
| 42 |
+
normalize = st.checkbox("Normalize to maximum", value=True,
|
| 43 |
+
help="Normalize the curve to its peak value for shape comparison.")
|
| 44 |
+
|
| 45 |
+
# Wavelength array (in meters, from 50 nm to 50,000 nm)
|
| 46 |
+
lambda_m = np.linspace(5e-8, 5e-5, 1000)
|
| 47 |
+
# Calculate spectral radiance in W/m²/sr/m
|
| 48 |
+
B_m = planck(lambda_m, T)
|
| 49 |
+
# Convert to nanometers for plotting
|
| 50 |
+
lambda_nm = lambda_m * 1e9
|
| 51 |
+
# Convert spectral radiance to W/m²/sr/nm
|
| 52 |
+
B_nm = B_m / 1e9
|
| 53 |
+
|
| 54 |
+
# Prepare data for plotting
|
| 55 |
+
if normalize:
|
| 56 |
+
B_plot = B_nm / B_nm.max()
|
| 57 |
+
y_label = "Normalized Spectral Radiance"
|
| 58 |
+
else:
|
| 59 |
+
B_plot = B_nm
|
| 60 |
+
y_label = "Spectral Radiance (W/m²/sr/nm)"
|
| 61 |
+
|
| 62 |
+
# Create the plot
|
| 63 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 64 |
+
ax.plot(lambda_nm, B_plot, label=f"T = {T} K")
|
| 65 |
+
ax.set_xlabel("Wavelength (nm)")
|
| 66 |
+
ax.set_ylabel(y_label)
|
| 67 |
+
ax.set_title(f"Blackbody Radiation Spectrum at T = {T} K")
|
| 68 |
+
ax.grid(True)
|
| 69 |
+
|
| 70 |
+
# Highlight visible spectrum (400 nm to 700 nm)
|
| 71 |
+
ax.axvspan(400, 700, color='yellow', alpha=0.3, label="Visible Range")
|
| 72 |
+
|
| 73 |
+
# Calculate and mark peak wavelength
|
| 74 |
+
lambda_peak_m = b / T
|
| 75 |
+
lambda_peak_nm = lambda_peak_m * 1e9
|
| 76 |
+
ax.axvline(lambda_peak_nm, color='red', linestyle='--', label=f"Peak: {lambda_peak_nm:.2f} nm")
|
| 77 |
+
|
| 78 |
+
# Add legend
|
| 79 |
+
ax.legend()
|
| 80 |
+
|
| 81 |
+
# Display the plot in Streamlit
|
| 82 |
+
st.pyplot(fig)
|
| 83 |
+
|
| 84 |
+
# Display peak wavelength
|
| 85 |
+
st.write(f"**Peak wavelength**: {lambda_peak_nm:.2f} nm")
|
pages/capacitance.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
# Constants
|
| 6 |
+
epsilon_0 = 8.85e-12 # Permittivity of free space in F/m
|
| 7 |
+
|
| 8 |
+
# Streamlit UI
|
| 9 |
+
st.title("Capacitance Simulation")
|
| 10 |
+
st.markdown("""
|
| 11 |
+
This simulation demonstrates the capacitance of a parallel plate capacitor.
|
| 12 |
+
- **Capacitance (C)** is calculated as \( C = \epsilon_0 \frac{A}{d} \), where \(\epsilon_0 = 8.85 \times 10^{-12} \, \text{F/m}\).
|
| 13 |
+
- **Charge (Q)** is calculated as \( Q = C \times V \).
|
| 14 |
+
Adjust the sliders below to explore how \(A\), \(d\), and \(V\) affect \(C\) and \(Q\).
|
| 15 |
+
""")
|
| 16 |
+
|
| 17 |
+
# Inputs
|
| 18 |
+
A = st.slider("Plate Area (m²)", 0.01, 1.0, 0.1, step=0.01)
|
| 19 |
+
d = st.slider("Plate Separation (m)", 0.001, 0.1, 0.01, step=0.001)
|
| 20 |
+
V = st.slider("Voltage (V)", 0.0, 10.0, 5.0, step=0.1)
|
| 21 |
+
|
| 22 |
+
# Calculations
|
| 23 |
+
C = epsilon_0 * A / d
|
| 24 |
+
Q = C * V
|
| 25 |
+
|
| 26 |
+
# Display results
|
| 27 |
+
st.write(f"**Capacitance (C):** {C:.2e} F")
|
| 28 |
+
st.write(f"**Charge (Q):** {Q:.2e} C")
|
| 29 |
+
|
| 30 |
+
# Visualizations
|
| 31 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
|
| 32 |
+
|
| 33 |
+
# Capacitor diagram
|
| 34 |
+
h = 0.05 # Fixed height of plates
|
| 35 |
+
# Scale width based on sqrt(A) for visual representation
|
| 36 |
+
w = 0.1 + 0.4 * (np.sqrt(A) - np.sqrt(0.01)) / (np.sqrt(1) - np.sqrt(0.01))
|
| 37 |
+
# Scale separation based on d
|
| 38 |
+
d_scaled = 0.1 + 0.4 * (d - 0.001) / (0.1 - 0.001)
|
| 39 |
+
|
| 40 |
+
# Draw bottom plate
|
| 41 |
+
ax1.add_patch(plt.Rectangle((0.5 - w/2, 0), w, h, color='blue'))
|
| 42 |
+
# Draw top plate
|
| 43 |
+
ax1.add_patch(plt.Rectangle((0.5 - w/2, d_scaled), w, h, color='red'))
|
| 44 |
+
ax1.set_xlim(0, 1)
|
| 45 |
+
ax1.set_ylim(0, 1)
|
| 46 |
+
ax1.set_aspect('equal')
|
| 47 |
+
ax1.set_title("Parallel Plate Capacitor")
|
| 48 |
+
ax1.axis('off')
|
| 49 |
+
# Add labels
|
| 50 |
+
ax1.text(0.5, -0.1, f'A = {A:.2f} m²', ha='center')
|
| 51 |
+
ax1.text(0.5, d_scaled/2, f'd = {d:.3f} m', ha='center', va='center')
|
| 52 |
+
|
| 53 |
+
# Q vs V plot
|
| 54 |
+
V_range = np.linspace(0, 10, 100)
|
| 55 |
+
Q_range = C * V_range
|
| 56 |
+
ax2.plot(V_range, Q_range, label=f'C = {C:.2e} F')
|
| 57 |
+
ax2.plot(V, Q, 'ro', label='Current point')
|
| 58 |
+
ax2.set_xlabel('Voltage (V)')
|
| 59 |
+
ax2.set_ylabel('Charge (C)')
|
| 60 |
+
ax2.set_title('Q vs V')
|
| 61 |
+
ax2.legend()
|
| 62 |
+
ax2.grid(True)
|
| 63 |
+
|
| 64 |
+
# Display the plot
|
| 65 |
+
st.pyplot(fig)
|
pages/center-of-gravity.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
from shapely.geometry import Polygon
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
# App title and introduction
|
| 7 |
+
st.title("Center of Gravity Visualization")
|
| 8 |
+
st.markdown("""
|
| 9 |
+
The **center of gravity** is the point where the weight of an object is evenly distributed in all directions.
|
| 10 |
+
This simulation lets you explore the center of gravity for different shapes by inputting their parameters.
|
| 11 |
+
""")
|
| 12 |
+
|
| 13 |
+
# Sidebar for shape selection
|
| 14 |
+
st.sidebar.title("Shape Selection")
|
| 15 |
+
shape = st.sidebar.selectbox("Select a shape", ["Rectangle", "Triangle", "Circle", "Custom Polygon"])
|
| 16 |
+
|
| 17 |
+
# Input fields and shape definition based on selection
|
| 18 |
+
if shape == "Rectangle":
|
| 19 |
+
st.markdown("### Rectangle")
|
| 20 |
+
width = st.number_input("Width", min_value=0.0, value=1.0, step=0.1)
|
| 21 |
+
height = st.number_input("Height", min_value=0.0, value=1.0, step=0.1)
|
| 22 |
+
# Define rectangle points starting at (0,0)
|
| 23 |
+
points = [(0, 0), (width, 0), (width, height), (0, height)]
|
| 24 |
+
|
| 25 |
+
elif shape == "Triangle":
|
| 26 |
+
st.markdown("### Triangle")
|
| 27 |
+
st.markdown("Enter the coordinates of the three vertices:")
|
| 28 |
+
x1 = st.number_input("X1", value=0.0, step=0.1)
|
| 29 |
+
y1 = st.number_input("Y1", value=0.0, step=0.1)
|
| 30 |
+
x2 = st.number_input("X2", value=1.0, step=0.1)
|
| 31 |
+
y2 = st.number_input("Y2", value=0.0, step=0.1)
|
| 32 |
+
x3 = st.number_input("X3", value=0.5, step=0.1)
|
| 33 |
+
y3 = st.number_input("Y3", value=1.0, step=0.1)
|
| 34 |
+
points = [(x1, y1), (x2, y2), (x3, y3)]
|
| 35 |
+
|
| 36 |
+
elif shape == "Circle":
|
| 37 |
+
st.markdown("### Circle")
|
| 38 |
+
radius = st.number_input("Radius", min_value=0.0, value=1.0, step=0.1)
|
| 39 |
+
# Approximate circle as a polygon with 100 points
|
| 40 |
+
num_points = 100
|
| 41 |
+
points = [(radius * np.cos(theta), radius * np.sin(theta))
|
| 42 |
+
for theta in np.linspace(0, 2 * np.pi, num_points)]
|
| 43 |
+
|
| 44 |
+
elif shape == "Custom Polygon":
|
| 45 |
+
st.markdown("### Custom Polygon")
|
| 46 |
+
st.markdown("Enter the coordinates of the vertices (one per line, e.g., 'x y') in order:")
|
| 47 |
+
points_str = st.text_area("Points", value="0 0\n1 0\n1 1\n0 1")
|
| 48 |
+
points = []
|
| 49 |
+
for line in points_str.split('\n'):
|
| 50 |
+
if line.strip():
|
| 51 |
+
try:
|
| 52 |
+
x, y = map(float, line.split())
|
| 53 |
+
points.append((x, y))
|
| 54 |
+
except ValueError:
|
| 55 |
+
st.error("Invalid input: Please enter numbers separated by a space (e.g., '1 2').")
|
| 56 |
+
st.stop()
|
| 57 |
+
if len(points) < 3:
|
| 58 |
+
st.error("Need at least 3 points to form a polygon.")
|
| 59 |
+
st.stop()
|
| 60 |
+
|
| 61 |
+
# Create Polygon object and compute centroid
|
| 62 |
+
poly = Polygon(points)
|
| 63 |
+
centroid = poly.centroid
|
| 64 |
+
|
| 65 |
+
# Plotting
|
| 66 |
+
st.markdown(f"### {shape} and its Center of Gravity")
|
| 67 |
+
fig, ax = plt.subplots()
|
| 68 |
+
poly_patch = plt.Polygon(points, fill=False, edgecolor='blue')
|
| 69 |
+
ax.add_artist(poly_patch)
|
| 70 |
+
ax.plot(centroid.x, centroid.y, 'ro', label='Center of Gravity')
|
| 71 |
+
ax.set_aspect('equal') # Ensure the shape isn't distorted
|
| 72 |
+
# Set plot limits with some padding
|
| 73 |
+
xs = [p[0] for p in points]
|
| 74 |
+
ys = [p[1] for p in points]
|
| 75 |
+
min_x, max_x = min(xs), max(xs)
|
| 76 |
+
min_y, max_y = min(ys), max(ys)
|
| 77 |
+
ax.set_xlim(min_x - 1, max_x + 1)
|
| 78 |
+
ax.set_ylim(min_y - 1, max_y + 1)
|
| 79 |
+
ax.legend()
|
| 80 |
+
st.pyplot(fig)
|
| 81 |
+
|
| 82 |
+
# Display centroid coordinates
|
| 83 |
+
st.write(f"**Center of Gravity:** ({centroid.x:.2f}, {centroid.y:.2f})")
|
pages/center-of-mass.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
# Sidebar for user input
|
| 6 |
+
st.sidebar.title("Input Particles")
|
| 7 |
+
instructions = """
|
| 8 |
+
Enter particles in the format: mass,x,y
|
| 9 |
+
One particle per line.
|
| 10 |
+
For example:
|
| 11 |
+
1,0,0
|
| 12 |
+
2,3,4
|
| 13 |
+
1.5,2,1
|
| 14 |
+
"""
|
| 15 |
+
st.sidebar.markdown(instructions)
|
| 16 |
+
|
| 17 |
+
# Prefill with an example for immediate visualization
|
| 18 |
+
example_input = "1,0,0\n2,3,4\n1.5,2,1"
|
| 19 |
+
input_text = st.sidebar.text_area("Particles", value=example_input, height=200)
|
| 20 |
+
|
| 21 |
+
# Parse the input
|
| 22 |
+
particles = []
|
| 23 |
+
lines = input_text.strip().split('\n')
|
| 24 |
+
for line in lines:
|
| 25 |
+
if line.strip() == '': # Skip empty lines
|
| 26 |
+
continue
|
| 27 |
+
try:
|
| 28 |
+
m, x, y = map(float, line.split(','))
|
| 29 |
+
particles.append((m, x, y))
|
| 30 |
+
except ValueError:
|
| 31 |
+
st.error(f"Invalid input: {line}")
|
| 32 |
+
particles = [] # Reset on error
|
| 33 |
+
break
|
| 34 |
+
|
| 35 |
+
# Process and visualize if there are valid particles
|
| 36 |
+
if particles:
|
| 37 |
+
# Extract masses and coordinates
|
| 38 |
+
masses = np.array([p[0] for p in particles])
|
| 39 |
+
x_coords = np.array([p[1] for p in particles])
|
| 40 |
+
y_coords = np.array([p[2] for p in particles])
|
| 41 |
+
total_mass = np.sum(masses)
|
| 42 |
+
|
| 43 |
+
if total_mass == 0:
|
| 44 |
+
st.write("Total mass is zero. Cannot compute Center of Mass.")
|
| 45 |
+
else:
|
| 46 |
+
# Calculate Center of Mass
|
| 47 |
+
x_com = np.sum(masses * x_coords) / total_mass
|
| 48 |
+
y_com = np.sum(masses * y_coords) / total_mass
|
| 49 |
+
|
| 50 |
+
# Create the plot
|
| 51 |
+
fig, ax = plt.subplots()
|
| 52 |
+
ax.plot(x_coords, y_coords, 'bo', label='Particles') # Blue dots for particles
|
| 53 |
+
ax.plot(x_com, y_com, 'r*', markersize=15, label='Center of Mass') # Red star for COM
|
| 54 |
+
ax.set_xlabel('X')
|
| 55 |
+
ax.set_ylabel('Y')
|
| 56 |
+
ax.set_title("Center of Mass Visualization")
|
| 57 |
+
ax.legend()
|
| 58 |
+
ax.grid(True) # Add grid for readability
|
| 59 |
+
ax.set_aspect('equal') # Equal aspect ratio for accurate distances
|
| 60 |
+
|
| 61 |
+
# Set plot limits dynamically
|
| 62 |
+
if len(particles) > 1:
|
| 63 |
+
x_min, x_max = np.min(x_coords), np.max(x_coords)
|
| 64 |
+
y_min, y_max = np.min(y_coords), np.max(y_coords)
|
| 65 |
+
ax.set_xlim(x_min - 1, x_max + 1)
|
| 66 |
+
ax.set_ylim(y_min - 1, y_max + 1)
|
| 67 |
+
else:
|
| 68 |
+
ax.set_xlim(-5, 5)
|
| 69 |
+
ax.set_ylim(-5, 5)
|
| 70 |
+
|
| 71 |
+
# Display the plot in Streamlit
|
| 72 |
+
st.pyplot(fig)
|
| 73 |
+
st.write(f"Center of Mass: ({x_com:.2f}, {y_com:.2f})")
|
| 74 |
+
else:
|
| 75 |
+
st.write("Please enter at least one particle.")
|
pages/centripetal-force.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
st.title("Centripetal Force Simulation")
|
| 7 |
+
|
| 8 |
+
# Sidebar parameters for tuning the simulation
|
| 9 |
+
mass = st.sidebar.slider("Mass (kg)", 0.1, 10.0, 1.0, 0.1)
|
| 10 |
+
speed = st.sidebar.slider("Speed (m/s)", 0.1, 10.0, 5.0, 0.1)
|
| 11 |
+
radius = st.sidebar.slider("Radius (m)", 0.5, 10.0, 3.0, 0.1)
|
| 12 |
+
|
| 13 |
+
# Calculate centripetal force: F = m * v^2 / r
|
| 14 |
+
centripetal_force = mass * (speed ** 2) / radius
|
| 15 |
+
st.sidebar.write("Centripetal Force (N):", round(centripetal_force, 2))
|
| 16 |
+
|
| 17 |
+
st.write("""
|
| 18 |
+
This simulation visualizes an object in uniform circular motion.
|
| 19 |
+
The object follows a circular path and the green arrow indicates the centripetal force,
|
| 20 |
+
pointing inward toward the center of the circle.
|
| 21 |
+
""")
|
| 22 |
+
|
| 23 |
+
if st.button("Start Simulation"):
|
| 24 |
+
plot_area = st.empty() # placeholder for updating plot
|
| 25 |
+
|
| 26 |
+
# Time parameters for simulation
|
| 27 |
+
t = 0.0
|
| 28 |
+
dt = 0.05 # time step in seconds
|
| 29 |
+
simulation_duration = 20 # seconds
|
| 30 |
+
|
| 31 |
+
while t < simulation_duration:
|
| 32 |
+
# Compute angular position (theta = omega * t, where omega = speed/radius)
|
| 33 |
+
theta = (speed / radius) * t
|
| 34 |
+
x = radius * np.cos(theta)
|
| 35 |
+
y = radius * np.sin(theta)
|
| 36 |
+
|
| 37 |
+
# Set up the plot
|
| 38 |
+
fig, ax = plt.subplots(figsize=(6,6))
|
| 39 |
+
# Draw the circular path
|
| 40 |
+
circle = plt.Circle((0, 0), radius, color='blue', fill=False, linestyle='--')
|
| 41 |
+
ax.add_artist(circle)
|
| 42 |
+
# Plot the moving object as a red dot
|
| 43 |
+
ax.plot(x, y, 'ro', markersize=10)
|
| 44 |
+
# Draw the centripetal force arrow (from the object pointing to the center)
|
| 45 |
+
ax.arrow(x, y, -x, -y, head_width=0.2, head_length=0.3, fc='green', ec='green')
|
| 46 |
+
|
| 47 |
+
# Set plot limits and formatting
|
| 48 |
+
ax.set_xlim(-radius - 1, radius + 1)
|
| 49 |
+
ax.set_ylim(-radius - 1, radius + 1)
|
| 50 |
+
ax.set_aspect('equal', 'box')
|
| 51 |
+
ax.set_title("Centripetal Force Simulation")
|
| 52 |
+
ax.set_xlabel("x (m)")
|
| 53 |
+
ax.set_ylabel("y (m)")
|
| 54 |
+
|
| 55 |
+
# Update the plot in the Streamlit app
|
| 56 |
+
plot_area.pyplot(fig)
|
| 57 |
+
|
| 58 |
+
t += dt
|
| 59 |
+
time.sleep(0.05)
|
pages/chaos-theory.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
from scipy.integrate import odeint
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
|
| 6 |
+
# Define the Lorenz system
|
| 7 |
+
def lorenz(state, t, sigma, beta, rho):
|
| 8 |
+
x, y, z = state
|
| 9 |
+
dx = sigma * (y - x)
|
| 10 |
+
dy = x * (rho - z) - y
|
| 11 |
+
dz = x * y - beta * z
|
| 12 |
+
return [dx, dy, dz]
|
| 13 |
+
|
| 14 |
+
# Title and description
|
| 15 |
+
st.title("Lorenz Attractor - Chaos Theory Visualization")
|
| 16 |
+
st.write(
|
| 17 |
+
"This simulation visualizes the Lorenz attractor, a system that exhibits chaotic behavior. "
|
| 18 |
+
"Adjust the parameters using the sidebar to see how the system's trajectory changes."
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Sidebar for interactive parameter controls
|
| 22 |
+
st.sidebar.header("Simulation Parameters")
|
| 23 |
+
sigma = st.sidebar.slider("Sigma (σ)", 0.1, 20.0, 10.0)
|
| 24 |
+
beta = st.sidebar.slider("Beta (β)", 0.1, 10.0, 8.0/3.0, step=0.1)
|
| 25 |
+
rho = st.sidebar.slider("Rho (ρ)", 0.1, 50.0, 28.0, step=0.1)
|
| 26 |
+
|
| 27 |
+
initial_x = st.sidebar.number_input("Initial x", value=1.0)
|
| 28 |
+
initial_y = st.sidebar.number_input("Initial y", value=1.0)
|
| 29 |
+
initial_z = st.sidebar.number_input("Initial z", value=1.0)
|
| 30 |
+
|
| 31 |
+
t_max = st.sidebar.number_input("Simulation Time", value=40.0, min_value=1.0)
|
| 32 |
+
num_points = st.sidebar.number_input("Number of Points", value=10000, min_value=100, step=100)
|
| 33 |
+
|
| 34 |
+
# Time array for simulation
|
| 35 |
+
t = np.linspace(0, t_max, num_points)
|
| 36 |
+
|
| 37 |
+
# Initial state and integration of the Lorenz equations
|
| 38 |
+
state0 = [initial_x, initial_y, initial_z]
|
| 39 |
+
states = odeint(lorenz, state0, t, args=(sigma, beta, rho))
|
| 40 |
+
|
| 41 |
+
# Plotting the Lorenz attractor
|
| 42 |
+
fig = plt.figure(figsize=(10, 6))
|
| 43 |
+
ax = fig.add_subplot(111, projection='3d')
|
| 44 |
+
ax.plot(states[:, 0], states[:, 1], states[:, 2], lw=0.5)
|
| 45 |
+
ax.set_xlabel("X")
|
| 46 |
+
ax.set_ylabel("Y")
|
| 47 |
+
ax.set_zlabel("Z")
|
| 48 |
+
ax.set_title("Lorenz Attractor")
|
| 49 |
+
|
| 50 |
+
st.pyplot(fig)
|
pages/conservation-of-angular-momentum.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import numpy as np
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
# Set initial parameters
|
| 7 |
+
m = 1.0 # mass (kg)
|
| 8 |
+
r0 = 1.0 # initial radius (m)
|
| 9 |
+
ω0 = 1.0 # initial angular velocity (rad/s)
|
| 10 |
+
L = m * r0**2 * ω0 # angular momentum (kg·m²/s, constant)
|
| 11 |
+
|
| 12 |
+
# Streamlit app title and explanation
|
| 13 |
+
st.title("Conservation of Angular Momentum Simulation")
|
| 14 |
+
st.write("""
|
| 15 |
+
This simulation illustrates the conservation of angular momentum. Angular momentum (L) is conserved in the absence of external torque. For a point mass rotating around an axis, \( L = I \cdot \omega \), where \( I = m \cdot r^2 \) is the moment of inertia, \( m \) is the mass, \( r \) is the radius, and \( \omega \) is the angular velocity. Adjust the radius (\( r \)) using the slider below to see how \( \omega \) changes to keep \( L \) constant, similar to an ice skater spinning faster when pulling their arms in.
|
| 16 |
+
""")
|
| 17 |
+
|
| 18 |
+
# Slider for radius r
|
| 19 |
+
r = st.slider("Radius (r)", min_value=0.5, max_value=2.0, value=1.0, step=0.1)
|
| 20 |
+
|
| 21 |
+
# Compute moment of inertia I and angular velocity ω
|
| 22 |
+
I = m * r**2
|
| 23 |
+
ω = L / I
|
| 24 |
+
|
| 25 |
+
# Display current values
|
| 26 |
+
st.write(f"**Mass (m):** {m} kg")
|
| 27 |
+
st.write(f"**Initial radius (r₀):** {r0} m")
|
| 28 |
+
st.write(f"**Initial angular velocity (ω₀):** {ω0} rad/s")
|
| 29 |
+
st.write(f"**Angular momentum (L):** {L} kg·m²/s")
|
| 30 |
+
st.write(f"**Current radius (r):** {r} m")
|
| 31 |
+
st.write(f"**Current moment of inertia (I):** {I} kg·m²")
|
| 32 |
+
st.write(f"**Current angular velocity (ω):** {ω:.2f} rad/s")
|
| 33 |
+
st.write(f"**Current L = I · ω:** {I * ω:.2f} kg·m²/s")
|
| 34 |
+
|
| 35 |
+
# Animation setup
|
| 36 |
+
placeholder = st.empty()
|
| 37 |
+
dt = 0.1 # time step for animation (s)
|
| 38 |
+
num_frames = 100 # number of frames
|
| 39 |
+
|
| 40 |
+
for frame in range(num_frames):
|
| 41 |
+
t = frame * dt
|
| 42 |
+
θ = (ω * t) % (2 * np.pi) # current angle, wrapped to 0-2π
|
| 43 |
+
x = r * np.cos(θ)
|
| 44 |
+
y = r * np.sin(θ)
|
| 45 |
+
|
| 46 |
+
# Create figure
|
| 47 |
+
fig, ax = plt.subplots()
|
| 48 |
+
# Plot the circular path
|
| 49 |
+
circle = plt.Circle((0, 0), r, color='blue', fill=False)
|
| 50 |
+
ax.add_artist(circle)
|
| 51 |
+
# Plot the mass
|
| 52 |
+
ax.plot(x, y, 'ro', markersize=10)
|
| 53 |
+
# Plot a line from center to mass
|
| 54 |
+
ax.plot([0, x], [0, y], 'r-')
|
| 55 |
+
# Set axis limits and aspect
|
| 56 |
+
ax.set_xlim(-2.5, 2.5)
|
| 57 |
+
ax.set_ylim(-2.5, 2.5)
|
| 58 |
+
ax.set_aspect('equal')
|
| 59 |
+
ax.set_title(f"ω = {ω:.2f} rad/s")
|
| 60 |
+
|
| 61 |
+
# Update the placeholder with the new figure
|
| 62 |
+
placeholder.pyplot(fig)
|
| 63 |
+
plt.close(fig) # Free memory
|
| 64 |
+
|
| 65 |
+
# Control animation speed
|
| 66 |
+
time.sleep(0.05)
|
pages/conservation-of-energy.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import matplotlib.animation as animation
|
| 5 |
+
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
|
| 6 |
+
|
| 7 |
+
class EnergyConservationSimulation:
|
| 8 |
+
def __init__(self, mass=1.0, gravity=9.8, initial_height=10.0):
|
| 9 |
+
"""
|
| 10 |
+
Initialize the simulation parameters
|
| 11 |
+
|
| 12 |
+
:param mass: Mass of the object (kg)
|
| 13 |
+
:param gravity: Acceleration due to gravity (m/s^2)
|
| 14 |
+
:param initial_height: Starting height of the object (m)
|
| 15 |
+
"""
|
| 16 |
+
self.mass = mass
|
| 17 |
+
self.gravity = gravity
|
| 18 |
+
self.initial_height = initial_height
|
| 19 |
+
|
| 20 |
+
def calculate_energies(self):
|
| 21 |
+
"""
|
| 22 |
+
Calculate potential and kinetic energies at different heights
|
| 23 |
+
|
| 24 |
+
:return: Tuple of lists (heights, potential_energies, kinetic_energies, total_energies)
|
| 25 |
+
"""
|
| 26 |
+
# Create array of heights from initial height to 0
|
| 27 |
+
heights = np.linspace(0, self.initial_height, 100)
|
| 28 |
+
|
| 29 |
+
# Calculate potential energy at each height
|
| 30 |
+
potential_energies = self.mass * self.gravity * heights
|
| 31 |
+
|
| 32 |
+
# Calculate kinetic energy at each height (using conservation of energy principle)
|
| 33 |
+
kinetic_energies = self.mass * self.gravity * self.initial_height - potential_energies
|
| 34 |
+
|
| 35 |
+
# Total energy remains constant
|
| 36 |
+
total_energies = np.full_like(heights, self.mass * self.gravity * self.initial_height)
|
| 37 |
+
|
| 38 |
+
return heights, potential_energies, kinetic_energies, total_energies
|
| 39 |
+
|
| 40 |
+
def create_energy_plot(self):
|
| 41 |
+
"""
|
| 42 |
+
Create a matplotlib figure showing energy transformations
|
| 43 |
+
|
| 44 |
+
:return: matplotlib figure
|
| 45 |
+
"""
|
| 46 |
+
# Calculate energies
|
| 47 |
+
heights, pe, ke, te = self.calculate_energies()
|
| 48 |
+
|
| 49 |
+
# Create the plot
|
| 50 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 51 |
+
|
| 52 |
+
# Plot the energy curves
|
| 53 |
+
ax.plot(heights, pe, label='Potential Energy', color='blue')
|
| 54 |
+
ax.plot(heights, ke, label='Kinetic Energy', color='red')
|
| 55 |
+
ax.plot(heights, te, label='Total Energy', color='green', linestyle='--')
|
| 56 |
+
|
| 57 |
+
# Customize the plot
|
| 58 |
+
ax.set_title('Conservation of Energy: Energy vs Height', fontsize=15)
|
| 59 |
+
ax.set_xlabel('Height (m)', fontsize=12)
|
| 60 |
+
ax.set_ylabel('Energy (J)', fontsize=12)
|
| 61 |
+
ax.legend()
|
| 62 |
+
ax.grid(True, linestyle=':', alpha=0.7)
|
| 63 |
+
|
| 64 |
+
return fig
|
| 65 |
+
|
| 66 |
+
def main():
|
| 67 |
+
# Streamlit app title and description
|
| 68 |
+
st.title('Conservation of Energy Simulation')
|
| 69 |
+
st.write("""
|
| 70 |
+
## Exploring Energy Transformations
|
| 71 |
+
|
| 72 |
+
This interactive simulation demonstrates the principle of Conservation of Energy
|
| 73 |
+
for a falling object. As the object falls:
|
| 74 |
+
- Potential Energy decreases
|
| 75 |
+
- Kinetic Energy increases
|
| 76 |
+
- Total Energy remains constant
|
| 77 |
+
""")
|
| 78 |
+
|
| 79 |
+
# Sidebar for simulation parameters
|
| 80 |
+
st.sidebar.header('Simulation Parameters')
|
| 81 |
+
|
| 82 |
+
# Mass input
|
| 83 |
+
mass = st.sidebar.slider('Mass of Object (kg)',
|
| 84 |
+
min_value=0.1,
|
| 85 |
+
max_value=10.0,
|
| 86 |
+
value=1.0,
|
| 87 |
+
step=0.1)
|
| 88 |
+
|
| 89 |
+
# Initial height input
|
| 90 |
+
initial_height = st.sidebar.slider('Initial Height (m)',
|
| 91 |
+
min_value=1.0,
|
| 92 |
+
max_value=20.0,
|
| 93 |
+
value=10.0,
|
| 94 |
+
step=0.5)
|
| 95 |
+
|
| 96 |
+
# Gravity (with option to modify)
|
| 97 |
+
gravity = st.sidebar.number_input('Gravity (m/s²)',
|
| 98 |
+
min_value=0.1,
|
| 99 |
+
max_value=20.0,
|
| 100 |
+
value=9.8,
|
| 101 |
+
step=0.1)
|
| 102 |
+
|
| 103 |
+
# Create simulation instance
|
| 104 |
+
sim = EnergyConservationSimulation(
|
| 105 |
+
mass=mass,
|
| 106 |
+
gravity=gravity,
|
| 107 |
+
initial_height=initial_height
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# Generate and display the plot
|
| 111 |
+
fig = sim.create_energy_plot()
|
| 112 |
+
st.pyplot(fig)
|
| 113 |
+
|
| 114 |
+
# Additional information section
|
| 115 |
+
st.markdown("### Key Insights")
|
| 116 |
+
st.markdown("""
|
| 117 |
+
- 🔵 Blue Line: Potential Energy - Decreases as height decreases
|
| 118 |
+
- 🔴 Red Line: Kinetic Energy - Increases as height decreases
|
| 119 |
+
- 🟢 Green Dashed Line: Total Energy - Remains constant
|
| 120 |
+
|
| 121 |
+
The simulation shows how energy is transformed from potential to kinetic
|
| 122 |
+
energy while maintaining a constant total energy.
|
| 123 |
+
""")
|
| 124 |
+
|
| 125 |
+
# Calculate and display some specific values
|
| 126 |
+
heights, pe, ke, te = sim.calculate_energies()
|
| 127 |
+
col1, col2, col3 = st.columns(3)
|
| 128 |
+
|
| 129 |
+
with col1:
|
| 130 |
+
st.metric("Initial Potential Energy", f"{pe[0]:.2f} J")
|
| 131 |
+
with col2:
|
| 132 |
+
st.metric("Final Kinetic Energy", f"{ke[-1]:.2f} J")
|
| 133 |
+
with col3:
|
| 134 |
+
st.metric("Total Energy", f"{te[0]:.2f} J")
|
| 135 |
+
|
| 136 |
+
if __name__ == "__main__":
|
| 137 |
+
main()
|
pages/conservation-of-momentum.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import numpy as np
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
# Title of the app
|
| 7 |
+
st.title("Conservation of Momentum Simulation")
|
| 8 |
+
|
| 9 |
+
# Sidebar for user inputs
|
| 10 |
+
st.sidebar.title("Parameters")
|
| 11 |
+
m1 = st.sidebar.slider("Mass of Object 1 (kg)", 0.1, 10.0, 1.0, step=0.1)
|
| 12 |
+
m2 = st.sidebar.slider("Mass of Object 2 (kg)", 0.1, 10.0, 1.0, step=0.1)
|
| 13 |
+
u1 = st.sidebar.slider("Initial Velocity of Object 1 (m/s)", -10.0, 10.0, 5.0, step=0.1)
|
| 14 |
+
u2 = st.sidebar.slider("Initial Velocity of Object 2 (m/s)", -10.0, 10.0, -3.0, step=0.1)
|
| 15 |
+
initial_distance = st.sidebar.slider("Initial Distance Between Objects (m)", 1.0, 20.0, 10.0, step=0.1)
|
| 16 |
+
|
| 17 |
+
# Initial positions
|
| 18 |
+
x1 = 0.0 # Object 1 starts at origin
|
| 19 |
+
x2 = initial_distance # Object 2 starts at the initial distance
|
| 20 |
+
|
| 21 |
+
# Determine if and when a collision occurs
|
| 22 |
+
# For x1 < x2, collision happens if u1 > u2 (Object 1 catches up to Object 2)
|
| 23 |
+
if u1 > u2:
|
| 24 |
+
t_collision = (x2 - x1) / (u1 - u2)
|
| 25 |
+
collision_message = f"Time to collision: {t_collision:.2f} s"
|
| 26 |
+
else:
|
| 27 |
+
t_collision = float('inf')
|
| 28 |
+
collision_message = "Objects will not collide with these velocities."
|
| 29 |
+
|
| 30 |
+
st.write(collision_message)
|
| 31 |
+
|
| 32 |
+
# Calculate final velocities after elastic collision (if it occurs)
|
| 33 |
+
if t_collision < float('inf'):
|
| 34 |
+
v1 = (u1 * (m1 - m2) + 2 * m2 * u2) / (m1 + m2)
|
| 35 |
+
v2 = (u2 * (m2 - m1) + 2 * m1 * u1) / (m1 + m2)
|
| 36 |
+
else:
|
| 37 |
+
v1 = u1 # No collision, velocities remain unchanged
|
| 38 |
+
v2 = u2
|
| 39 |
+
|
| 40 |
+
# Simulation parameters
|
| 41 |
+
dt = 0.1 # Time step (s)
|
| 42 |
+
total_time = 10.0 # Total simulation time (s)
|
| 43 |
+
|
| 44 |
+
# Initialize simulation variables
|
| 45 |
+
pos1 = x1
|
| 46 |
+
pos2 = x2
|
| 47 |
+
vel1 = u1
|
| 48 |
+
vel2 = u2
|
| 49 |
+
collided = False
|
| 50 |
+
times = []
|
| 51 |
+
momenta1 = []
|
| 52 |
+
momenta2 = []
|
| 53 |
+
total_momenta = []
|
| 54 |
+
|
| 55 |
+
# Placeholder for the dynamic plot
|
| 56 |
+
placeholder = st.empty()
|
| 57 |
+
|
| 58 |
+
# Run the simulation
|
| 59 |
+
t = 0.0
|
| 60 |
+
while t < total_time:
|
| 61 |
+
# Check for collision
|
| 62 |
+
if not collided and t >= t_collision:
|
| 63 |
+
vel1 = v1
|
| 64 |
+
vel2 = v2
|
| 65 |
+
collided = True
|
| 66 |
+
|
| 67 |
+
# Update positions
|
| 68 |
+
pos1 += vel1 * dt
|
| 69 |
+
pos2 += vel2 * dt
|
| 70 |
+
|
| 71 |
+
# Calculate momenta
|
| 72 |
+
momentum1 = m1 * vel1
|
| 73 |
+
momentum2 = m2 * vel2
|
| 74 |
+
total_momentum = momentum1 + momentum2
|
| 75 |
+
|
| 76 |
+
# Store data for plotting
|
| 77 |
+
times.append(t)
|
| 78 |
+
momenta1.append(momentum1)
|
| 79 |
+
momenta2.append(momentum2)
|
| 80 |
+
total_momenta.append(total_momentum)
|
| 81 |
+
|
| 82 |
+
# Create the visualization
|
| 83 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
|
| 84 |
+
|
| 85 |
+
# Top plot: Positions of the objects
|
| 86 |
+
ax1.plot([pos1], [0], 'ro', markersize=10, label='Object 1 (Red)')
|
| 87 |
+
ax1.plot([pos2], [0], 'bo', markersize=10, label='Object 2 (Blue)')
|
| 88 |
+
ax1.set_xlim(-5, initial_distance + 5)
|
| 89 |
+
ax1.set_ylim(-1, 1)
|
| 90 |
+
ax1.set_xlabel('Position (m)')
|
| 91 |
+
ax1.set_title(f'Time: {t:.1f} s')
|
| 92 |
+
ax1.legend()
|
| 93 |
+
ax1.grid(True)
|
| 94 |
+
|
| 95 |
+
# Bottom plot: Momentum over time
|
| 96 |
+
ax2.plot(times, momenta1, 'b-', label='Momentum of Object 1')
|
| 97 |
+
ax2.plot(times, momenta2, 'g-', label='Momentum of Object 2')
|
| 98 |
+
ax2.plot(times, total_momenta, 'r-', label='Total Momentum')
|
| 99 |
+
ax2.set_xlabel('Time (s)')
|
| 100 |
+
ax2.set_ylabel('Momentum (kg m/s)')
|
| 101 |
+
ax2.legend()
|
| 102 |
+
ax2.grid(True)
|
| 103 |
+
|
| 104 |
+
# Update the plot in Streamlit
|
| 105 |
+
placeholder.pyplot(fig)
|
| 106 |
+
plt.close(fig)
|
| 107 |
+
|
| 108 |
+
# Increment time and add a delay for animation
|
| 109 |
+
t += dt
|
| 110 |
+
time.sleep(0.1)
|
pages/coulombs-law.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
|
| 4 |
+
# Set up the Streamlit app
|
| 5 |
+
st.title("Coulomb's Law Visualization")
|
| 6 |
+
st.write("Adjust the charges Q1, Q2, and the distance r to see the force between them.")
|
| 7 |
+
|
| 8 |
+
# Input sliders for Q1, Q2, and r
|
| 9 |
+
Q1 = st.slider("Charge Q1", min_value=-10.0, max_value=10.0, value=1.0, step=0.1)
|
| 10 |
+
Q2 = st.slider("Charge Q2", min_value=-10.0, max_value=10.0, value=1.0, step=0.1)
|
| 11 |
+
r = st.slider("Distance r", min_value=0.1, max_value=10.0, value=1.0, step=0.1)
|
| 12 |
+
|
| 13 |
+
# Calculate the force (k=1 for simplicity)
|
| 14 |
+
F = (Q1 * Q2) / r**2
|
| 15 |
+
|
| 16 |
+
# Function to determine color based on charge sign
|
| 17 |
+
def get_color(Q):
|
| 18 |
+
if Q > 0:
|
| 19 |
+
return 'blue' # Positive charge
|
| 20 |
+
elif Q < 0:
|
| 21 |
+
return 'red' # Negative charge
|
| 22 |
+
else:
|
| 23 |
+
return 'gray' # Zero charge
|
| 24 |
+
|
| 25 |
+
# Assign colors to charges
|
| 26 |
+
colors = [get_color(Q1), get_color(Q2)]
|
| 27 |
+
|
| 28 |
+
# Create the plot
|
| 29 |
+
fig, ax = plt.subplots()
|
| 30 |
+
ax.set_xlim(-1, r + 1) # Adjust x-axis based on distance r
|
| 31 |
+
ax.set_ylim(-1, 1) # Fixed y-axis for a 1D representation
|
| 32 |
+
|
| 33 |
+
# Plot the charges as points
|
| 34 |
+
ax.scatter([0, r], [0, 0], c=colors, s=100)
|
| 35 |
+
ax.text(-0.5, 0.5, f'Q1 = {Q1:.1f}') # Label for Q1
|
| 36 |
+
ax.text(r + 0.5, 0.5, f'Q2 = {Q2:.1f}') # Label for Q2
|
| 37 |
+
|
| 38 |
+
# Draw an arrow to represent the force direction on Q1
|
| 39 |
+
if F != 0:
|
| 40 |
+
# If Q1*Q2 > 0 (like charges), force is repulsive (to the left)
|
| 41 |
+
# If Q1*Q2 < 0 (unlike charges), force is attractive (to the right)
|
| 42 |
+
dx = -0.5 if Q1 * Q2 > 0 else 0.5
|
| 43 |
+
ax.quiver(0, 0, dx, 0, scale=1, scale_units='xy', angles='xy', color='green')
|
| 44 |
+
|
| 45 |
+
# Customize the plot
|
| 46 |
+
ax.set_xlabel('Distance')
|
| 47 |
+
ax.set_title("Coulomb's Law")
|
| 48 |
+
|
| 49 |
+
# Display the plot in Streamlit
|
| 50 |
+
st.pyplot(fig)
|
| 51 |
+
|
| 52 |
+
# Display force magnitude and direction
|
| 53 |
+
st.write(f"**Force magnitude:** {abs(F):.2f} units")
|
| 54 |
+
if Q1 * Q2 > 0:
|
| 55 |
+
direction = "to the left (repulsive)"
|
| 56 |
+
elif Q1 * Q2 < 0:
|
| 57 |
+
direction = "to the right (attractive)"
|
| 58 |
+
else:
|
| 59 |
+
direction = "zero"
|
| 60 |
+
st.write(f"**Force direction on Q1:** {direction}")
|
| 61 |
+
st.write("**Note:** Coulomb's constant k is set to 1 for simplicity.")
|
pages/diffraction.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
# Title and description
|
| 6 |
+
st.title("Single-Slit Diffraction Simulation")
|
| 7 |
+
st.write("Adjust the parameters below to explore how the diffraction pattern changes.")
|
| 8 |
+
|
| 9 |
+
# Theory section
|
| 10 |
+
st.markdown("### Theory")
|
| 11 |
+
st.write("In single-slit diffraction, light passing through a slit of width \(a\) produces an intensity pattern on a screen at distance \(D\). The intensity as a function of position \(y\) on the screen is given by:")
|
| 12 |
+
st.latex(r"I(y) = I_0 \left( \frac{\sin \beta}{\beta} \right)^2")
|
| 13 |
+
st.write("where")
|
| 14 |
+
st.latex(r"\beta = \frac{\pi a y}{\lambda D}")
|
| 15 |
+
st.write("- \(I_0\): maximum intensity at the center (set to 1 for relative intensity)")
|
| 16 |
+
st.write("- \(\lambda\): wavelength of the light")
|
| 17 |
+
st.write("- \(a\): slit width")
|
| 18 |
+
st.write("- \(D\): distance from slit to screen")
|
| 19 |
+
st.write("- \(y\): position on the screen")
|
| 20 |
+
st.write("The minima (dark bands) occur at:")
|
| 21 |
+
st.latex(r"y = \pm \frac{m \lambda D}{a}, \quad m = 1, 2, 3, \dots")
|
| 22 |
+
st.write("This simulation plots \(I(y)\) vs. \(y\), showing how the pattern spreads or narrows based on your inputs.")
|
| 23 |
+
|
| 24 |
+
# Parameter sliders
|
| 25 |
+
lambda_nm = st.slider("Wavelength λ (nm)", 300, 800, 500, help="Wavelength of light in nanometers (visible range: 400-700 nm)")
|
| 26 |
+
a_um = st.slider("Slit width a (μm)", 10, 1000, 100, help="Width of the slit in micrometers")
|
| 27 |
+
D_m = st.slider("Distance to screen D (m)", 0.5, 5.0, 1.0, help="Distance from slit to screen in meters")
|
| 28 |
+
|
| 29 |
+
# Convert units to meters
|
| 30 |
+
lambda_m = lambda_nm * 1e-9 # nm to m
|
| 31 |
+
a_m = a_um * 1e-6 # μm to m
|
| 32 |
+
D_m = D_m # already in m
|
| 33 |
+
|
| 34 |
+
# Generate position array (y) in meters
|
| 35 |
+
y_m = np.linspace(-0.02, 0.02, 1000) # -20 mm to 20 mm in meters
|
| 36 |
+
|
| 37 |
+
# Compute the diffraction parameter β and intensity I(y)
|
| 38 |
+
z = (a_m * y_m) / (lambda_m * D_m)
|
| 39 |
+
I = (np.sinc(z))**2 # np.sinc(x) = sin(πx)/(πx), and I = [sin(β)/β]^2
|
| 40 |
+
|
| 41 |
+
# Convert y to millimeters for plotting
|
| 42 |
+
y_mm = y_m * 1e3 # m to mm
|
| 43 |
+
|
| 44 |
+
# Create the plot
|
| 45 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 46 |
+
ax.plot(y_mm, I, color='blue')
|
| 47 |
+
ax.set_xlabel("Position on Screen (mm)")
|
| 48 |
+
ax.set_ylabel("Relative Intensity")
|
| 49 |
+
ax.set_ylim(0, 1.05) # Intensity from 0 to just above 1
|
| 50 |
+
ax.grid(True)
|
| 51 |
+
st.pyplot(fig)
|
| 52 |
+
|
| 53 |
+
# Calculate and display the position of the first minimum
|
| 54 |
+
y_min_m = lambda_m * D_m / a_m # in meters
|
| 55 |
+
y_min_mm = y_min_m * 1e3 # in millimeters
|
| 56 |
+
st.write(f"First minimum at y = ± {y_min_mm:.2f} mm")
|
pages/dimensional-analysis.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pint
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
# Initialize the unit registry from Pint
|
| 6 |
+
ureg = pint.UnitRegistry()
|
| 7 |
+
|
| 8 |
+
st.title("Dimensional Analysis Visualizer")
|
| 9 |
+
st.markdown("""
|
| 10 |
+
This interactive simulation demonstrates how dimensional analysis works for different physics equations.
|
| 11 |
+
Select an example from the sidebar, adjust the parameters, and see how the units come together to verify that the equation is dimensionally consistent.
|
| 12 |
+
""")
|
| 13 |
+
|
| 14 |
+
# Sidebar to select the example
|
| 15 |
+
option = st.sidebar.selectbox("Select a physics concept",
|
| 16 |
+
["Gravitational Force", "Pendulum Period", "Kinetic Energy"])
|
| 17 |
+
|
| 18 |
+
if option == "Gravitational Force":
|
| 19 |
+
st.header("Gravitational Force: F = G * m₁ * m₂ / r²")
|
| 20 |
+
st.markdown("""
|
| 21 |
+
**Concept:** The gravitational force between two masses is given by Newton’s law of gravitation.
|
| 22 |
+
|
| 23 |
+
**Units Check:**
|
| 24 |
+
- **G (Gravitational constant):** m³/(kg·s²)
|
| 25 |
+
- **m₁ and m₂ (Masses):** kg
|
| 26 |
+
- **r (Distance):** m
|
| 27 |
+
When you combine these, the result should have the units of force (Newton): kg·m/s².
|
| 28 |
+
""")
|
| 29 |
+
# User inputs for masses and distance
|
| 30 |
+
m1_val = st.number_input("Mass 1 (kg)", value=5.0, min_value=0.0)
|
| 31 |
+
m2_val = st.number_input("Mass 2 (kg)", value=5.0, min_value=0.0)
|
| 32 |
+
r_val = st.number_input("Distance (m)", value=1.0, min_value=0.1)
|
| 33 |
+
|
| 34 |
+
# Define constants and variables with units
|
| 35 |
+
G_val = 6.67430e-11 # Gravitational constant (SI units)
|
| 36 |
+
G = G_val * ureg("meter**3/(kilogram*second**2)")
|
| 37 |
+
m1 = m1_val * ureg.kilogram
|
| 38 |
+
m2 = m2_val * ureg.kilogram
|
| 39 |
+
r = r_val * ureg.meter
|
| 40 |
+
|
| 41 |
+
# Compute gravitational force using dimensional quantities
|
| 42 |
+
F = G * m1 * m2 / (r**2)
|
| 43 |
+
|
| 44 |
+
st.write("**Computed Gravitational Force:**", F)
|
| 45 |
+
st.write("**Dimensionality of F:**", F.dimensionality)
|
| 46 |
+
# Expected dimension for force (Newton)
|
| 47 |
+
expected = ureg.newton
|
| 48 |
+
st.write("**Expected Dimensionality (Newton):**", expected.dimensionality)
|
| 49 |
+
if F.dimensionality == expected.dimensionality:
|
| 50 |
+
st.success("Dimensional analysis checks out!")
|
| 51 |
+
else:
|
| 52 |
+
st.error("There is an inconsistency in the dimensions.")
|
| 53 |
+
|
| 54 |
+
elif option == "Pendulum Period":
|
| 55 |
+
st.header("Pendulum Period: T = 2π √(L/g)")
|
| 56 |
+
st.markdown("""
|
| 57 |
+
**Concept:** The period of a simple pendulum (for small oscillations) depends on its length L and the gravitational acceleration g.
|
| 58 |
+
|
| 59 |
+
**Units Check:**
|
| 60 |
+
- **L (Length):** m
|
| 61 |
+
- **g (Gravitational acceleration):** m/s²
|
| 62 |
+
When you take the square root of (L/g), the result has units of time (s), and multiplying by 2π (a dimensionless constant) keeps the dimension unchanged.
|
| 63 |
+
""")
|
| 64 |
+
# User inputs for pendulum length and gravitational acceleration
|
| 65 |
+
L_val = st.number_input("Length L (m)", value=1.0, min_value=0.0)
|
| 66 |
+
g_val = st.number_input("Gravitational acceleration g (m/s²)", value=9.8, min_value=0.1)
|
| 67 |
+
|
| 68 |
+
L = L_val * ureg.meter
|
| 69 |
+
g = g_val * ureg("meter/second**2")
|
| 70 |
+
|
| 71 |
+
# Calculate the period numerically (2π is dimensionless)
|
| 72 |
+
T_numeric = 2 * math.pi * math.sqrt(L_val / g_val)
|
| 73 |
+
st.write("**Computed Pendulum Period T:**", f"{T_numeric:.3f} seconds")
|
| 74 |
+
|
| 75 |
+
# Dimensional analysis using Pint: sqrt(L/g) must have the dimension of time
|
| 76 |
+
time_dim = (L / g)**0.5
|
| 77 |
+
st.write("**Dimensionality of √(L/g):**", time_dim.dimensionality)
|
| 78 |
+
expected = ureg.second
|
| 79 |
+
st.write("**Expected Dimensionality (second):**", expected.dimensionality)
|
| 80 |
+
if time_dim.dimensionality == expected.dimensionality:
|
| 81 |
+
st.success("Dimensional analysis checks out!")
|
| 82 |
+
else:
|
| 83 |
+
st.error("There is an inconsistency in the dimensions.")
|
| 84 |
+
|
| 85 |
+
elif option == "Kinetic Energy":
|
| 86 |
+
st.header("Kinetic Energy: E = ½ m v²")
|
| 87 |
+
st.markdown("""
|
| 88 |
+
**Concept:** The kinetic energy of a moving object is given by one-half the product of its mass and the square of its velocity.
|
| 89 |
+
|
| 90 |
+
**Units Check:**
|
| 91 |
+
- **m (Mass):** kg
|
| 92 |
+
- **v (Velocity):** m/s
|
| 93 |
+
Therefore, m·v² has units kg·(m²/s²), which are the units of energy (Joule).
|
| 94 |
+
""")
|
| 95 |
+
# User inputs for mass and velocity
|
| 96 |
+
m_val = st.number_input("Mass m (kg)", value=1.0, min_value=0.0)
|
| 97 |
+
v_val = st.number_input("Velocity v (m/s)", value=1.0, min_value=0.0)
|
| 98 |
+
|
| 99 |
+
m = m_val * ureg.kilogram
|
| 100 |
+
v = v_val * ureg("meter/second")
|
| 101 |
+
E = 0.5 * m * (v**2)
|
| 102 |
+
|
| 103 |
+
st.write("**Computed Kinetic Energy E:**", E)
|
| 104 |
+
st.write("**Dimensionality of E:**", E.dimensionality)
|
| 105 |
+
expected = ureg.joule
|
| 106 |
+
st.write("**Expected Dimensionality (Joule):**", expected.dimensionality)
|
| 107 |
+
if E.dimensionality == expected.dimensionality:
|
| 108 |
+
st.success("Dimensional analysis checks out!")
|
| 109 |
+
else:
|
| 110 |
+
st.error("There is an inconsistency in the dimensions.")
|
pages/distance.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
def calculate_euclidean_distance(x1, y1, x2, y2):
|
| 7 |
+
"""Calculate Euclidean distance between two points."""
|
| 8 |
+
return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
|
| 9 |
+
|
| 10 |
+
def create_distance_plot(x1, y1, x2, y2):
|
| 11 |
+
"""Create a matplotlib plot showing distance between two points."""
|
| 12 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 13 |
+
ax.plot(x1, y1, 'ro', label='Point 1') # Red dot for first point
|
| 14 |
+
ax.plot(x2, y2, 'bo', label='Point 2') # Blue dot for second point
|
| 15 |
+
ax.plot([x1, x2], [y1, y2], 'g--', label='Distance') # Green dashed line
|
| 16 |
+
ax.set_title('Distance Between Two Points')
|
| 17 |
+
ax.set_xlabel('X-axis')
|
| 18 |
+
ax.set_ylabel('Y-axis')
|
| 19 |
+
ax.grid(True)
|
| 20 |
+
ax.legend()
|
| 21 |
+
return fig
|
| 22 |
+
|
| 23 |
+
def create_manhattan_distance_plot(x1, y1, x2, y2):
|
| 24 |
+
"""Create a matplotlib plot showing Manhattan distance."""
|
| 25 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 26 |
+
ax.plot(x1, y1, 'ro', label='Point 1') # Red dot for first point
|
| 27 |
+
ax.plot(x2, y2, 'bo', label='Point 2') # Blue dot for second point
|
| 28 |
+
|
| 29 |
+
# Draw Manhattan distance path
|
| 30 |
+
ax.plot([x1, x2], [y1, y1], 'g--') # Horizontal path
|
| 31 |
+
ax.plot([x2, x2], [y1, y2], 'g--') # Vertical path
|
| 32 |
+
|
| 33 |
+
ax.set_title('Manhattan Distance Visualization')
|
| 34 |
+
ax.set_xlabel('X-axis')
|
| 35 |
+
ax.set_ylabel('Y-axis')
|
| 36 |
+
ax.grid(True)
|
| 37 |
+
ax.legend()
|
| 38 |
+
return fig
|
| 39 |
+
|
| 40 |
+
def create_distance_over_time_plot(velocity, time):
|
| 41 |
+
"""Create a matplotlib plot showing distance over time."""
|
| 42 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 43 |
+
|
| 44 |
+
# Distance vs Time plot
|
| 45 |
+
t = np.linspace(0, time, 100)
|
| 46 |
+
d = velocity * t
|
| 47 |
+
|
| 48 |
+
ax.plot(t, d, 'b-', label='Distance')
|
| 49 |
+
ax.set_title('Distance Traveled vs Time')
|
| 50 |
+
ax.set_xlabel('Time (seconds)')
|
| 51 |
+
ax.set_ylabel('Distance (units)')
|
| 52 |
+
ax.grid(True)
|
| 53 |
+
ax.legend()
|
| 54 |
+
|
| 55 |
+
# Annotate final distance
|
| 56 |
+
distance = velocity * time
|
| 57 |
+
ax.annotate(f'Final Distance: {distance} units',
|
| 58 |
+
xy=(time, distance),
|
| 59 |
+
xytext=(time/2, distance/2),
|
| 60 |
+
arrowprops=dict(facecolor='black', shrink=0.05))
|
| 61 |
+
|
| 62 |
+
return fig
|
| 63 |
+
|
| 64 |
+
def main():
|
| 65 |
+
st.title('Distance Visualization')
|
| 66 |
+
|
| 67 |
+
# Sidebar for navigation
|
| 68 |
+
app_mode = st.sidebar.selectbox('Choose Visualization Mode',
|
| 69 |
+
['Euclidean Distance', 'Manhattan Distance', 'Distance Over Time'])
|
| 70 |
+
|
| 71 |
+
if app_mode == 'Euclidean Distance':
|
| 72 |
+
st.header('Euclidean Distance Calculator')
|
| 73 |
+
|
| 74 |
+
# Input for coordinates
|
| 75 |
+
col1, col2 = st.columns(2)
|
| 76 |
+
with col1:
|
| 77 |
+
x1 = st.number_input('X1 Coordinate', value=0.0, key='x1_euc')
|
| 78 |
+
y1 = st.number_input('Y1 Coordinate', value=0.0, key='y1_euc')
|
| 79 |
+
|
| 80 |
+
with col2:
|
| 81 |
+
x2 = st.number_input('X2 Coordinate', value=3.0, key='x2_euc')
|
| 82 |
+
y2 = st.number_input('Y2 Coordinate', value=4.0, key='y2_euc')
|
| 83 |
+
|
| 84 |
+
# Calculate distance
|
| 85 |
+
distance = calculate_euclidean_distance(x1, y1, x2, y2)
|
| 86 |
+
|
| 87 |
+
# Display results
|
| 88 |
+
st.write(f'Euclidean Distance: {distance:.2f} units')
|
| 89 |
+
|
| 90 |
+
# Create and display plot
|
| 91 |
+
fig = create_distance_plot(x1, y1, x2, y2)
|
| 92 |
+
st.pyplot(fig)
|
| 93 |
+
plt.close(fig)
|
| 94 |
+
|
| 95 |
+
elif app_mode == 'Manhattan Distance':
|
| 96 |
+
st.header('Manhattan Distance Calculator')
|
| 97 |
+
|
| 98 |
+
# Input for coordinates
|
| 99 |
+
col1, col2 = st.columns(2)
|
| 100 |
+
with col1:
|
| 101 |
+
x1 = st.number_input('X1 Coordinate', value=0.0, key='x1_man')
|
| 102 |
+
y1 = st.number_input('Y1 Coordinate', value=0.0, key='y1_man')
|
| 103 |
+
|
| 104 |
+
with col2:
|
| 105 |
+
x2 = st.number_input('X2 Coordinate', value=3.0, key='x2_man')
|
| 106 |
+
y2 = st.number_input('Y2 Coordinate', value=4.0, key='y2_man')
|
| 107 |
+
|
| 108 |
+
# Calculate Manhattan distance
|
| 109 |
+
manhattan_distance = abs(x2 - x1) + abs(y2 - y1)
|
| 110 |
+
|
| 111 |
+
# Display results
|
| 112 |
+
st.write(f'Manhattan Distance: {manhattan_distance:.2f} units')
|
| 113 |
+
|
| 114 |
+
# Create Manhattan distance visualization
|
| 115 |
+
fig = create_manhattan_distance_plot(x1, y1, x2, y2)
|
| 116 |
+
st.pyplot(fig)
|
| 117 |
+
plt.close(fig)
|
| 118 |
+
|
| 119 |
+
elif app_mode == 'Distance Over Time':
|
| 120 |
+
st.header('Distance Traveled Over Time')
|
| 121 |
+
|
| 122 |
+
# Velocity input
|
| 123 |
+
velocity = st.slider('Velocity (units/second)', 1, 20, 5)
|
| 124 |
+
|
| 125 |
+
# Time input
|
| 126 |
+
time = st.slider('Time (seconds)', 1, 60, 10)
|
| 127 |
+
|
| 128 |
+
# Calculate distance
|
| 129 |
+
distance = velocity * time
|
| 130 |
+
|
| 131 |
+
# Visualization
|
| 132 |
+
fig = create_distance_over_time_plot(velocity, time)
|
| 133 |
+
st.pyplot(fig)
|
| 134 |
+
plt.close(fig)
|
| 135 |
+
|
| 136 |
+
# Display numerical results
|
| 137 |
+
st.write(f'Velocity: {velocity} units/second')
|
| 138 |
+
st.write(f'Time: {time} seconds')
|
| 139 |
+
st.write(f'Total Distance: {distance} units')
|
| 140 |
+
|
| 141 |
+
if __name__ == '__main__':
|
| 142 |
+
main()
|
pages/electic-charge.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import plotly.graph_objects as go
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
# Initialize session state to store charges
|
| 6 |
+
if 'charges' not in st.session_state:
|
| 7 |
+
st.session_state.charges = []
|
| 8 |
+
|
| 9 |
+
# User interface
|
| 10 |
+
st.title("Electric Charge Simulation")
|
| 11 |
+
st.write("Add charges and visualize the forces between them.")
|
| 12 |
+
|
| 13 |
+
# Input fields for adding a charge
|
| 14 |
+
x = st.number_input("X position", min_value=-10.0, max_value=10.0, value=0.0)
|
| 15 |
+
y = st.number_input("Y position", min_value=-10.0, max_value=10.0, value=0.0)
|
| 16 |
+
q = st.selectbox("Charge", [1, -1])
|
| 17 |
+
|
| 18 |
+
# Button to add a charge
|
| 19 |
+
if st.button("Add Charge"):
|
| 20 |
+
st.session_state.charges.append({'x': x, 'y': y, 'q': q})
|
| 21 |
+
|
| 22 |
+
# Button to clear all charges
|
| 23 |
+
if st.button("Clear All Charges"):
|
| 24 |
+
st.session_state.charges = []
|
| 25 |
+
|
| 26 |
+
# Slider to adjust force arrow scale
|
| 27 |
+
force_scale = st.slider("Force scale", min_value=0.1, max_value=10.0, value=1.0)
|
| 28 |
+
|
| 29 |
+
# Create Plotly figure
|
| 30 |
+
fig = go.Figure()
|
| 31 |
+
|
| 32 |
+
# Plot all charges
|
| 33 |
+
for charge in st.session_state.charges:
|
| 34 |
+
fig.add_trace(go.Scatter(
|
| 35 |
+
x=[charge['x']],
|
| 36 |
+
y=[charge['y']],
|
| 37 |
+
mode='markers+text',
|
| 38 |
+
text=[str(charge['q'])],
|
| 39 |
+
textposition='top right',
|
| 40 |
+
marker=dict(size=10, color='red' if charge['q'] > 0 else 'blue')
|
| 41 |
+
))
|
| 42 |
+
|
| 43 |
+
# Calculate and plot forces
|
| 44 |
+
k = 1.0 # Coulomb's constant (simplified for visualization)
|
| 45 |
+
for charge in st.session_state.charges:
|
| 46 |
+
Fx, Fy = 0, 0
|
| 47 |
+
for other in st.session_state.charges:
|
| 48 |
+
if other != charge: # Skip self-interaction
|
| 49 |
+
dx = other['x'] - charge['x']
|
| 50 |
+
dy = other['y'] - charge['y']
|
| 51 |
+
r = np.sqrt(dx**2 + dy**2)
|
| 52 |
+
if r > 0: # Avoid division by zero
|
| 53 |
+
# Force magnitude: F = k * q1 * q2 / r^2
|
| 54 |
+
F = k * charge['q'] * other['q'] / (r**2)
|
| 55 |
+
# Force components along unit vector from other to charge
|
| 56 |
+
Fx += F * (-dx / r)
|
| 57 |
+
Fy += F * (-dy / r)
|
| 58 |
+
# Draw force arrow
|
| 59 |
+
arrow_dx = Fx * force_scale
|
| 60 |
+
arrow_dy = Fy * force_scale
|
| 61 |
+
if np.sqrt(arrow_dx**2 + arrow_dy**2) > 0: # Only draw if force exists
|
| 62 |
+
fig.add_annotation(
|
| 63 |
+
x=charge['x'] + arrow_dx,
|
| 64 |
+
y=charge['y'] + arrow_dy,
|
| 65 |
+
ax=charge['x'],
|
| 66 |
+
ay=charge['y'],
|
| 67 |
+
xref='x', yref='y', axref='x', ayref='y',
|
| 68 |
+
showarrow=True,
|
| 69 |
+
arrowhead=2,
|
| 70 |
+
arrowsize=1,
|
| 71 |
+
arrowwidth=2,
|
| 72 |
+
arrowcolor='green'
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Configure plot layout
|
| 76 |
+
fig.update_layout(
|
| 77 |
+
xaxis_range=[-10, 10],
|
| 78 |
+
yaxis_range=[-10, 10],
|
| 79 |
+
width=600,
|
| 80 |
+
height=600,
|
| 81 |
+
showlegend=False,
|
| 82 |
+
title="Charge Interactions"
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Display the plot in Streamlit
|
| 86 |
+
st.plotly_chart(fig)
|
pages/electic-field.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
# Title of the Streamlit app
|
| 6 |
+
st.title("Electric Field Visualization")
|
| 7 |
+
st.write("Add charges to see how the electric field changes! Positive charges are red, negative charges are blue.")
|
| 8 |
+
|
| 9 |
+
# Initialize session state to store charges
|
| 10 |
+
if 'charges' not in st.session_state:
|
| 11 |
+
st.session_state.charges = []
|
| 12 |
+
|
| 13 |
+
# Function to calculate the electric field on a grid
|
| 14 |
+
def calculate_electric_field(charges, X, Y, k=1, epsilon=1e-6):
|
| 15 |
+
"""
|
| 16 |
+
Calculate the electric field components (E_x, E_y) at each point on the grid.
|
| 17 |
+
|
| 18 |
+
Parameters:
|
| 19 |
+
- charges: List of dictionaries with 'x', 'y', and 'q' for each charge
|
| 20 |
+
- X, Y: 2D arrays from np.meshgrid representing grid coordinates
|
| 21 |
+
- k: Coulomb's constant (set to 1 for simplicity)
|
| 22 |
+
- epsilon: Small value to avoid division by zero at charge locations
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
- E_x, E_y: Electric field components as 2D arrays
|
| 26 |
+
"""
|
| 27 |
+
E_x_total = np.zeros_like(X)
|
| 28 |
+
E_y_total = np.zeros_like(Y)
|
| 29 |
+
for charge in charges:
|
| 30 |
+
dx = X - charge['x']
|
| 31 |
+
dy = Y - charge['y']
|
| 32 |
+
r = np.sqrt(dx**2 + dy**2 + epsilon) # Distance with epsilon to avoid singularity
|
| 33 |
+
# E = k * q * r̂ / r^2, where r̂ is the unit vector (dx/r, dy/r)
|
| 34 |
+
# So, E_x = k * q * dx / r^3, E_y = k * q * dy / r^3
|
| 35 |
+
E_x_charge = k * charge['q'] * dx / r**3
|
| 36 |
+
E_y_charge = k * charge['q'] * dy / r**3
|
| 37 |
+
E_x_total += E_x_charge
|
| 38 |
+
E_y_total += E_y_charge
|
| 39 |
+
return E_x_total, E_y_total
|
| 40 |
+
|
| 41 |
+
# UI to add a charge
|
| 42 |
+
st.subheader("Add a Charge")
|
| 43 |
+
x = st.slider("X position", -5.0, 5.0, 0.0, key="x_add")
|
| 44 |
+
y = st.slider("Y position", -5.0, 5.0, 0.0, key="y_add")
|
| 45 |
+
q = st.selectbox("Charge", [1, -1], format_func=lambda val: "+1" if val > 0 else "-1", key="q_add")
|
| 46 |
+
if st.button("Add Charge", key="add_charge"):
|
| 47 |
+
st.session_state.charges.append({'x': x, 'y': y, 'q': q})
|
| 48 |
+
st.success(f"Added charge q={q} at ({x}, {y})")
|
| 49 |
+
|
| 50 |
+
# UI to display and remove charges
|
| 51 |
+
st.subheader("Current Charges")
|
| 52 |
+
if st.session_state.charges:
|
| 53 |
+
for i, charge in enumerate(st.session_state.charges):
|
| 54 |
+
st.write(f"Charge {i}: x={charge['x']}, y={charge['y']}, q={'+' if charge['q'] > 0 else '-'}1")
|
| 55 |
+
|
| 56 |
+
# Select and remove a charge
|
| 57 |
+
charge_to_remove = st.selectbox(
|
| 58 |
+
"Select charge to remove",
|
| 59 |
+
options=range(len(st.session_state.charges)),
|
| 60 |
+
format_func=lambda i: f"Charge {i}: x={st.session_state.charges[i]['x']}, y={st.session_state.charges[i]['y']}, q={'+' if st.session_state.charges[i]['q'] > 0 else '-'}1",
|
| 61 |
+
key="charge_to_remove"
|
| 62 |
+
)
|
| 63 |
+
if st.button("Remove Selected Charge", key="remove_charge"):
|
| 64 |
+
del st.session_state.charges[charge_to_remove]
|
| 65 |
+
st.success("Charge removed!")
|
| 66 |
+
else:
|
| 67 |
+
st.write("No charges added yet. Add some to see the electric field.")
|
| 68 |
+
|
| 69 |
+
# Create the plot if there are charges
|
| 70 |
+
if st.session_state.charges:
|
| 71 |
+
# Define the grid for calculation
|
| 72 |
+
x_grid = np.linspace(-5, 5, 20)
|
| 73 |
+
y_grid = np.linspace(-5, 5, 20)
|
| 74 |
+
X, Y = np.meshgrid(x_grid, y_grid)
|
| 75 |
+
|
| 76 |
+
# Calculate the electric field
|
| 77 |
+
E_x, E_y = calculate_electric_field(st.session_state.charges, X, Y)
|
| 78 |
+
|
| 79 |
+
# Create the plot
|
| 80 |
+
fig, ax = plt.subplots(figsize=(8, 8))
|
| 81 |
+
ax.streamplot(X, Y, E_x, E_y, color='black', linewidth=1, density=1.5)
|
| 82 |
+
|
| 83 |
+
# Plot each charge
|
| 84 |
+
for charge in st.session_state.charges:
|
| 85 |
+
color = 'red' if charge['q'] > 0 else 'blue'
|
| 86 |
+
ax.plot(charge['x'], charge['y'], 'o', color=color, markersize=10, label=f"q={charge['q']}")
|
| 87 |
+
|
| 88 |
+
# Set plot limits and labels
|
| 89 |
+
ax.set_xlim(-5, 5)
|
| 90 |
+
ax.set_ylim(-5, 5)
|
| 91 |
+
ax.set_xlabel("X")
|
| 92 |
+
ax.set_ylabel("Y")
|
| 93 |
+
ax.set_title("Electric Field Lines")
|
| 94 |
+
ax.grid(True)
|
| 95 |
+
|
| 96 |
+
# Display the plot in Streamlit
|
| 97 |
+
st.pyplot(fig)
|
| 98 |
+
else:
|
| 99 |
+
st.info("Add some charges above to visualize the electric field!")
|
| 100 |
+
|
| 101 |
+
# Footer
|
| 102 |
+
st.write("Note: The field lines point away from positive charges and toward negative charges, following the direction a positive test charge would move.")
|
pages/electomagnetic-induction.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from matplotlib.animation import FuncAnimation
|
| 5 |
+
import matplotlib.patches as patches
|
| 6 |
+
from matplotlib.collections import PatchCollection
|
| 7 |
+
import matplotlib.gridspec as gridspec
|
| 8 |
+
from io import BytesIO
|
| 9 |
+
import base64
|
| 10 |
+
|
| 11 |
+
# Set page config
|
| 12 |
+
st.set_page_config(
|
| 13 |
+
page_title="Electromagnetic Induction Simulator",
|
| 14 |
+
layout="wide"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# Title and description
|
| 18 |
+
st.title("Electromagnetic Induction Simulator")
|
| 19 |
+
st.markdown("""
|
| 20 |
+
This simulation demonstrates the principle of electromagnetic induction, where a changing magnetic field
|
| 21 |
+
induces an electric current in a conductor. You can adjust various parameters to see how they affect the induced voltage.
|
| 22 |
+
""")
|
| 23 |
+
|
| 24 |
+
# Create two columns for controls and visualization
|
| 25 |
+
col1, col2 = st.columns([1, 3])
|
| 26 |
+
|
| 27 |
+
# Control parameters
|
| 28 |
+
with col1:
|
| 29 |
+
st.subheader("Controls")
|
| 30 |
+
|
| 31 |
+
# Magnet properties
|
| 32 |
+
st.markdown("### Magnet Properties")
|
| 33 |
+
magnet_strength = st.slider("Magnetic Field Strength (T)", 0.1, 2.0, 1.0, 0.1)
|
| 34 |
+
magnet_speed = st.slider("Magnet Speed", 0.5, 3.0, 1.5, 0.1)
|
| 35 |
+
magnet_direction = st.radio("Magnet Motion", ["Moving In", "Moving Out", "Oscillating"])
|
| 36 |
+
|
| 37 |
+
# Coil properties
|
| 38 |
+
st.markdown("### Coil Properties")
|
| 39 |
+
coil_turns = st.slider("Number of Coil Turns", 5, 50, 20, 5)
|
| 40 |
+
coil_radius = st.slider("Coil Radius (cm)", 2.0, 8.0, 5.0, 0.5)
|
| 41 |
+
coil_resistance = st.slider("Coil Resistance (Ω)", 1.0, 20.0, 10.0, 1.0)
|
| 42 |
+
|
| 43 |
+
# Main visualization
|
| 44 |
+
with col2:
|
| 45 |
+
st.markdown("### Simulation")
|
| 46 |
+
|
| 47 |
+
# Create a placeholder for our animation
|
| 48 |
+
plot_placeholder = st.empty()
|
| 49 |
+
|
| 50 |
+
# Create a static visualization that updates with parameters
|
| 51 |
+
fig = plt.figure(figsize=(10, 8))
|
| 52 |
+
gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1])
|
| 53 |
+
|
| 54 |
+
# Top plot: Physical setup
|
| 55 |
+
ax1 = fig.add_subplot(gs[0])
|
| 56 |
+
ax1.set_xlim(-10, 10)
|
| 57 |
+
ax1.set_ylim(-10, 10)
|
| 58 |
+
ax1.set_aspect('equal')
|
| 59 |
+
ax1.set_title('Magnet and Coil Configuration')
|
| 60 |
+
ax1.set_xlabel('Position (cm)')
|
| 61 |
+
ax1.set_ylabel('Position (cm)')
|
| 62 |
+
|
| 63 |
+
# Draw the coil (circle)
|
| 64 |
+
coil = plt.Circle((0, 0), coil_radius, fill=False, color='black', linewidth=2)
|
| 65 |
+
ax1.add_artist(coil)
|
| 66 |
+
|
| 67 |
+
# Add coil turns visualization
|
| 68 |
+
theta = np.linspace(0, 2*np.pi, coil_turns+1)[:-1]
|
| 69 |
+
coil_points_x = coil_radius * np.cos(theta)
|
| 70 |
+
coil_points_y = coil_radius * np.sin(theta)
|
| 71 |
+
|
| 72 |
+
for i in range(coil_turns):
|
| 73 |
+
ax1.plot(coil_points_x[i], coil_points_y[i], 'ko', markersize=3)
|
| 74 |
+
|
| 75 |
+
# Initial magnet position depends on the selected motion
|
| 76 |
+
if magnet_direction == "Moving In":
|
| 77 |
+
magnet_pos = -9 # Start far left
|
| 78 |
+
elif magnet_direction == "Moving Out":
|
| 79 |
+
magnet_pos = 0 # Start at center
|
| 80 |
+
else: # Oscillating
|
| 81 |
+
magnet_pos = -5 # Start slightly to the left
|
| 82 |
+
|
| 83 |
+
# Draw the magnet (rectangle)
|
| 84 |
+
magnet_width, magnet_height = 2, 4
|
| 85 |
+
magnet = patches.Rectangle((magnet_pos-magnet_width/2, -magnet_height/2),
|
| 86 |
+
magnet_width, magnet_height,
|
| 87 |
+
linewidth=1, edgecolor='r', facecolor='r', alpha=0.7)
|
| 88 |
+
ax1.add_patch(magnet)
|
| 89 |
+
|
| 90 |
+
# Add N and S labels to the magnet
|
| 91 |
+
ax1.text(magnet_pos, 1, "N", fontsize=12, ha='center', color='white', fontweight='bold')
|
| 92 |
+
ax1.text(magnet_pos, -1, "S", fontsize=12, ha='center', color='white', fontweight='bold')
|
| 93 |
+
|
| 94 |
+
# Add magnetic field lines
|
| 95 |
+
def plot_magnetic_field(pos_x, strength):
|
| 96 |
+
# Clear previous field lines
|
| 97 |
+
for line in ax1.lines[:]:
|
| 98 |
+
if line not in coil_points:
|
| 99 |
+
ax1.lines.remove(line)
|
| 100 |
+
|
| 101 |
+
# Field strength indicator by number and spread of field lines
|
| 102 |
+
num_field_lines = int(strength * 5)
|
| 103 |
+
|
| 104 |
+
# Field around the magnet
|
| 105 |
+
for i in range(-num_field_lines, num_field_lines+1, 2):
|
| 106 |
+
y_offset = i * 0.3
|
| 107 |
+
|
| 108 |
+
# Field lines from N to S pole (outside the magnet)
|
| 109 |
+
if abs(y_offset) > magnet_height/2:
|
| 110 |
+
x = np.linspace(pos_x, pos_x, 20)
|
| 111 |
+
y = np.linspace(magnet_height/2, -magnet_height/2, 20)
|
| 112 |
+
ax1.plot(x, y + y_offset, 'b-', linewidth=0.7, alpha=0.6)
|
| 113 |
+
|
| 114 |
+
# Field lines extending outward (further out based on strength)
|
| 115 |
+
extent = 2 + strength * 3
|
| 116 |
+
|
| 117 |
+
# Top field lines (from North pole)
|
| 118 |
+
x_top = np.linspace(pos_x - extent, pos_x + extent, 20)
|
| 119 |
+
y_top = np.zeros_like(x_top)
|
| 120 |
+
y_curvature = 4 * strength * (1 - ((x_top - pos_x) / extent) ** 2)
|
| 121 |
+
ax1.plot(x_top, magnet_height/2 + y_curvature, 'b-', linewidth=0.7, alpha=0.6)
|
| 122 |
+
|
| 123 |
+
# Bottom field lines (from South pole)
|
| 124 |
+
x_bottom = np.linspace(pos_x - extent, pos_x + extent, 20)
|
| 125 |
+
y_bottom = np.zeros_like(x_bottom)
|
| 126 |
+
y_curvature = 4 * strength * (1 - ((x_bottom - pos_x) / extent) ** 2)
|
| 127 |
+
ax1.plot(x_bottom, -magnet_height/2 - y_curvature, 'b-', linewidth=0.7, alpha=0.6)
|
| 128 |
+
|
| 129 |
+
# Store coil points reference
|
| 130 |
+
coil_points = ax1.lines.copy()
|
| 131 |
+
|
| 132 |
+
# Initialize magnetic field lines
|
| 133 |
+
plot_magnetic_field(magnet_pos, magnet_strength)
|
| 134 |
+
|
| 135 |
+
# Add a current indicator around the coil
|
| 136 |
+
current_indicator = ax1.text(0, coil_radius + 1.5, "Current: 0 mA",
|
| 137 |
+
ha='center', fontsize=10, bbox=dict(facecolor='white', alpha=0.5))
|
| 138 |
+
|
| 139 |
+
# Bottom plot: Induced voltage over time
|
| 140 |
+
ax2 = fig.add_subplot(gs[1])
|
| 141 |
+
ax2.set_xlim(0, 100)
|
| 142 |
+
ax2.set_ylim(-10, 10)
|
| 143 |
+
ax2.set_title('Induced Voltage vs. Time')
|
| 144 |
+
ax2.set_xlabel('Time')
|
| 145 |
+
ax2.set_ylabel('Voltage (mV)')
|
| 146 |
+
ax2.grid(True)
|
| 147 |
+
|
| 148 |
+
# Initialize voltage data
|
| 149 |
+
voltage_data = np.zeros(100)
|
| 150 |
+
time_data = np.arange(100)
|
| 151 |
+
voltage_line, = ax2.plot(time_data, voltage_data, 'g-')
|
| 152 |
+
|
| 153 |
+
# Function to calculate induced voltage based on parameters
|
| 154 |
+
def calculate_induced_voltage(pos, prev_pos, strength, turns, radius):
|
| 155 |
+
# Flux change calculation based on position change
|
| 156 |
+
area = np.pi * (radius ** 2)
|
| 157 |
+
|
| 158 |
+
# Distance from coil center affects field strength (inverse square law approximation)
|
| 159 |
+
distance_factor = max(0.1, 1 / (1 + abs(pos) ** 2))
|
| 160 |
+
|
| 161 |
+
# Flux through coil
|
| 162 |
+
current_flux = strength * area * distance_factor
|
| 163 |
+
|
| 164 |
+
# Flux from previous position
|
| 165 |
+
prev_distance_factor = max(0.1, 1 / (1 + abs(prev_pos) ** 2))
|
| 166 |
+
previous_flux = strength * area * prev_distance_factor
|
| 167 |
+
|
| 168 |
+
# Change in flux
|
| 169 |
+
d_flux = current_flux - previous_flux
|
| 170 |
+
|
| 171 |
+
# Induced voltage (Faraday's law: E = -N * dΦ/dt)
|
| 172 |
+
# We approximate dt as 1 time unit
|
| 173 |
+
induced_voltage = -turns * d_flux
|
| 174 |
+
|
| 175 |
+
# Add sign based on approach/retreat
|
| 176 |
+
if magnet_direction == "Moving In":
|
| 177 |
+
if pos < 0: # Approaching from left
|
| 178 |
+
induced_voltage *= -1
|
| 179 |
+
elif magnet_direction == "Moving Out":
|
| 180 |
+
if pos > 0: # Moving away to the right
|
| 181 |
+
induced_voltage *= -1
|
| 182 |
+
|
| 183 |
+
return induced_voltage
|
| 184 |
+
|
| 185 |
+
# Function to simulate current direction
|
| 186 |
+
def show_current_direction(voltage, ax, coil_r):
|
| 187 |
+
current = voltage / coil_resistance # I = V/R
|
| 188 |
+
|
| 189 |
+
# Update current text
|
| 190 |
+
current_indicator.set_text(f"Current: {current*1000:.2f} mA")
|
| 191 |
+
|
| 192 |
+
# Remove any existing current direction indicators
|
| 193 |
+
for artist in ax.artists:
|
| 194 |
+
if hasattr(artist, 'current_indicator'):
|
| 195 |
+
artist.remove()
|
| 196 |
+
|
| 197 |
+
# If current is significant, show direction
|
| 198 |
+
if abs(current) > 0.01:
|
| 199 |
+
# Define arrow properties based on current direction
|
| 200 |
+
if current > 0:
|
| 201 |
+
color = 'g'
|
| 202 |
+
rotation = 90 # Clockwise
|
| 203 |
+
else:
|
| 204 |
+
color = 'r'
|
| 205 |
+
rotation = -90 # Counter-clockwise
|
| 206 |
+
|
| 207 |
+
# Current magnitude affects arrow size
|
| 208 |
+
arrow_size = min(1.5, max(0.5, abs(current) * 10))
|
| 209 |
+
|
| 210 |
+
# Add arrow around the coil
|
| 211 |
+
arrow = patches.FancyArrowPatch((-coil_r, 0), (coil_r, 0),
|
| 212 |
+
connectionstyle=f"arc3,rad={rotation/180}",
|
| 213 |
+
arrowstyle=f"fancy,head_width={arrow_size},head_length={arrow_size*1.5}",
|
| 214 |
+
fc=color, ec=color, alpha=0.7)
|
| 215 |
+
arrow.current_indicator = True
|
| 216 |
+
arrow.set_transform(ax.transData)
|
| 217 |
+
|
| 218 |
+
ax.add_patch(arrow)
|
| 219 |
+
|
| 220 |
+
# Animation state variables
|
| 221 |
+
animation_state = {
|
| 222 |
+
'magnet_pos': magnet_pos,
|
| 223 |
+
'previous_pos': magnet_pos,
|
| 224 |
+
'frame_count': 0
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
def update():
|
| 228 |
+
magnet_pos = animation_state['magnet_pos']
|
| 229 |
+
previous_pos = animation_state['previous_pos']
|
| 230 |
+
frame_count = animation_state['frame_count']
|
| 231 |
+
|
| 232 |
+
# Update magnet position based on selected motion
|
| 233 |
+
if magnet_direction == "Moving In":
|
| 234 |
+
magnet_pos = min(0, magnet_pos + magnet_speed * 0.2)
|
| 235 |
+
elif magnet_direction == "Moving Out":
|
| 236 |
+
magnet_pos = min(9, magnet_pos + magnet_speed * 0.2)
|
| 237 |
+
else: # Oscillating
|
| 238 |
+
magnet_pos = 5 * np.sin(frame_count * 0.1 * magnet_speed)
|
| 239 |
+
|
| 240 |
+
# Update magnet position
|
| 241 |
+
magnet.set_x(magnet_pos - magnet_width/2)
|
| 242 |
+
|
| 243 |
+
# Update magnetic field
|
| 244 |
+
plot_magnetic_field(magnet_pos, magnet_strength)
|
| 245 |
+
|
| 246 |
+
# Calculate induced voltage
|
| 247 |
+
induced_voltage = calculate_induced_voltage(
|
| 248 |
+
magnet_pos, previous_pos, magnet_strength, coil_turns, coil_radius/10) # Convert cm to dm
|
| 249 |
+
|
| 250 |
+
# Scale for display
|
| 251 |
+
display_voltage = induced_voltage * 5
|
| 252 |
+
|
| 253 |
+
# Update voltage data
|
| 254 |
+
voltage_data[:-1] = voltage_data[1:]
|
| 255 |
+
voltage_data[-1] = display_voltage
|
| 256 |
+
voltage_line.set_ydata(voltage_data)
|
| 257 |
+
|
| 258 |
+
# Update current indicator
|
| 259 |
+
show_current_direction(induced_voltage, ax1, coil_radius)
|
| 260 |
+
|
| 261 |
+
# Update previous position for next frame
|
| 262 |
+
# Update state for next frame
|
| 263 |
+
animation_state['magnet_pos'] = magnet_pos
|
| 264 |
+
animation_state['previous_pos'] = magnet_pos
|
| 265 |
+
animation_state['frame_count'] = frame_count + 1
|
| 266 |
+
# Convert plot to image
|
| 267 |
+
buf = BytesIO()
|
| 268 |
+
fig.tight_layout()
|
| 269 |
+
fig.savefig(buf, format='png', dpi=100)
|
| 270 |
+
buf.seek(0)
|
| 271 |
+
img_str = base64.b64encode(buf.read()).decode('utf-8')
|
| 272 |
+
|
| 273 |
+
return f'data:image/png;base64,{img_str}'
|
| 274 |
+
|
| 275 |
+
# Create a loop to update the plot repeatedly
|
| 276 |
+
while True:
|
| 277 |
+
img_data = update()
|
| 278 |
+
plot_placeholder.image(img_data, use_column_width=True)
|
| 279 |
+
# Sleep briefly to control animation speed
|
| 280 |
+
import time
|
| 281 |
+
time.sleep(0.1)
|
| 282 |
+
|
| 283 |
+
# Additional explanations
|
| 284 |
+
st.markdown("""
|
| 285 |
+
### How Electromagnetic Induction Works
|
| 286 |
+
|
| 287 |
+
1. **Faraday's Law**: When a magnetic field changes through a coil of wire, an electromotive force (EMF) is induced, leading to current flow if there is a complete circuit.
|
| 288 |
+
|
| 289 |
+
2. **Key Factors Affecting Induced Voltage**:
|
| 290 |
+
- Magnetic field strength: Stronger fields create greater induced voltages
|
| 291 |
+
- Number of coil turns: More turns create more induced voltage
|
| 292 |
+
- Rate of change: Faster movement of the magnet creates higher voltage
|
| 293 |
+
- Coil area: Larger coils intercept more magnetic flux
|
| 294 |
+
|
| 295 |
+
3. **Lenz's Law**: The induced current creates its own magnetic field that opposes the change that produced it.
|
| 296 |
+
|
| 297 |
+
Try moving the magnet at different speeds and directions to see how the induced voltage changes!
|
| 298 |
+
""")
|
pages/electric-potential.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import plotly.graph_objects as go
|
| 4 |
+
from scipy.constants import k as coulomb_k
|
| 5 |
+
|
| 6 |
+
# Title of the app
|
| 7 |
+
st.title("Electric Potential Visualization")
|
| 8 |
+
|
| 9 |
+
# Sidebar for user inputs
|
| 10 |
+
st.sidebar.header("Charge Configuration")
|
| 11 |
+
num_charges = st.sidebar.selectbox("Number of charges", [1, 2, 3, 4, 5])
|
| 12 |
+
|
| 13 |
+
charges = []
|
| 14 |
+
for i in range(num_charges):
|
| 15 |
+
st.sidebar.subheader(f"Charge {i+1}")
|
| 16 |
+
# Sliders for charge magnitude and position
|
| 17 |
+
Q = st.sidebar.slider(f"Q_{i+1} (arbitrary units)", min_value=-10.0, max_value=10.0, value=1.0, key=f"Q{i}")
|
| 18 |
+
x = st.sidebar.slider(f"x_{i+1}", min_value=-5.0, max_value=5.0, value=0.0, key=f"x{i}")
|
| 19 |
+
y = st.sidebar.slider(f"y_{i+1}", min_value=-5.0, max_value=5.0, value=0.0, key=f"y{i}")
|
| 20 |
+
charges.append({'Q': Q, 'x': x, 'y': y})
|
| 21 |
+
|
| 22 |
+
# Define the 2D grid for calculation
|
| 23 |
+
x = np.linspace(-10, 10, 100)
|
| 24 |
+
y = np.linspace(-10, 10, 100)
|
| 25 |
+
X, Y = np.meshgrid(x, y)
|
| 26 |
+
|
| 27 |
+
# Calculate total electric potential
|
| 28 |
+
V = np.zeros_like(X)
|
| 29 |
+
for charge in charges:
|
| 30 |
+
r = np.sqrt((X - charge['x'])**2 + (Y - charge['y'])**2)
|
| 31 |
+
# Avoid division by zero at charge location
|
| 32 |
+
r = np.where(r == 0, 1e-6, r)
|
| 33 |
+
V += coulomb_k * charge['Q'] / r
|
| 34 |
+
|
| 35 |
+
# Create interactive Plotly contour plot
|
| 36 |
+
fig = go.Figure(data=go.Contour(x=x, y=y, z=V, colorscale='RdBu', colorbar=dict(title='Potential')))
|
| 37 |
+
|
| 38 |
+
# Add charge markers
|
| 39 |
+
for charge in charges:
|
| 40 |
+
color = 'red' if charge['Q'] > 0 else 'blue'
|
| 41 |
+
fig.add_trace(go.Scatter(x=[charge['x']], y=[charge['y']], mode='markers',
|
| 42 |
+
marker=dict(color=color, size=10)))
|
| 43 |
+
|
| 44 |
+
# Customize plot layout
|
| 45 |
+
fig.update_layout(title='Electric Potential', xaxis_title='x', yaxis_title='y')
|
| 46 |
+
fig.update_yaxes(scaleanchor="x", scaleratio=1) # Equal aspect ratio
|
| 47 |
+
|
| 48 |
+
# Display the plot in Streamlit
|
| 49 |
+
st.plotly_chart(fig)
|
pages/electromagnetc-waves.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from matplotlib.animation import FuncAnimation
|
| 5 |
+
|
| 6 |
+
# Set up the figure and axis
|
| 7 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 8 |
+
ax.set_xlim(0, 4*np.pi)
|
| 9 |
+
ax.set_ylim(-2, 2)
|
| 10 |
+
ax.set_xlabel("Position (m)")
|
| 11 |
+
ax.set_ylabel("Field Strength")
|
| 12 |
+
ax.grid(True)
|
| 13 |
+
|
| 14 |
+
# Initialize empty lines for E and B fields
|
| 15 |
+
e_line, = ax.plot([], [], lw=2, label='Electric Field (E)')
|
| 16 |
+
b_line, = ax.plot([], [], lw=2, label='Magnetic Field (B)')
|
| 17 |
+
ax.legend()
|
| 18 |
+
|
| 19 |
+
# Widgets in sidebar
|
| 20 |
+
st.sidebar.header("Wave Parameters")
|
| 21 |
+
frequency = st.sidebar.slider("Frequency (Hz)", 1, 20, 5)
|
| 22 |
+
wavelength = st.sidebar.slider("Wavelength (m)", 1, 10, 5)
|
| 23 |
+
amplitude = st.sidebar.slider("Amplitude (V/m)", 0.1, 2.0, 1.0)
|
| 24 |
+
|
| 25 |
+
# Animation function
|
| 26 |
+
def animate(i):
|
| 27 |
+
x = np.linspace(0, 4*np.pi, 1000)
|
| 28 |
+
phase = 2 * np.pi * frequency * (i/30)
|
| 29 |
+
|
| 30 |
+
# Electric field (y-direction)
|
| 31 |
+
e = amplitude * np.sin((2*np.pi*x)/wavelength - phase)
|
| 32 |
+
|
| 33 |
+
# Magnetic field (z-direction, scaled by 1/c)
|
| 34 |
+
b = (amplitude/3e8) * np.sin((2*np.pi*x)/wavelength - phase)
|
| 35 |
+
|
| 36 |
+
e_line.set_data(x, e)
|
| 37 |
+
b_line.set_data(x, b)
|
| 38 |
+
return e_line, b_line
|
| 39 |
+
|
| 40 |
+
# Create and display animation
|
| 41 |
+
ani = FuncAnimation(fig, animate, frames=100, interval=50, blit=True)
|
| 42 |
+
st.pyplot(fig)
|
pages/electromagnetic-spectrum.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
|
| 6 |
+
# Constants
|
| 7 |
+
SPEED_OF_LIGHT = 3e8 # m/s
|
| 8 |
+
|
| 9 |
+
# Configure app
|
| 10 |
+
st.set_page_config(page_title="EM Spectrum Visualizer", layout="wide")
|
| 11 |
+
st.title("Electromagnetic Spectrum Visualization")
|
| 12 |
+
st.markdown("Explore different regions of the electromagnetic spectrum")
|
| 13 |
+
|
| 14 |
+
# Sidebar controls
|
| 15 |
+
with st.sidebar:
|
| 16 |
+
st.header("Configuration")
|
| 17 |
+
wavelength = st.slider("Wavelength (meters)",
|
| 18 |
+
1e-12, 100.0,
|
| 19 |
+
value=5e-7,
|
| 20 |
+
format="%.1e")
|
| 21 |
+
|
| 22 |
+
region = st.selectbox("Predefined Regions", [
|
| 23 |
+
"Radio Waves", "Microwaves", "Infrared",
|
| 24 |
+
"Visible Light", "Ultraviolet", "X-Rays", "Gamma Rays"
|
| 25 |
+
])
|
| 26 |
+
|
| 27 |
+
# Calculate frequency
|
| 28 |
+
frequency = SPEED_OF_LIGHT / wavelength
|
| 29 |
+
|
| 30 |
+
# Main display
|
| 31 |
+
tab1, tab2 = st.tabs(["Wave Visualization", "Spectrum Diagram"])
|
| 32 |
+
|
| 33 |
+
with tab1:
|
| 34 |
+
# Create wave visualization
|
| 35 |
+
x = np.linspace(0, 4*np.pi, 1000)
|
| 36 |
+
y_electric = np.sin(x * wavelength * 100) # Scaled for visualization
|
| 37 |
+
y_magnetic = np.cos(x * wavelength * 100)
|
| 38 |
+
|
| 39 |
+
fig, ax = plt.subplots(figsize=(10, 4))
|
| 40 |
+
ax.plot(x, y_electric, label='Electric Field (E)')
|
| 41 |
+
ax.plot(x, y_magnetic, label='Magnetic Field (B)')
|
| 42 |
+
ax.set_title(f"EM Wave Visualization\nλ = {wavelength:.2e} m | ν = {frequency:.2e} Hz")
|
| 43 |
+
ax.set_xlabel("Propagation Direction")
|
| 44 |
+
ax.legend()
|
| 45 |
+
ax.grid(True)
|
| 46 |
+
st.pyplot(fig)
|
| 47 |
+
|
| 48 |
+
with tab2:
|
| 49 |
+
# Create spectrum diagram
|
| 50 |
+
spectrum_data = {
|
| 51 |
+
"Region": ["Gamma", "X-Ray", "UV", "Visible", "IR", "Microwave", "Radio"],
|
| 52 |
+
"Start (m)": [1e-12, 1e-10, 4e-7, 4e-7, 7e-7, 1e-3, 1e-1],
|
| 53 |
+
"End (m)": [1e-10, 1e-8, 4e-7, 7e-7, 1e-3, 1e-1, 1e4],
|
| 54 |
+
"Color": ["purple", "blue", "violet", "#FF00FF", "red", "orange", "yellow"]
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
fig = px.bar(spectrum_data,
|
| 58 |
+
x=["Gamma", "X-Ray", "UV", "Visible", "IR", "Microwave", "Radio"],
|
| 59 |
+
y=[1,1,1,1,1,1,1],
|
| 60 |
+
color="Region",
|
| 61 |
+
color_discrete_map={r: c for r,c in zip(spectrum_data["Region"],
|
| 62 |
+
spectrum_data["Color"])},
|
| 63 |
+
orientation="h",
|
| 64 |
+
log_x=True,
|
| 65 |
+
height=400)
|
| 66 |
+
|
| 67 |
+
fig.update_layout(title="Electromagnetic Spectrum Regions",
|
| 68 |
+
xaxis_title="Wavelength (meters)",
|
| 69 |
+
yaxis_visible=False,
|
| 70 |
+
showlegend=False)
|
| 71 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 72 |
+
|
| 73 |
+
# Region information
|
| 74 |
+
region_info = {
|
| 75 |
+
"Radio Waves": {"range": "1 mm - 100 km", "uses": "Communications, broadcasting"},
|
| 76 |
+
"Microwaves": {"range": "1 mm - 1 m", "uses": "Radar, cooking"},
|
| 77 |
+
"Infrared": {"range": "700 nm - 1 mm", "uses": "Thermal imaging"},
|
| 78 |
+
"Visible Light": {"range": "400-700 nm", "uses": "Human vision, photography"},
|
| 79 |
+
"Ultraviolet": {"range": "10-400 nm", "uses": "Sterilization, astronomy"},
|
| 80 |
+
"X-Rays": {"range": "0.01-10 nm", "uses": "Medical imaging"},
|
| 81 |
+
"Gamma Rays": {"range": "<0.01 nm", "uses": "Cancer treatment"}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
st.subheader(f"Region Information: {region}")
|
| 85 |
+
col1, col2 = st.columns(2)
|
| 86 |
+
with col1:
|
| 87 |
+
st.metric("Wavelength Range", region_info[region]["range"])
|
| 88 |
+
with col2:
|
| 89 |
+
st.metric("Common Applications", region_info[region]["uses"])
|
pages/energy.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import plotly.graph_objs as go
|
| 5 |
+
import plotly.express as px
|
| 6 |
+
|
| 7 |
+
class EnergyVisualization:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
# Energy types and their descriptions
|
| 10 |
+
self.energy_types = {
|
| 11 |
+
"Kinetic Energy": {
|
| 12 |
+
"description": "Energy of motion. Depends on mass and velocity.",
|
| 13 |
+
"formula": "KE = 1/2 * m * v²",
|
| 14 |
+
"example": "A moving car, a rolling ball"
|
| 15 |
+
},
|
| 16 |
+
"Potential Energy": {
|
| 17 |
+
"description": "Stored energy due to position or configuration.",
|
| 18 |
+
"subtypes": {
|
| 19 |
+
"Gravitational": "Energy due to height in a gravitational field.",
|
| 20 |
+
"Elastic": "Energy stored in stretched or compressed springs.",
|
| 21 |
+
"Chemical": "Energy stored in chemical bonds"
|
| 22 |
+
},
|
| 23 |
+
"formula": "PE = m * g * h (for gravitational)",
|
| 24 |
+
"example": "A book on a high shelf, a stretched rubber band"
|
| 25 |
+
},
|
| 26 |
+
"Thermal Energy": {
|
| 27 |
+
"description": "Energy associated with the motion of particles.",
|
| 28 |
+
"formula": "Q = m * c * ΔT",
|
| 29 |
+
"example": "Heat from a fire, warmth of your body"
|
| 30 |
+
},
|
| 31 |
+
"Electrical Energy": {
|
| 32 |
+
"description": "Energy from electric charges and their movement.",
|
| 33 |
+
"formula": "E = V * Q",
|
| 34 |
+
"example": "Lightning, electricity in your home"
|
| 35 |
+
},
|
| 36 |
+
"Chemical Energy": {
|
| 37 |
+
"description": "Energy stored in chemical bonds between atoms and molecules.",
|
| 38 |
+
"formula": "ΔH (Change in Enthalpy)",
|
| 39 |
+
"example": "Batteries, food, fuel"
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
def display_energy_overview(self):
|
| 44 |
+
"""Create an overview of energy types"""
|
| 45 |
+
st.title("Energy Visualization and Exploration")
|
| 46 |
+
|
| 47 |
+
# Sidebar for energy type selection
|
| 48 |
+
selected_energy = st.sidebar.selectbox(
|
| 49 |
+
"Select Energy Type",
|
| 50 |
+
list(self.energy_types.keys())
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# Display details of selected energy type
|
| 54 |
+
energy_info = self.energy_types[selected_energy]
|
| 55 |
+
|
| 56 |
+
st.header(f"{selected_energy}")
|
| 57 |
+
st.write(energy_info["description"])
|
| 58 |
+
st.write(f"**Formula:** {energy_info['formula']}")
|
| 59 |
+
st.write(f"**Example:** {energy_info['example']}")
|
| 60 |
+
|
| 61 |
+
# Check for subtypes
|
| 62 |
+
if "subtypes" in energy_info:
|
| 63 |
+
st.subheader("Subtypes")
|
| 64 |
+
for subtype, sub_desc in energy_info["subtypes"].items():
|
| 65 |
+
st.write(f"- **{subtype}:** {sub_desc}")
|
| 66 |
+
|
| 67 |
+
# Interactive visualization based on energy type
|
| 68 |
+
self._visualize_energy_type(selected_energy)
|
| 69 |
+
|
| 70 |
+
def _visualize_energy_type(self, energy_type):
|
| 71 |
+
"""Create interactive visualizations for different energy types"""
|
| 72 |
+
if energy_type == "Kinetic Energy":
|
| 73 |
+
self._kinetic_energy_visualization()
|
| 74 |
+
elif energy_type == "Potential Energy":
|
| 75 |
+
self._potential_energy_visualization()
|
| 76 |
+
elif energy_type == "Thermal Energy":
|
| 77 |
+
self._thermal_energy_visualization()
|
| 78 |
+
elif energy_type == "Electrical Energy":
|
| 79 |
+
self._electrical_energy_visualization()
|
| 80 |
+
elif energy_type == "Chemical Energy":
|
| 81 |
+
self._chemical_energy_visualization()
|
| 82 |
+
|
| 83 |
+
def _kinetic_energy_visualization(self):
|
| 84 |
+
"""Visualization for Kinetic Energy"""
|
| 85 |
+
st.subheader("Kinetic Energy Interactive Simulation")
|
| 86 |
+
|
| 87 |
+
# Mass and velocity sliders
|
| 88 |
+
mass = st.slider("Mass (kg)", 1.0, 100.0, 10.0, 0.1)
|
| 89 |
+
velocity = st.slider("Velocity (m/s)", 0.0, 50.0, 10.0, 0.1)
|
| 90 |
+
|
| 91 |
+
# Calculate Kinetic Energy
|
| 92 |
+
ke = 0.5 * mass * (velocity ** 2)
|
| 93 |
+
|
| 94 |
+
# Plotly visualization
|
| 95 |
+
fig = go.Figure(data=[go.Bar(
|
| 96 |
+
x=['Kinetic Energy'],
|
| 97 |
+
y=[ke],
|
| 98 |
+
text=[f'{ke:.2f} J'],
|
| 99 |
+
textposition='auto'
|
| 100 |
+
)])
|
| 101 |
+
fig.update_layout(
|
| 102 |
+
title='Kinetic Energy Calculation',
|
| 103 |
+
yaxis_title='Energy (Joules)'
|
| 104 |
+
)
|
| 105 |
+
st.plotly_chart(fig)
|
| 106 |
+
|
| 107 |
+
st.write(f"**Calculation:** KE = 1/2 * {mass} * {velocity}² = {ke:.2f} Joules")
|
| 108 |
+
|
| 109 |
+
def _potential_energy_visualization(self):
|
| 110 |
+
"""Visualization for Potential Energy"""
|
| 111 |
+
st.subheader("Gravitational Potential Energy Simulation")
|
| 112 |
+
|
| 113 |
+
# Inputs for potential energy calculation
|
| 114 |
+
mass = st.slider("Mass (kg)", 1.0, 100.0, 10.0, 0.1)
|
| 115 |
+
height = st.slider("Height (m)", 0.0, 50.0, 10.0, 0.1)
|
| 116 |
+
g = 9.8 # acceleration due to gravity
|
| 117 |
+
|
| 118 |
+
# Calculate Potential Energy
|
| 119 |
+
pe = mass * g * height
|
| 120 |
+
|
| 121 |
+
# Create a simple visualization
|
| 122 |
+
fig = plt.figure(figsize=(8, 6))
|
| 123 |
+
plt.title("Gravitational Potential Energy")
|
| 124 |
+
plt.bar(['Potential Energy'], [pe], color='green')
|
| 125 |
+
plt.ylabel('Energy (Joules)')
|
| 126 |
+
plt.text(0, pe, f'{pe:.2f} J', ha='center', va='bottom')
|
| 127 |
+
st.pyplot(fig)
|
| 128 |
+
|
| 129 |
+
st.write(f"**Calculation:** PE = {mass} * {g} * {height} = {pe:.2f} Joules")
|
| 130 |
+
|
| 131 |
+
def _thermal_energy_visualization(self):
|
| 132 |
+
"""Visualization for Thermal Energy"""
|
| 133 |
+
st.subheader("Thermal Energy Temperature Simulation")
|
| 134 |
+
|
| 135 |
+
# Inputs for thermal energy
|
| 136 |
+
mass = st.slider("Mass (kg)", 0.1, 10.0, 1.0, 0.1)
|
| 137 |
+
specific_heat = st.slider("Specific Heat Capacity (J/kg·K)", 100, 5000, 1000, 10)
|
| 138 |
+
delta_temp = st.slider("Temperature Change (°C)", -100, 100, 20, 1)
|
| 139 |
+
|
| 140 |
+
# Calculate Thermal Energy
|
| 141 |
+
thermal_energy = mass * specific_heat * delta_temp
|
| 142 |
+
|
| 143 |
+
# Plotly heat map visualization
|
| 144 |
+
temps = np.linspace(-100, 100, 100)
|
| 145 |
+
heat_map = [mass * specific_heat * t for t in temps]
|
| 146 |
+
|
| 147 |
+
fig = go.Figure(data=[go.Scatter(
|
| 148 |
+
x=temps,
|
| 149 |
+
y=heat_map,
|
| 150 |
+
mode='lines',
|
| 151 |
+
name='Thermal Energy'
|
| 152 |
+
)])
|
| 153 |
+
fig.update_layout(
|
| 154 |
+
title='Thermal Energy vs Temperature',
|
| 155 |
+
xaxis_title='Temperature Change (°C)',
|
| 156 |
+
yaxis_title='Thermal Energy (Joules)'
|
| 157 |
+
)
|
| 158 |
+
st.plotly_chart(fig)
|
| 159 |
+
|
| 160 |
+
st.write(f"**Calculation:** Q = {mass} * {specific_heat} * {delta_temp} = {thermal_energy:.2f} Joules")
|
| 161 |
+
|
| 162 |
+
def _electrical_energy_visualization(self):
|
| 163 |
+
"""Visualization for Electrical Energy"""
|
| 164 |
+
st.subheader("Electrical Energy Simulation")
|
| 165 |
+
|
| 166 |
+
# Inputs for electrical energy
|
| 167 |
+
voltage = st.slider("Voltage (V)", 1, 240, 120, 1)
|
| 168 |
+
charge = st.slider("Charge (Coulombs)", 0.1, 10.0, 1.0, 0.1)
|
| 169 |
+
|
| 170 |
+
# Calculate Electrical Energy
|
| 171 |
+
electrical_energy = voltage * charge
|
| 172 |
+
|
| 173 |
+
# Plotly bar chart
|
| 174 |
+
fig = go.Figure(data=[go.Bar(
|
| 175 |
+
x=['Electrical Energy'],
|
| 176 |
+
y=[electrical_energy],
|
| 177 |
+
text=[f'{electrical_energy:.2f} J'],
|
| 178 |
+
textposition='auto'
|
| 179 |
+
)])
|
| 180 |
+
fig.update_layout(
|
| 181 |
+
title='Electrical Energy Calculation',
|
| 182 |
+
yaxis_title='Energy (Joules)'
|
| 183 |
+
)
|
| 184 |
+
st.plotly_chart(fig)
|
| 185 |
+
|
| 186 |
+
st.write(f"**Calculation:** E = {voltage} * {charge} = {electrical_energy:.2f} Joules")
|
| 187 |
+
|
| 188 |
+
def _chemical_energy_visualization(self):
|
| 189 |
+
"""Visualization for Chemical Energy"""
|
| 190 |
+
st.subheader("Chemical Energy Bond Simulation")
|
| 191 |
+
|
| 192 |
+
# Chemical bond energy types
|
| 193 |
+
bond_energies = {
|
| 194 |
+
"Hydrogen Bond": 20,
|
| 195 |
+
"Ionic Bond": 700,
|
| 196 |
+
"Covalent Bond": 350,
|
| 197 |
+
"Metallic Bond": 250
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
# Multiselect for bond types
|
| 201 |
+
selected_bonds = st.multiselect(
|
| 202 |
+
"Select Bond Types",
|
| 203 |
+
list(bond_energies.keys()),
|
| 204 |
+
default=["Covalent Bond"]
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# Calculate total bond energy
|
| 208 |
+
total_bond_energy = sum(bond_energies[bond] for bond in selected_bonds)
|
| 209 |
+
|
| 210 |
+
# Pie chart visualization
|
| 211 |
+
fig = go.Figure(data=[go.Pie(
|
| 212 |
+
labels=selected_bonds,
|
| 213 |
+
values=[bond_energies[bond] for bond in selected_bonds],
|
| 214 |
+
hole=.3
|
| 215 |
+
)])
|
| 216 |
+
fig.update_layout(title='Chemical Bond Energy Distribution')
|
| 217 |
+
st.plotly_chart(fig)
|
| 218 |
+
|
| 219 |
+
st.write("**Bond Energies:**")
|
| 220 |
+
for bond in selected_bonds:
|
| 221 |
+
st.write(f"- {bond}: {bond_energies[bond]} kJ/mol")
|
| 222 |
+
st.write(f"**Total Bond Energy:** {total_bond_energy} kJ/mol")
|
| 223 |
+
|
| 224 |
+
def main():
|
| 225 |
+
# Initialize and run the Energy Visualization
|
| 226 |
+
energy_viz = EnergyVisualization()
|
| 227 |
+
energy_viz.display_energy_overview()
|
| 228 |
+
|
| 229 |
+
if __name__ == "__main__":
|
| 230 |
+
main()
|
| 231 |
+
|
| 232 |
+
# Requirements for this Streamlit app
|
| 233 |
+
# Run this in terminal:
|
| 234 |
+
# pip install streamlit numpy matplotlib plotly
|
| 235 |
+
# streamlit run energy_visualization.py
|
pages/farady-law.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from mpl_toolkits.mplot3d import Axes3D
|
| 5 |
+
|
| 6 |
+
# Set up the page
|
| 7 |
+
st.set_page_config(page_title="Faraday's Law Visualization", layout="wide")
|
| 8 |
+
st.title("Visualization of Faraday's Law of Electromagnetic Induction")
|
| 9 |
+
|
| 10 |
+
# Theory section
|
| 11 |
+
st.markdown("""
|
| 12 |
+
## Faraday's Law
|
| 13 |
+
Faraday's Law states that the induced electromotive force (EMF) in a closed circuit is equal to:
|
| 14 |
+
""")
|
| 15 |
+
st.latex(r'''\varepsilon = -N \frac{d\Phi_B}{dt}''')
|
| 16 |
+
st.markdown("""
|
| 17 |
+
Where:
|
| 18 |
+
- ε = Induced EMF (Volts)
|
| 19 |
+
- N = Number of turns in coil
|
| 20 |
+
- Φ_B = Magnetic flux (Weber)
|
| 21 |
+
- t = Time (seconds)
|
| 22 |
+
|
| 23 |
+
Magnetic flux is given by:
|
| 24 |
+
""")
|
| 25 |
+
st.latex(r'''\Phi_B = B \cdot A \cdot \cos(\theta)''')
|
| 26 |
+
|
| 27 |
+
# Sidebar controls
|
| 28 |
+
st.sidebar.header("Simulation Parameters")
|
| 29 |
+
B0 = st.sidebar.slider("Peak Magnetic Field (T)", 0.1, 10.0, 2.0)
|
| 30 |
+
N = st.sidebar.slider("Number of Turns", 1, 100, 50)
|
| 31 |
+
radius = st.sidebar.slider("Coil Radius (m)", 0.1, 2.0, 0.5)
|
| 32 |
+
theta_deg = st.sidebar.slider("Angle between B and Normal (degrees)", 0, 180, 45)
|
| 33 |
+
freq = st.sidebar.slider("Oscillation Frequency (Hz)", 0.1, 10.0, 1.0)
|
| 34 |
+
|
| 35 |
+
# Convert angle to radians
|
| 36 |
+
theta = np.deg2rad(theta_deg)
|
| 37 |
+
|
| 38 |
+
# Time array
|
| 39 |
+
t = np.linspace(0, 2, 1000) # 2 seconds simulation
|
| 40 |
+
|
| 41 |
+
# Calculations
|
| 42 |
+
B = B0 * np.sin(2 * np.pi * freq * t) # Time-varying magnetic field
|
| 43 |
+
A = np.pi * radius**2 # Coil area
|
| 44 |
+
flux = N * B * A * np.cos(theta) # Magnetic flux
|
| 45 |
+
emf = -np.gradient(flux, t) # Induced EMF
|
| 46 |
+
|
| 47 |
+
# Create figure for plots
|
| 48 |
+
fig = plt.figure(figsize=(12, 10))
|
| 49 |
+
|
| 50 |
+
# Magnetic field plot
|
| 51 |
+
ax1 = plt.subplot(311)
|
| 52 |
+
ax1.plot(t, B, color='tab:red')
|
| 53 |
+
ax1.set_ylabel('Magnetic Field (T)')
|
| 54 |
+
ax1.set_title('Time-varying Magnetic Field')
|
| 55 |
+
ax1.grid(True)
|
| 56 |
+
|
| 57 |
+
# Magnetic flux plot
|
| 58 |
+
ax2 = plt.subplot(312)
|
| 59 |
+
ax2.plot(t, flux, color='tab:green')
|
| 60 |
+
ax2.set_ylabel('Magnetic Flux (Wb)')
|
| 61 |
+
ax2.set_title('Magnetic Flux Through Coil')
|
| 62 |
+
ax2.grid(True)
|
| 63 |
+
|
| 64 |
+
# Induced EMF plot
|
| 65 |
+
ax3 = plt.subplot(313)
|
| 66 |
+
ax3.plot(t, emf, color='tab:blue')
|
| 67 |
+
ax3.set_ylabel('Induced EMF (V)')
|
| 68 |
+
ax3.set_xlabel('Time (s)')
|
| 69 |
+
ax3.set_title('Induced Electromotive Force')
|
| 70 |
+
ax3.grid(True)
|
| 71 |
+
|
| 72 |
+
plt.tight_layout()
|
| 73 |
+
st.pyplot(fig)
|
| 74 |
+
|
| 75 |
+
# 3D Visualization
|
| 76 |
+
st.markdown("### 3D Visualization of Coil Orientation")
|
| 77 |
+
|
| 78 |
+
fig3d = plt.figure(figsize=(8, 8))
|
| 79 |
+
ax3d = fig3d.add_subplot(111, projection='3d')
|
| 80 |
+
|
| 81 |
+
# Generate coil coordinates
|
| 82 |
+
theta_coil = np.linspace(0, 2*np.pi, 100)
|
| 83 |
+
x = radius * np.cos(theta_coil)
|
| 84 |
+
y = radius * np.sin(theta_coil) * np.cos(theta)
|
| 85 |
+
z = radius * np.sin(theta_coil) * np.sin(theta)
|
| 86 |
+
|
| 87 |
+
# Plot coil
|
| 88 |
+
ax3d.plot(x, y, z, color='blue', linewidth=2, label='Coil')
|
| 89 |
+
|
| 90 |
+
# Plot magnetic field direction
|
| 91 |
+
ax3d.quiver(0, 0, -1, 0, 0, 2, color='red',
|
| 92 |
+
linewidth=3, arrow_length_ratio=0.1, label='Magnetic Field (B)')
|
| 93 |
+
|
| 94 |
+
# Set plot limits and labels
|
| 95 |
+
ax3d.set_xlim([-radius*1.5, radius*1.5])
|
| 96 |
+
ax3d.set_ylim([-radius*1.5, radius*1.5])
|
| 97 |
+
ax3d.set_zlim([-radius*1.5, radius*1.5])
|
| 98 |
+
ax3d.set_xlabel('X-axis')
|
| 99 |
+
ax3d.set_ylabel('Y-axis')
|
| 100 |
+
ax3d.set_zlabel('Z-axis')
|
| 101 |
+
ax3d.set_title('Coil Orientation Relative to Magnetic Field')
|
| 102 |
+
ax3d.legend()
|
| 103 |
+
|
| 104 |
+
st.pyplot(fig3d)
|
| 105 |
+
|
| 106 |
+
# Explanation
|
| 107 |
+
st.markdown("""
|
| 108 |
+
## Explanation
|
| 109 |
+
1. **Magnetic Field (Red Plot):** Shows the sinusoidal variation of the external magnetic field
|
| 110 |
+
2. **Magnetic Flux (Green Plot):** Demonstrates the flux through the coil depending on angle θ
|
| 111 |
+
3. **Induced EMF (Blue Plot):** Shows the EMF generated by the changing magnetic flux
|
| 112 |
+
|
| 113 |
+
The 3D visualization shows:
|
| 114 |
+
- Blue coil in its orientation relative to the magnetic field (red arrow)
|
| 115 |
+
- The angle θ between the coil's normal vector and magnetic field direction
|
| 116 |
+
""")
|
pages/field-concepts.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
# Introduction and instructions
|
| 6 |
+
st.markdown("""
|
| 7 |
+
# Electric Field Visualization
|
| 8 |
+
|
| 9 |
+
This app helps you visualize the electric field created by point charges in a 2D space.
|
| 10 |
+
|
| 11 |
+
### How to Use
|
| 12 |
+
- In the sidebar, enter the charges in the format `x,y,Q` (one per line), where:
|
| 13 |
+
- `x` and `y` are the coordinates of the charge.
|
| 14 |
+
- `Q` is the charge value (positive or negative).
|
| 15 |
+
- Example: `0,0,1` places a positive charge of 1 at the origin, and `3,0,-1` places a negative charge at (3,0).
|
| 16 |
+
- Select the visualization type:
|
| 17 |
+
- **Quiver Plot**: Shows arrows representing the direction and strength of the electric field.
|
| 18 |
+
- **Field Lines**: Displays lines that follow the electric field's direction.
|
| 19 |
+
|
| 20 |
+
Try starting with `0,0,1` and `3,0,-1` to see a dipole field!
|
| 21 |
+
""")
|
| 22 |
+
|
| 23 |
+
# Sidebar inputs
|
| 24 |
+
charges_input = st.sidebar.text_area(
|
| 25 |
+
"Enter charges (x,y,Q) one per line",
|
| 26 |
+
"0,0,1\n3,0,-1"
|
| 27 |
+
)
|
| 28 |
+
vis_type = st.sidebar.radio("Visualization Type", ["Quiver Plot", "Field Lines"])
|
| 29 |
+
|
| 30 |
+
# Parse the input charges
|
| 31 |
+
charges = []
|
| 32 |
+
for line in charges_input.split('\n'):
|
| 33 |
+
if line.strip():
|
| 34 |
+
try:
|
| 35 |
+
x, y, Q = map(float, line.split(','))
|
| 36 |
+
charges.append((x, y, Q))
|
| 37 |
+
except ValueError:
|
| 38 |
+
st.sidebar.error(f"Invalid input: {line}. Use format x,y,Q.")
|
| 39 |
+
st.stop()
|
| 40 |
+
|
| 41 |
+
# Check if there are any charges
|
| 42 |
+
if not charges:
|
| 43 |
+
st.warning("No charges entered. Please add charges in the sidebar.")
|
| 44 |
+
st.stop()
|
| 45 |
+
|
| 46 |
+
# Function to compute the electric field at a point (x, y)
|
| 47 |
+
def electric_field(x, y, charges):
|
| 48 |
+
Ex, Ey = 0, 0
|
| 49 |
+
for cx, cy, Q in charges:
|
| 50 |
+
dx = x - cx
|
| 51 |
+
dy = y - cy
|
| 52 |
+
r2 = dx**2 + dy**2
|
| 53 |
+
# Avoid division by zero (when point is exactly at a charge)
|
| 54 |
+
if r2 < 1e-10:
|
| 55 |
+
continue
|
| 56 |
+
r3 = r2**1.5
|
| 57 |
+
Ex += Q * dx / r3
|
| 58 |
+
Ey += Q * dy / r3
|
| 59 |
+
return Ex, Ey
|
| 60 |
+
|
| 61 |
+
# Set up the plot
|
| 62 |
+
fig, ax = plt.subplots(figsize=(8, 8))
|
| 63 |
+
|
| 64 |
+
# Define the grid for quiver plot
|
| 65 |
+
x = np.arange(-10, 11, 1)
|
| 66 |
+
y = np.arange(-10, 11, 1)
|
| 67 |
+
X, Y = np.meshgrid(x, y)
|
| 68 |
+
|
| 69 |
+
if vis_type == "Quiver Plot":
|
| 70 |
+
# Compute electric field across the grid
|
| 71 |
+
Ex = np.zeros_like(X, dtype=float)
|
| 72 |
+
Ey = np.zeros_like(Y, dtype=float)
|
| 73 |
+
for i in range(X.shape[0]):
|
| 74 |
+
for j in range(X.shape[1]):
|
| 75 |
+
Ex[i, j], Ey[i, j] = electric_field(X[i, j], Y[i, j], charges)
|
| 76 |
+
|
| 77 |
+
# Calculate field magnitude for coloring
|
| 78 |
+
M = np.sqrt(Ex**2 + Ey**2)
|
| 79 |
+
|
| 80 |
+
# Plot quiver with magnitude-based coloring
|
| 81 |
+
quiv = ax.quiver(X, Y, Ex, Ey, M, cmap='viridis', scale=50)
|
| 82 |
+
fig.colorbar(quiv, ax=ax, label='Field Magnitude')
|
| 83 |
+
|
| 84 |
+
else: # Field Lines
|
| 85 |
+
# Generate starting points around each charge
|
| 86 |
+
starting_points = []
|
| 87 |
+
for cx, cy, Q in charges:
|
| 88 |
+
# 8 points around each charge at a small radius
|
| 89 |
+
for angle in np.linspace(0, 2*np.pi, 8, endpoint=False):
|
| 90 |
+
x0 = cx + 0.1 * np.cos(angle)
|
| 91 |
+
y0 = cy + 0.1 * np.sin(angle)
|
| 92 |
+
starting_points.append((x0, y0))
|
| 93 |
+
|
| 94 |
+
# Trace field lines from each starting point
|
| 95 |
+
for x0, y0 in starting_points:
|
| 96 |
+
path = [(x0, y0)]
|
| 97 |
+
for _ in range(100): # Maximum steps
|
| 98 |
+
Ex, Ey = electric_field(x0, y0, charges)
|
| 99 |
+
mag = np.sqrt(Ex**2 + Ey**2)
|
| 100 |
+
if mag < 1e-5: # Stop if field is too weak
|
| 101 |
+
break
|
| 102 |
+
# Normalize step size
|
| 103 |
+
step_x = 0.1 * Ex / mag
|
| 104 |
+
step_y = 0.1 * Ey / mag
|
| 105 |
+
x0 += step_x
|
| 106 |
+
y0 += step_y
|
| 107 |
+
# Stop if outside the plot boundaries
|
| 108 |
+
if x0 < -10 or x0 > 10 or y0 < -10 or y0 > 10:
|
| 109 |
+
break
|
| 110 |
+
path.append((x0, y0))
|
| 111 |
+
|
| 112 |
+
# Plot the field line
|
| 113 |
+
px, py = zip(*path)
|
| 114 |
+
ax.plot(px, py, 'k-', linewidth=1)
|
| 115 |
+
|
| 116 |
+
# Plot the charges
|
| 117 |
+
for cx, cy, Q in charges:
|
| 118 |
+
color = 'red' if Q > 0 else 'blue'
|
| 119 |
+
ax.plot(cx, cy, 'o', color=color, markersize=10, label=f'Q={Q}')
|
| 120 |
+
|
| 121 |
+
# Customize the plot
|
| 122 |
+
ax.set_xlim(-10, 10)
|
| 123 |
+
ax.set_ylim(-10, 10)
|
| 124 |
+
ax.set_aspect('equal')
|
| 125 |
+
ax.set_xlabel('x')
|
| 126 |
+
ax.set_ylabel('y')
|
| 127 |
+
ax.set_title(f'Electric Field - {vis_type}')
|
| 128 |
+
ax.grid(True)
|
| 129 |
+
|
| 130 |
+
# Display the plot in Streamlit
|
| 131 |
+
st.pyplot(fig)
|
pages/first-law-of-thermodynamics.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
st.title("First Law of Thermodynamics Simulation")
|
| 6 |
+
st.write("""
|
| 7 |
+
The First Law of Thermodynamics is essentially an expression of energy conservation.
|
| 8 |
+
It states that the change in internal energy (ΔU) of a system equals the heat (Q) added
|
| 9 |
+
to the system minus the work (W) done by the system:
|
| 10 |
+
|
| 11 |
+
**ΔU = Q - W**
|
| 12 |
+
|
| 13 |
+
Use the sliders below to adjust the heat added and the work done.
|
| 14 |
+
""")
|
| 15 |
+
|
| 16 |
+
# User input for heat (Q) and work (W)
|
| 17 |
+
Q = st.slider("Heat added (Q) [Joules]", min_value=-200, max_value=200, value=50, step=1)
|
| 18 |
+
W = st.slider("Work done (W) [Joules]", min_value=-200, max_value=200, value=30, step=1)
|
| 19 |
+
|
| 20 |
+
# Calculate the change in internal energy
|
| 21 |
+
delta_U = Q - W
|
| 22 |
+
st.write(f"**Calculated change in internal energy (ΔU):** {delta_U} Joules")
|
| 23 |
+
|
| 24 |
+
# Create a bar chart to visualize the energy contributions
|
| 25 |
+
labels = ['Heat (Q)', 'Work (W)', 'ΔU']
|
| 26 |
+
values = [Q, W, delta_U]
|
| 27 |
+
colors = ['#69b3a2', '#404080', '#e377c2']
|
| 28 |
+
|
| 29 |
+
fig, ax = plt.subplots()
|
| 30 |
+
bars = ax.bar(labels, values, color=colors)
|
| 31 |
+
ax.axhline(0, color='black', linewidth=0.8)
|
| 32 |
+
ax.set_ylabel("Energy (Joules)")
|
| 33 |
+
ax.set_title("Energy Balance: Q, W, and ΔU")
|
| 34 |
+
for bar in bars:
|
| 35 |
+
yval = bar.get_height()
|
| 36 |
+
# Position the text slightly above the bar if positive, or below if negative.
|
| 37 |
+
offset = 3 if yval >= 0 else -15
|
| 38 |
+
ax.text(bar.get_x() + bar.get_width()/2.0, yval + offset, f'{yval}',
|
| 39 |
+
ha='center', color='black', fontsize=10)
|
| 40 |
+
st.pyplot(fig)
|
| 41 |
+
|
| 42 |
+
st.write("""
|
| 43 |
+
**Explanation:**
|
| 44 |
+
- **Q (Heat added):** The energy put into the system via heating.
|
| 45 |
+
- **W (Work done):** The energy used by the system to perform work (like expanding against a piston).
|
| 46 |
+
- **ΔU:** The resulting change in the system's internal energy.
|
| 47 |
+
|
| 48 |
+
Notice that increasing Q or decreasing W (or both) will result in a higher ΔU, showing that more energy remains stored within the system.
|
| 49 |
+
""")
|
| 50 |
+
|
| 51 |
+
if __name__ == '__main__':
|
| 52 |
+
main()
|
pages/fluid-dynamics.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
# Sidebar parameters for simulation control
|
| 7 |
+
num_particles = st.sidebar.number_input("Number of particles", min_value=10, max_value=500, value=100)
|
| 8 |
+
dt = st.sidebar.slider("Time step (dt)", min_value=0.01, max_value=1.0, value=0.1)
|
| 9 |
+
num_steps = st.sidebar.slider("Number of simulation steps", min_value=10, max_value=500, value=200)
|
| 10 |
+
refresh_rate = st.sidebar.slider("Refresh rate (seconds)", min_value=0.01, max_value=0.5, value=0.1)
|
| 11 |
+
|
| 12 |
+
# Initialize particle positions randomly within a region
|
| 13 |
+
positions = np.random.uniform(-5, 5, (num_particles, 2))
|
| 14 |
+
|
| 15 |
+
def velocity_field(x, y):
|
| 16 |
+
"""
|
| 17 |
+
Defines a vortex velocity field centered at the origin.
|
| 18 |
+
u = -y, v = x gives a counter-clockwise rotation.
|
| 19 |
+
"""
|
| 20 |
+
u = -y
|
| 21 |
+
v = x
|
| 22 |
+
return u, v
|
| 23 |
+
|
| 24 |
+
st.title("Fluid Dynamics Simulation: Particle Advection in a Vortex")
|
| 25 |
+
|
| 26 |
+
if st.button("Start Simulation"):
|
| 27 |
+
placeholder = st.empty() # container to update the plot
|
| 28 |
+
for step in range(num_steps):
|
| 29 |
+
# Compute the velocity for all particles at their current positions
|
| 30 |
+
u, v = velocity_field(positions[:, 0], positions[:, 1])
|
| 31 |
+
# Update particle positions using Euler integration
|
| 32 |
+
positions[:, 0] += u * dt
|
| 33 |
+
positions[:, 1] += v * dt
|
| 34 |
+
|
| 35 |
+
# Create a plot of the current particle positions
|
| 36 |
+
fig, ax = plt.subplots(figsize=(6, 6))
|
| 37 |
+
ax.scatter(positions[:, 0], positions[:, 1], color='blue', s=10)
|
| 38 |
+
ax.set_xlim(-10, 10)
|
| 39 |
+
ax.set_ylim(-10, 10)
|
| 40 |
+
ax.set_title(f"Step {step + 1}")
|
| 41 |
+
ax.set_xlabel("X")
|
| 42 |
+
ax.set_ylabel("Y")
|
| 43 |
+
|
| 44 |
+
# Update the plot in the Streamlit app
|
| 45 |
+
placeholder.pyplot(fig)
|
| 46 |
+
time.sleep(refresh_rate)
|
pages/fluid-statics.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
st.title("Fluid Statics Simulation")
|
| 6 |
+
st.markdown("""
|
| 7 |
+
This simulation visualizes how pressure varies in a fluid column at rest.
|
| 8 |
+
According to fluid statics, the pressure increases with depth according to:
|
| 9 |
+
\[ P = P_0 + \rho g h \]
|
| 10 |
+
where:
|
| 11 |
+
- \(P_0\) is the surface pressure,
|
| 12 |
+
- \(\rho\) is the fluid density,
|
| 13 |
+
- \(g\) is the gravitational acceleration,
|
| 14 |
+
- \(h\) is the depth.
|
| 15 |
+
""")
|
| 16 |
+
|
| 17 |
+
# User input for simulation parameters
|
| 18 |
+
density = st.slider("Fluid Density (kg/m³)", min_value=500, max_value=2000, value=1000, step=50)
|
| 19 |
+
gravity = st.slider("Gravitational Acceleration (m/s²)", min_value=1.0, max_value=20.0, value=9.81, step=0.1)
|
| 20 |
+
column_height = st.slider("Fluid Column Height (m)", min_value=1.0, max_value=100.0, value=10.0, step=1.0)
|
| 21 |
+
P0 = st.slider("Surface Pressure (Pa)", min_value=0, max_value=200000, value=101325, step=1000)
|
| 22 |
+
|
| 23 |
+
# Calculate pressures at various depths
|
| 24 |
+
depths = np.linspace(0, column_height, 200)
|
| 25 |
+
pressures = P0 + density * gravity * depths
|
| 26 |
+
|
| 27 |
+
# Plot Pressure vs. Depth
|
| 28 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 29 |
+
ax.plot(pressures, depths, color="blue")
|
| 30 |
+
ax.set_xlabel("Pressure (Pa)")
|
| 31 |
+
ax.set_ylabel("Depth (m)")
|
| 32 |
+
ax.set_title("Pressure Variation with Depth")
|
| 33 |
+
ax.invert_yaxis() # so that the plot displays depth increasing downward
|
| 34 |
+
st.pyplot(fig)
|
| 35 |
+
|
| 36 |
+
# Visualize the fluid column using a color gradient
|
| 37 |
+
fig2, ax2 = plt.subplots(figsize=(2, 6))
|
| 38 |
+
# Create a vertical gradient image
|
| 39 |
+
gradient = np.linspace(0, 1, 256)
|
| 40 |
+
gradient = np.vstack((gradient, gradient))
|
| 41 |
+
ax2.imshow(gradient, extent=[0, 1, 0, column_height], aspect='auto', cmap='Blues')
|
| 42 |
+
ax2.set_title("Fluid Column (Pressure Gradient)")
|
| 43 |
+
ax2.set_ylabel("Depth (m)")
|
| 44 |
+
ax2.set_xticks([])
|
| 45 |
+
st.pyplot(fig2)
|
pages/force.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import matplotlib.patches as patches
|
| 5 |
+
|
| 6 |
+
def calculate_resultant_force(forces):
|
| 7 |
+
"""
|
| 8 |
+
Calculate the resultant force vector from multiple forces.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
forces (list): List of tuples (magnitude, angle in degrees)
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
tuple: Resultant force magnitude and angle
|
| 15 |
+
"""
|
| 16 |
+
# Convert forces to x and y components
|
| 17 |
+
fx = sum(force[0] * np.cos(np.radians(force[1])) for force in forces)
|
| 18 |
+
fy = sum(force[0] * np.sin(np.radians(force[1])) for force in forces)
|
| 19 |
+
|
| 20 |
+
# Calculate resultant magnitude and angle
|
| 21 |
+
resultant_magnitude = np.sqrt(fx**2 + fy**2)
|
| 22 |
+
resultant_angle = np.degrees(np.arctan2(fy, fx))
|
| 23 |
+
|
| 24 |
+
return resultant_magnitude, resultant_angle
|
| 25 |
+
|
| 26 |
+
def draw_force_diagram(forces):
|
| 27 |
+
"""
|
| 28 |
+
Create a force diagram using matplotlib.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
forces (list): List of tuples (magnitude, angle in degrees)
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
matplotlib figure
|
| 35 |
+
"""
|
| 36 |
+
fig, ax = plt.subplots(figsize=(8, 8))
|
| 37 |
+
ax.set_xlim(-10, 10)
|
| 38 |
+
ax.set_ylim(-10, 10)
|
| 39 |
+
ax.grid(True, linestyle='--', alpha=0.7)
|
| 40 |
+
ax.set_title('Force Diagram')
|
| 41 |
+
ax.set_xlabel('X-axis')
|
| 42 |
+
ax.set_ylabel('Y-axis')
|
| 43 |
+
|
| 44 |
+
# Draw origin point
|
| 45 |
+
ax.plot(0, 0, 'ro', markersize=10)
|
| 46 |
+
|
| 47 |
+
# Colors for different forces
|
| 48 |
+
colors = ['blue', 'green', 'red', 'purple', 'orange']
|
| 49 |
+
|
| 50 |
+
# Draw individual forces
|
| 51 |
+
for i, (magnitude, angle) in enumerate(forces):
|
| 52 |
+
color = colors[i % len(colors)]
|
| 53 |
+
# Calculate x and y components
|
| 54 |
+
fx = magnitude * np.cos(np.radians(angle))
|
| 55 |
+
fy = magnitude * np.sin(np.radians(angle))
|
| 56 |
+
|
| 57 |
+
# Draw force vector
|
| 58 |
+
ax.arrow(0, 0, fx, fy, head_width=0.3, head_length=0.5,
|
| 59 |
+
fc=color, ec=color, alpha=0.7,
|
| 60 |
+
label=f'Force {i+1}: {magnitude} N at {angle}°')
|
| 61 |
+
|
| 62 |
+
# Calculate and draw resultant force
|
| 63 |
+
resultant_magnitude, resultant_angle = calculate_resultant_force(forces)
|
| 64 |
+
ax.arrow(0, 0,
|
| 65 |
+
resultant_magnitude * np.cos(np.radians(resultant_angle)),
|
| 66 |
+
resultant_magnitude * np.sin(np.radians(resultant_angle)),
|
| 67 |
+
head_width=0.5, head_length=0.8,
|
| 68 |
+
fc='black', ec='black', alpha=1,
|
| 69 |
+
label=f'Resultant: {resultant_magnitude:.2f} N at {resultant_angle:.2f}°')
|
| 70 |
+
|
| 71 |
+
ax.legend()
|
| 72 |
+
return fig
|
| 73 |
+
|
| 74 |
+
def main():
|
| 75 |
+
st.title('Force Visualization Simulator')
|
| 76 |
+
|
| 77 |
+
st.sidebar.header('Force Input')
|
| 78 |
+
|
| 79 |
+
# Initialize session state for forces
|
| 80 |
+
if 'forces' not in st.session_state:
|
| 81 |
+
st.session_state.forces = []
|
| 82 |
+
|
| 83 |
+
# Force input
|
| 84 |
+
magnitude = st.sidebar.number_input('Force Magnitude (N)', min_value=0.0, step=0.1, value=5.0)
|
| 85 |
+
angle = st.sidebar.number_input('Force Angle (degrees)', min_value=0.0, max_value=360.0, step=1.0, value=0.0)
|
| 86 |
+
|
| 87 |
+
# Add force button
|
| 88 |
+
if st.sidebar.button('Add Force'):
|
| 89 |
+
st.session_state.forces.append((magnitude, angle))
|
| 90 |
+
|
| 91 |
+
# Remove force button
|
| 92 |
+
if st.sidebar.button('Remove Last Force') and st.session_state.forces:
|
| 93 |
+
st.session_state.forces.pop()
|
| 94 |
+
|
| 95 |
+
# Clear all forces
|
| 96 |
+
if st.sidebar.button('Clear All Forces'):
|
| 97 |
+
st.session_state.forces = []
|
| 98 |
+
|
| 99 |
+
# Display current forces
|
| 100 |
+
st.sidebar.subheader('Current Forces:')
|
| 101 |
+
for i, (mag, ang) in enumerate(st.session_state.forces, 1):
|
| 102 |
+
st.sidebar.text(f'Force {i}: {mag} N at {ang}°')
|
| 103 |
+
|
| 104 |
+
# Visualization
|
| 105 |
+
if st.session_state.forces:
|
| 106 |
+
fig = draw_force_diagram(st.session_state.forces)
|
| 107 |
+
st.pyplot(fig)
|
| 108 |
+
|
| 109 |
+
# Resultant force calculation
|
| 110 |
+
resultant_magnitude, resultant_angle = calculate_resultant_force(st.session_state.forces)
|
| 111 |
+
st.subheader('Resultant Force')
|
| 112 |
+
st.write(f'Magnitude: {resultant_magnitude:.2f} N')
|
| 113 |
+
st.write(f'Angle: {resultant_angle:.2f}°')
|
| 114 |
+
else:
|
| 115 |
+
st.info('Add forces to visualize the force diagram')
|
| 116 |
+
|
| 117 |
+
# Physics explanation
|
| 118 |
+
st.sidebar.markdown("""
|
| 119 |
+
### How to Use
|
| 120 |
+
1. Enter force magnitude and angle
|
| 121 |
+
2. Click 'Add Force' to include in diagram
|
| 122 |
+
3. Visualize vector forces and resultant
|
| 123 |
+
|
| 124 |
+
### Force Visualization Concepts
|
| 125 |
+
- **Magnitude**: Strength of the force (Newtons)
|
| 126 |
+
- **Angle**: Direction of the force (0-360 degrees)
|
| 127 |
+
- **Resultant Force**: Combined effect of all forces
|
| 128 |
+
""")
|
| 129 |
+
|
| 130 |
+
if __name__ == '__main__':
|
| 131 |
+
main()
|
pages/free-fall.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
def calculate_free_fall(initial_height, gravity=9.8):
|
| 6 |
+
"""
|
| 7 |
+
Calculate free fall motion parameters
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
initial_height (float): Starting height in meters
|
| 11 |
+
gravity (float): Acceleration due to gravity (default 9.8 m/s²)
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
tuple: Time of fall, position array, velocity array
|
| 15 |
+
"""
|
| 16 |
+
# Calculate time to hit ground
|
| 17 |
+
time_to_ground = np.sqrt(2 * initial_height / gravity)
|
| 18 |
+
|
| 19 |
+
# Create time array
|
| 20 |
+
time = np.linspace(0, time_to_ground, 100)
|
| 21 |
+
|
| 22 |
+
# Calculate position and velocity
|
| 23 |
+
position = initial_height - 0.5 * gravity * time**2
|
| 24 |
+
velocity = -gravity * time
|
| 25 |
+
|
| 26 |
+
return time, position, velocity
|
| 27 |
+
|
| 28 |
+
def plot_free_fall(time, position, velocity):
|
| 29 |
+
"""
|
| 30 |
+
Create plots for free fall motion
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
time (array): Time array
|
| 34 |
+
position (array): Position array
|
| 35 |
+
velocity (array): Velocity array
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
matplotlib figure
|
| 39 |
+
"""
|
| 40 |
+
# Create figure and axis
|
| 41 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 10),
|
| 42 |
+
gridspec_kw={'height_ratios': [2, 1]})
|
| 43 |
+
|
| 44 |
+
# Position plot
|
| 45 |
+
ax1.plot(time, position, 'b-')
|
| 46 |
+
ax1.scatter(time[-1], position[-1], color='red', s=100)
|
| 47 |
+
ax1.set_title('Free Fall - Position vs Time')
|
| 48 |
+
ax1.set_xlabel('Time (s)')
|
| 49 |
+
ax1.set_ylabel('Height (m)')
|
| 50 |
+
ax1.grid(True, linestyle='--', alpha=0.7)
|
| 51 |
+
|
| 52 |
+
# Velocity plot
|
| 53 |
+
ax2.plot(time, velocity, 'g-')
|
| 54 |
+
ax2.scatter(time[-1], velocity[-1], color='red', s=100)
|
| 55 |
+
ax2.set_title('Free Fall - Velocity vs Time')
|
| 56 |
+
ax2.set_xlabel('Time (s)')
|
| 57 |
+
ax2.set_ylabel('Velocity (m/s)')
|
| 58 |
+
ax2.grid(True, linestyle='--', alpha=0.7)
|
| 59 |
+
|
| 60 |
+
plt.tight_layout()
|
| 61 |
+
return fig
|
| 62 |
+
|
| 63 |
+
def main():
|
| 64 |
+
"""
|
| 65 |
+
Streamlit app for Free Fall Simulation
|
| 66 |
+
"""
|
| 67 |
+
st.title('Free Fall Motion Simulator')
|
| 68 |
+
|
| 69 |
+
# Sidebar for inputs
|
| 70 |
+
st.sidebar.header('Simulation Parameters')
|
| 71 |
+
|
| 72 |
+
# Initial height input
|
| 73 |
+
initial_height = st.sidebar.slider(
|
| 74 |
+
'Initial Height (m)',
|
| 75 |
+
min_value=1.0,
|
| 76 |
+
max_value=100.0,
|
| 77 |
+
value=10.0,
|
| 78 |
+
step=0.5
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# Gravity selection (optional)
|
| 82 |
+
gravity_options = [
|
| 83 |
+
('Earth (9.8 m/s²)', 9.8),
|
| 84 |
+
('Moon (1.6 m/s²)', 1.6),
|
| 85 |
+
('Mars (3.7 m/s²)', 3.7),
|
| 86 |
+
]
|
| 87 |
+
gravity = st.sidebar.selectbox(
|
| 88 |
+
'Planet/Location',
|
| 89 |
+
gravity_options,
|
| 90 |
+
format_func=lambda x: x[0]
|
| 91 |
+
)[1]
|
| 92 |
+
|
| 93 |
+
# Calculate free fall motion
|
| 94 |
+
time, position, velocity = calculate_free_fall(initial_height, gravity)
|
| 95 |
+
|
| 96 |
+
# Create columns for information display
|
| 97 |
+
col1, col2 = st.columns(2)
|
| 98 |
+
|
| 99 |
+
with col1:
|
| 100 |
+
st.metric('Initial Height', f'{initial_height} m')
|
| 101 |
+
st.metric('Time to Ground', f'{time[-1]:.2f} s')
|
| 102 |
+
|
| 103 |
+
with col2:
|
| 104 |
+
st.metric('Gravity', f'{gravity} m/s²')
|
| 105 |
+
st.metric('Final Velocity', f'{velocity[-1]:.2f} m/s')
|
| 106 |
+
|
| 107 |
+
# Create and display plots
|
| 108 |
+
st.header('Motion Visualization')
|
| 109 |
+
fig = plot_free_fall(time, position, velocity)
|
| 110 |
+
|
| 111 |
+
# Display matplotlib figure
|
| 112 |
+
st.pyplot(fig)
|
| 113 |
+
|
| 114 |
+
# Detailed explanation
|
| 115 |
+
st.markdown("""
|
| 116 |
+
### Physics Behind Free Fall
|
| 117 |
+
|
| 118 |
+
In free fall motion:
|
| 119 |
+
- The object falls under the influence of gravity
|
| 120 |
+
- Acceleration is constant (9.8 m/s² on Earth)
|
| 121 |
+
- Air resistance is neglected
|
| 122 |
+
- Position follows quadratic equation:
|
| 123 |
+
$h(t) = h_0 - \\frac{1}{2}gt^2$
|
| 124 |
+
- Velocity follows linear equation:
|
| 125 |
+
$v(t) = -gt$
|
| 126 |
+
|
| 127 |
+
### How to Interpret the Graphs
|
| 128 |
+
- Top Graph: Shows object's height decreasing over time
|
| 129 |
+
- Bottom Graph: Shows velocity increasing in magnitude
|
| 130 |
+
(becoming more negative) as the object falls
|
| 131 |
+
- Red dot indicates final position/velocity
|
| 132 |
+
""")
|
| 133 |
+
|
| 134 |
+
if __name__ == '__main__':
|
| 135 |
+
main()
|
| 136 |
+
|
| 137 |
+
# Installation requirements
|
| 138 |
+
# Run in terminal:
|
| 139 |
+
# pip install streamlit numpy matplotlib
|
| 140 |
+
# streamlit run free_fall_simulation.py
|
pages/friction.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import seaborn as sns
|
| 6 |
+
|
| 7 |
+
class FrictionSimulation:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
# Constants for simulation
|
| 10 |
+
self.g = 9.8 # Acceleration due to gravity (m/s^2)
|
| 11 |
+
self.mass = 10.0 # Mass of the object (kg)
|
| 12 |
+
self.angle = 0.0 # Angle of the inclined plane (degrees)
|
| 13 |
+
self.coefficient_static = 0.5 # Static friction coefficient
|
| 14 |
+
self.coefficient_kinetic = 0.3 # Kinetic friction coefficient
|
| 15 |
+
|
| 16 |
+
def calculate_forces(self):
|
| 17 |
+
"""
|
| 18 |
+
Calculate forces acting on an object on an inclined plane
|
| 19 |
+
"""
|
| 20 |
+
# Convert angle to radians
|
| 21 |
+
theta = np.deg2rad(self.angle)
|
| 22 |
+
|
| 23 |
+
# Normal force
|
| 24 |
+
normal_force = self.mass * self.g * np.cos(theta)
|
| 25 |
+
|
| 26 |
+
# Weight components
|
| 27 |
+
weight_parallel = self.mass * self.g * np.sin(theta)
|
| 28 |
+
weight_perpendicular = self.mass * self.g * np.cos(theta)
|
| 29 |
+
|
| 30 |
+
# Maximum static friction
|
| 31 |
+
max_static_friction = self.coefficient_static * normal_force
|
| 32 |
+
|
| 33 |
+
# Kinetic friction
|
| 34 |
+
kinetic_friction = self.coefficient_kinetic * normal_force
|
| 35 |
+
|
| 36 |
+
return {
|
| 37 |
+
'Normal Force': normal_force,
|
| 38 |
+
'Weight Parallel': weight_parallel,
|
| 39 |
+
'Weight Perpendicular': weight_perpendicular,
|
| 40 |
+
'Max Static Friction': max_static_friction,
|
| 41 |
+
'Kinetic Friction': kinetic_friction
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
def plot_force_diagram(self, forces):
|
| 45 |
+
"""
|
| 46 |
+
Create a force diagram visualization
|
| 47 |
+
"""
|
| 48 |
+
plt.figure(figsize=(10, 6))
|
| 49 |
+
|
| 50 |
+
# Create a bar plot of forces
|
| 51 |
+
force_names = list(forces.keys())
|
| 52 |
+
force_values = list(forces.values())
|
| 53 |
+
|
| 54 |
+
plt.bar(force_names, force_values)
|
| 55 |
+
plt.title('Forces Acting on Object')
|
| 56 |
+
plt.xlabel('Force Types')
|
| 57 |
+
plt.ylabel('Force Magnitude (N)')
|
| 58 |
+
plt.xticks(rotation=45, ha='right')
|
| 59 |
+
plt.tight_layout()
|
| 60 |
+
|
| 61 |
+
return plt
|
| 62 |
+
|
| 63 |
+
def main():
|
| 64 |
+
st.title('Interactive Friction Simulation')
|
| 65 |
+
|
| 66 |
+
# Sidebar for inputs
|
| 67 |
+
st.sidebar.header('Simulation Parameters')
|
| 68 |
+
|
| 69 |
+
# Create simulation instance
|
| 70 |
+
sim = FrictionSimulation()
|
| 71 |
+
|
| 72 |
+
# Mass input
|
| 73 |
+
sim.mass = st.sidebar.slider('Object Mass (kg)', 1.0, 50.0, 10.0, 0.5)
|
| 74 |
+
|
| 75 |
+
# Angle input
|
| 76 |
+
sim.angle = st.sidebar.slider('Incline Angle (degrees)', 0, 90, 30, 1)
|
| 77 |
+
|
| 78 |
+
# Friction coefficients
|
| 79 |
+
sim.coefficient_static = st.sidebar.slider('Static Friction Coefficient', 0.0, 1.0, 0.5, 0.01)
|
| 80 |
+
sim.coefficient_kinetic = st.sidebar.slider('Kinetic Friction Coefficient', 0.0, 1.0, 0.3, 0.01)
|
| 81 |
+
|
| 82 |
+
# Calculate forces
|
| 83 |
+
forces = sim.calculate_forces()
|
| 84 |
+
|
| 85 |
+
# Display force calculations
|
| 86 |
+
st.header('Force Calculations')
|
| 87 |
+
force_df = pd.DataFrame.from_dict(forces, orient='index', columns=['Force (N)'])
|
| 88 |
+
st.dataframe(force_df)
|
| 89 |
+
|
| 90 |
+
# Visualization of forces
|
| 91 |
+
st.header('Force Diagram')
|
| 92 |
+
fig = sim.plot_force_diagram(forces)
|
| 93 |
+
st.pyplot(fig)
|
| 94 |
+
|
| 95 |
+
# Explanation section
|
| 96 |
+
st.header('Friction Explanation')
|
| 97 |
+
st.markdown("""
|
| 98 |
+
### Understanding Friction on an Inclined Plane
|
| 99 |
+
|
| 100 |
+
- **Normal Force**: The force perpendicular to the surface, supporting the object's weight
|
| 101 |
+
- **Weight Parallel**: Component of weight pushing the object down the slope
|
| 102 |
+
- **Max Static Friction**: Maximum friction force that prevents object from sliding
|
| 103 |
+
- **Kinetic Friction**: Friction force when the object is moving
|
| 104 |
+
|
| 105 |
+
#### When will the object start sliding?
|
| 106 |
+
- If the parallel component of weight exceeds maximum static friction
|
| 107 |
+
- The critical angle can be calculated using the static friction coefficient
|
| 108 |
+
""")
|
| 109 |
+
|
| 110 |
+
# Critical angle calculation
|
| 111 |
+
critical_angle = np.arctan(sim.coefficient_static) * 180 / np.pi
|
| 112 |
+
st.info(f"Critical Angle: {critical_angle:.2f} degrees")
|
| 113 |
+
|
| 114 |
+
if __name__ == '__main__':
|
| 115 |
+
main()
|
pages/general-relativity.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import plotly.graph_objects as go
|
| 4 |
+
|
| 5 |
+
def generate_surface(mass, grid_size=100, range_val=10):
|
| 6 |
+
"""
|
| 7 |
+
Generates a grid and calculates a "curvature" value for each point,
|
| 8 |
+
using a simplified gravitational potential analogy:
|
| 9 |
+
|
| 10 |
+
Z = - mass / sqrt(x^2 + y^2 + epsilon)
|
| 11 |
+
|
| 12 |
+
The epsilon is added to avoid singularity at (0,0).
|
| 13 |
+
"""
|
| 14 |
+
x = np.linspace(-range_val, range_val, grid_size)
|
| 15 |
+
y = np.linspace(-range_val, range_val, grid_size)
|
| 16 |
+
X, Y = np.meshgrid(x, y)
|
| 17 |
+
epsilon = 0.1 # small constant to prevent division by zero
|
| 18 |
+
Z = -mass / np.sqrt(X**2 + Y**2 + epsilon)
|
| 19 |
+
return X, Y, Z
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
st.title("General Relativity Visualization")
|
| 23 |
+
st.write("""
|
| 24 |
+
This simulation visualizes a simplified model of space-time curvature due to a massive object.
|
| 25 |
+
Although general relativity is much more complex, the following "rubber-sheet" analogy
|
| 26 |
+
helps illustrate how mass can curve space.
|
| 27 |
+
""")
|
| 28 |
+
|
| 29 |
+
# Sidebar controls for interactivity
|
| 30 |
+
mass = st.sidebar.slider("Mass", min_value=1.0, max_value=10.0, value=5.0, step=0.5,
|
| 31 |
+
help="Increase mass to deepen the gravitational well.")
|
| 32 |
+
grid_size = st.sidebar.slider("Grid Resolution", min_value=50, max_value=200, value=100, step=10,
|
| 33 |
+
help="Adjust the grid resolution of the visualization.")
|
| 34 |
+
range_val = st.sidebar.slider("Range", min_value=5, max_value=20, value=10, step=1,
|
| 35 |
+
help="Set the spatial range for the grid (in arbitrary units).")
|
| 36 |
+
|
| 37 |
+
# Generate the grid and curvature data
|
| 38 |
+
X, Y, Z = generate_surface(mass, grid_size, range_val)
|
| 39 |
+
|
| 40 |
+
# Create a 3D surface plot using Plotly
|
| 41 |
+
fig = go.Figure(data=[go.Surface(x=X, y=Y, z=Z, colorscale="Viridis")])
|
| 42 |
+
fig.update_layout(
|
| 43 |
+
title="Space-time Curvature",
|
| 44 |
+
scene=dict(
|
| 45 |
+
xaxis_title="X",
|
| 46 |
+
yaxis_title="Y",
|
| 47 |
+
zaxis_title="Curvature (analogy)",
|
| 48 |
+
aspectratio=dict(x=1, y=1, z=0.5)
|
| 49 |
+
),
|
| 50 |
+
autosize=True
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
main()
|
pages/gravity.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from matplotlib.animation import FuncAnimation
|
| 5 |
+
import matplotlib.animation as animation
|
| 6 |
+
|
| 7 |
+
class GravitySimulation:
|
| 8 |
+
def __init__(self, num_bodies=3, width=10, height=10):
|
| 9 |
+
"""
|
| 10 |
+
Initialize the gravity simulation
|
| 11 |
+
|
| 12 |
+
Parameters:
|
| 13 |
+
-----------
|
| 14 |
+
num_bodies : int, optional
|
| 15 |
+
Number of bodies in the simulation (default is 3)
|
| 16 |
+
width : float, optional
|
| 17 |
+
Width of the simulation space (default is 10)
|
| 18 |
+
height : float, optional
|
| 19 |
+
Height of the simulation space (default is 10)
|
| 20 |
+
"""
|
| 21 |
+
self.num_bodies = num_bodies
|
| 22 |
+
self.width = width
|
| 23 |
+
self.height = height
|
| 24 |
+
|
| 25 |
+
# Gravitational constant (scaled for visualization)
|
| 26 |
+
self.G = 1.0
|
| 27 |
+
|
| 28 |
+
# Initialize positions, velocities, and masses
|
| 29 |
+
self.positions = np.random.rand(num_bodies, 2) * np.array([width, height])
|
| 30 |
+
self.velocities = np.random.randn(num_bodies, 2) * 0.1
|
| 31 |
+
self.masses = np.random.uniform(0.5, 2, num_bodies)
|
| 32 |
+
|
| 33 |
+
def compute_accelerations(self):
|
| 34 |
+
"""
|
| 35 |
+
Compute gravitational accelerations between bodies
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
--------
|
| 39 |
+
numpy.ndarray
|
| 40 |
+
Accelerations for each body
|
| 41 |
+
"""
|
| 42 |
+
accelerations = np.zeros_like(self.positions)
|
| 43 |
+
|
| 44 |
+
for i in range(self.num_bodies):
|
| 45 |
+
for j in range(self.num_bodies):
|
| 46 |
+
if i != j:
|
| 47 |
+
# Vector from body i to body j
|
| 48 |
+
r_vector = self.positions[j] - self.positions[i]
|
| 49 |
+
|
| 50 |
+
# Distance between bodies
|
| 51 |
+
r_magnitude = np.linalg.norm(r_vector)
|
| 52 |
+
|
| 53 |
+
# Avoid division by zero
|
| 54 |
+
if r_magnitude > 0:
|
| 55 |
+
# Gravitational acceleration
|
| 56 |
+
acceleration_magnitude = self.G * self.masses[j] / (r_magnitude ** 2)
|
| 57 |
+
|
| 58 |
+
# Direction of acceleration
|
| 59 |
+
acceleration = acceleration_magnitude * (r_vector / r_magnitude)
|
| 60 |
+
|
| 61 |
+
accelerations[i] += acceleration
|
| 62 |
+
|
| 63 |
+
return accelerations
|
| 64 |
+
|
| 65 |
+
def update(self, dt=0.01):
|
| 66 |
+
"""
|
| 67 |
+
Update positions and velocities using Euler integration
|
| 68 |
+
|
| 69 |
+
Parameters:
|
| 70 |
+
-----------
|
| 71 |
+
dt : float, optional
|
| 72 |
+
Time step for simulation (default is 0.01)
|
| 73 |
+
"""
|
| 74 |
+
# Compute accelerations
|
| 75 |
+
accelerations = self.compute_accelerations()
|
| 76 |
+
|
| 77 |
+
# Update velocities
|
| 78 |
+
self.velocities += accelerations * dt
|
| 79 |
+
|
| 80 |
+
# Update positions
|
| 81 |
+
self.positions += self.velocities * dt
|
| 82 |
+
|
| 83 |
+
# Simple boundary conditions (bouncing off walls)
|
| 84 |
+
for i in range(self.num_bodies):
|
| 85 |
+
for dim in range(2):
|
| 86 |
+
if (self.positions[i, dim] < 0) or (self.positions[i, dim] > [self.width, self.height][dim]):
|
| 87 |
+
self.velocities[i, dim] *= -0.9 # Damped bounce
|
| 88 |
+
self.positions[i, dim] = np.clip(self.positions[i, dim], 0, [self.width, self.height][dim])
|
| 89 |
+
|
| 90 |
+
def main():
|
| 91 |
+
st.title("Gravitational N-Body Simulation")
|
| 92 |
+
|
| 93 |
+
# Sidebar for simulation parameters
|
| 94 |
+
st.sidebar.header("Simulation Parameters")
|
| 95 |
+
|
| 96 |
+
# Number of bodies slider
|
| 97 |
+
num_bodies = st.sidebar.slider("Number of Bodies", min_value=2, max_value=10, value=3)
|
| 98 |
+
|
| 99 |
+
# Simulation space dimensions
|
| 100 |
+
width = st.sidebar.number_input("Simulation Width", min_value=5.0, max_value=20.0, value=10.0)
|
| 101 |
+
height = st.sidebar.number_input("Simulation Height", min_value=5.0, max_value=20.0, value=10.0)
|
| 102 |
+
|
| 103 |
+
# Time step slider
|
| 104 |
+
dt = st.sidebar.slider("Time Step", min_value=0.001, max_value=0.1, value=0.01, step=0.001)
|
| 105 |
+
|
| 106 |
+
# Create simulation
|
| 107 |
+
sim = GravitySimulation(num_bodies=num_bodies, width=width, height=height)
|
| 108 |
+
|
| 109 |
+
# Matplotlib figure for animation
|
| 110 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 111 |
+
ax.set_xlim(0, width)
|
| 112 |
+
ax.set_ylim(0, height)
|
| 113 |
+
ax.set_title("Gravitational Interaction")
|
| 114 |
+
ax.set_xlabel("X Position")
|
| 115 |
+
ax.set_ylabel("Y Position")
|
| 116 |
+
|
| 117 |
+
# Scatter plot for bodies
|
| 118 |
+
scatter = ax.scatter(sim.positions[:, 0], sim.positions[:, 1],
|
| 119 |
+
c=sim.masses, cmap='viridis',
|
| 120 |
+
s=sim.masses*100, alpha=0.7)
|
| 121 |
+
|
| 122 |
+
# Animation update function
|
| 123 |
+
def update_plot(frame):
|
| 124 |
+
sim.update(dt)
|
| 125 |
+
scatter.set_offsets(sim.positions)
|
| 126 |
+
return scatter,
|
| 127 |
+
|
| 128 |
+
# Create animation
|
| 129 |
+
anim = FuncAnimation(fig, update_plot, frames=200, interval=50, blit=True)
|
| 130 |
+
|
| 131 |
+
# Convert animation to HTML for Streamlit
|
| 132 |
+
plt.close(fig)
|
| 133 |
+
|
| 134 |
+
# Display animation
|
| 135 |
+
st.pyplot(fig)
|
| 136 |
+
|
| 137 |
+
# Optional: Provide download of animation
|
| 138 |
+
st.sidebar.header("Animation Options")
|
| 139 |
+
if st.sidebar.button("Save Animation"):
|
| 140 |
+
# Save animation as gif
|
| 141 |
+
anim.save('gravity_simulation.gif', writer='pillow')
|
| 142 |
+
with open('gravity_simulation.gif', 'rb') as f:
|
| 143 |
+
st.sidebar.download_button(
|
| 144 |
+
label="Download Animation",
|
| 145 |
+
data=f.read(),
|
| 146 |
+
file_name="gravity_simulation.gif",
|
| 147 |
+
mime="image/gif"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
# Information section
|
| 151 |
+
st.markdown("## About the Simulation")
|
| 152 |
+
st.markdown("""
|
| 153 |
+
This simulation demonstrates gravitational interactions between multiple bodies:
|
| 154 |
+
- Bodies attract each other based on their masses
|
| 155 |
+
- Gravitational force is inversely proportional to the square of the distance
|
| 156 |
+
- Bodies bounce off the simulation boundaries
|
| 157 |
+
- Larger/darker points represent bodies with more mass
|
| 158 |
+
""")
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
main()
|
pages/heat.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
st.title("Heat Diffusion Simulation")
|
| 7 |
+
|
| 8 |
+
# Simulation parameters
|
| 9 |
+
nx, ny = 50, 50 # Grid size
|
| 10 |
+
alpha = st.slider("Thermal Diffusivity (α)", min_value=0.1, max_value=1.0, value=0.2, step=0.1)
|
| 11 |
+
dt = st.slider("Time Step (dt)", min_value=0.001, max_value=0.1, value=0.01, step=0.005)
|
| 12 |
+
steps = st.number_input("Number of Simulation Steps", min_value=10, max_value=1000, value=100, step=10)
|
| 13 |
+
|
| 14 |
+
# Initialize temperature field: all zeros with a hot spot in the center
|
| 15 |
+
T = np.zeros((nx, ny))
|
| 16 |
+
T[nx//2, ny//2] = 100.0
|
| 17 |
+
|
| 18 |
+
# Placeholder for plotting
|
| 19 |
+
plot_placeholder = st.empty()
|
| 20 |
+
|
| 21 |
+
def update_temperature(T, alpha, dt):
|
| 22 |
+
"""
|
| 23 |
+
Update temperature using a simple finite difference scheme for the 2D heat equation.
|
| 24 |
+
The update formula for interior grid points is:
|
| 25 |
+
T_new[i, j] = T[i, j] + α * dt * (T[i+1, j] + T[i-1, j] + T[i, j+1] + T[i, j-1] - 4*T[i, j])
|
| 26 |
+
"""
|
| 27 |
+
T_new = T.copy()
|
| 28 |
+
T_new[1:-1, 1:-1] = T[1:-1, 1:-1] + alpha * dt * (
|
| 29 |
+
T[2:, 1:-1] + T[:-2, 1:-1] + T[1:-1, 2:] + T[1:-1, :-2] - 4 * T[1:-1, 1:-1]
|
| 30 |
+
)
|
| 31 |
+
return T_new
|
| 32 |
+
|
| 33 |
+
# Run the simulation loop
|
| 34 |
+
for i in range(int(steps)):
|
| 35 |
+
T = update_temperature(T, alpha, dt)
|
| 36 |
+
|
| 37 |
+
# Create the plot
|
| 38 |
+
fig, ax = plt.subplots()
|
| 39 |
+
heatmap = ax.imshow(T, cmap='hot', interpolation='nearest')
|
| 40 |
+
ax.set_title(f"Step {i+1}")
|
| 41 |
+
ax.axis('off')
|
| 42 |
+
|
| 43 |
+
# Display the plot in the Streamlit placeholder
|
| 44 |
+
plot_placeholder.pyplot(fig)
|
| 45 |
+
|
| 46 |
+
# Pause to simulate time evolution (adjust the sleep time as needed)
|
| 47 |
+
time.sleep(0.1)
|
pages/ideal-gas-law.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
# Set the title and description for the simulation
|
| 6 |
+
st.title("Ideal Gas Law Visualization")
|
| 7 |
+
st.write("""
|
| 8 |
+
This simulation visualizes the relationship between pressure, volume, and temperature of an ideal gas using the equation:
|
| 9 |
+
\[ P = \frac{nRT}{V} \]
|
| 10 |
+
Adjust the parameters in the sidebar to see how the pressure changes.
|
| 11 |
+
""")
|
| 12 |
+
|
| 13 |
+
# Constants
|
| 14 |
+
R = 8.314 # Ideal gas constant in J/(mol*K)
|
| 15 |
+
|
| 16 |
+
# Sidebar for user inputs
|
| 17 |
+
st.sidebar.header("Gas Parameters")
|
| 18 |
+
n = st.sidebar.slider("Number of moles (n)", min_value=0.1, max_value=5.0, value=1.0, step=0.1)
|
| 19 |
+
T = st.sidebar.slider("Temperature (T in Kelvin)", min_value=100, max_value=1000, value=300, step=50)
|
| 20 |
+
V = st.sidebar.slider("Volume (V in cubic meters)", min_value=0.1, max_value=10.0, value=1.0, step=0.1)
|
| 21 |
+
|
| 22 |
+
# Calculate pressure using the ideal gas law
|
| 23 |
+
P = n * R * T / V
|
| 24 |
+
st.write(f"### Computed Pressure:")
|
| 25 |
+
st.write(f"The pressure of the gas is **{P:.2f} Pascals**.")
|
| 26 |
+
|
| 27 |
+
# Create a range of volumes to plot the Pressure vs Volume curve
|
| 28 |
+
volumes = np.linspace(0.1, 10, 200)
|
| 29 |
+
pressures = n * R * T / volumes
|
| 30 |
+
|
| 31 |
+
# Plotting the curve
|
| 32 |
+
fig, ax = plt.subplots()
|
| 33 |
+
ax.plot(volumes, pressures, color='b', label=f"T = {T} K, n = {n} mol")
|
| 34 |
+
ax.set_xlabel("Volume (m³)")
|
| 35 |
+
ax.set_ylabel("Pressure (Pascals)")
|
| 36 |
+
ax.set_title("Pressure vs Volume")
|
| 37 |
+
ax.legend()
|
| 38 |
+
ax.grid(True)
|
| 39 |
+
|
| 40 |
+
st.pyplot(fig)
|
pages/impulse.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
def calculate_impulse(force, time):
|
| 6 |
+
"""Calculate impulse given force and time."""
|
| 7 |
+
return force * time
|
| 8 |
+
|
| 9 |
+
def calculate_velocity_change(mass, impulse):
|
| 10 |
+
"""Calculate change in velocity using impulse-momentum theorem."""
|
| 11 |
+
return impulse / mass
|
| 12 |
+
|
| 13 |
+
def plot_force_time_graph(force, time):
|
| 14 |
+
"""Create a force-time graph."""
|
| 15 |
+
plt.figure(figsize=(10, 6))
|
| 16 |
+
plt.plot([0, time, time], [force, force, 0], 'b-')
|
| 17 |
+
plt.fill_between([0, time], [force, force], alpha=0.3)
|
| 18 |
+
plt.title('Force-Time Graph')
|
| 19 |
+
plt.xlabel('Time (s)')
|
| 20 |
+
plt.ylabel('Force (N)')
|
| 21 |
+
plt.grid(True)
|
| 22 |
+
return plt
|
| 23 |
+
|
| 24 |
+
def main():
|
| 25 |
+
st.title('Impulse Visualization')
|
| 26 |
+
|
| 27 |
+
# Sidebar for input parameters
|
| 28 |
+
st.sidebar.header('Simulation Parameters')
|
| 29 |
+
|
| 30 |
+
# Mass input
|
| 31 |
+
mass = st.sidebar.number_input(
|
| 32 |
+
'Mass of Object (kg)',
|
| 33 |
+
min_value=0.1,
|
| 34 |
+
max_value=100.0,
|
| 35 |
+
value=1.0,
|
| 36 |
+
step=0.1
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Force input with different input methods
|
| 40 |
+
input_type = st.sidebar.radio(
|
| 41 |
+
'Force Input Method',
|
| 42 |
+
['Constant Force', 'Peak Force and Duration']
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
if input_type == 'Constant Force':
|
| 46 |
+
# Constant force input
|
| 47 |
+
force = st.sidebar.number_input(
|
| 48 |
+
'Constant Force (N)',
|
| 49 |
+
min_value=-1000.0,
|
| 50 |
+
max_value=1000.0,
|
| 51 |
+
value=10.0,
|
| 52 |
+
step=1.0
|
| 53 |
+
)
|
| 54 |
+
time = st.sidebar.number_input(
|
| 55 |
+
'Duration (s)',
|
| 56 |
+
min_value=0.01,
|
| 57 |
+
max_value=10.0,
|
| 58 |
+
value=0.5,
|
| 59 |
+
step=0.1
|
| 60 |
+
)
|
| 61 |
+
else:
|
| 62 |
+
# Peak force and duration input
|
| 63 |
+
force = st.sidebar.number_input(
|
| 64 |
+
'Peak Force (N)',
|
| 65 |
+
min_value=-1000.0,
|
| 66 |
+
max_value=1000.0,
|
| 67 |
+
value=50.0,
|
| 68 |
+
step=1.0
|
| 69 |
+
)
|
| 70 |
+
time = st.sidebar.number_input(
|
| 71 |
+
'Force Duration (s)',
|
| 72 |
+
min_value=0.01,
|
| 73 |
+
max_value=10.0,
|
| 74 |
+
value=0.5,
|
| 75 |
+
step=0.1
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# Calculate impulse and velocity change
|
| 79 |
+
impulse = calculate_impulse(force, time)
|
| 80 |
+
velocity_change = calculate_velocity_change(mass, impulse)
|
| 81 |
+
|
| 82 |
+
# Display results
|
| 83 |
+
st.header('Impulse Calculation Results')
|
| 84 |
+
col1, col2, col3 = st.columns(3)
|
| 85 |
+
|
| 86 |
+
with col1:
|
| 87 |
+
st.metric('Force', f'{force} N')
|
| 88 |
+
with col2:
|
| 89 |
+
st.metric('Duration', f'{time} s')
|
| 90 |
+
with col3:
|
| 91 |
+
st.metric('Mass', f'{mass} kg')
|
| 92 |
+
|
| 93 |
+
st.markdown('---')
|
| 94 |
+
|
| 95 |
+
col1, col2 = st.columns(2)
|
| 96 |
+
|
| 97 |
+
with col1:
|
| 98 |
+
st.metric('Impulse', f'{impulse:.2f} N·s')
|
| 99 |
+
with col2:
|
| 100 |
+
st.metric('Velocity Change', f'{velocity_change:.2f} m/s')
|
| 101 |
+
|
| 102 |
+
# Visualize Force-Time Graph
|
| 103 |
+
st.header('Force-Time Graph')
|
| 104 |
+
fig = plot_force_time_graph(force, time)
|
| 105 |
+
st.pyplot(fig)
|
| 106 |
+
|
| 107 |
+
# Explanation section
|
| 108 |
+
st.header('Understanding Impulse')
|
| 109 |
+
st.markdown("""
|
| 110 |
+
### What is Impulse?
|
| 111 |
+
- **Impulse** is defined as the product of force and time: Impulse = Force × Time
|
| 112 |
+
- It represents the total effect of a force acting over a period of time
|
| 113 |
+
- Impulse is equal to the change in momentum of an object
|
| 114 |
+
|
| 115 |
+
### Key Concepts
|
| 116 |
+
- The area under a Force-Time graph represents the impulse
|
| 117 |
+
- Larger impulse means a greater change in velocity
|
| 118 |
+
- Impulse can be increased by either increasing force or increasing time of application
|
| 119 |
+
""")
|
| 120 |
+
|
| 121 |
+
if __name__ == '__main__':
|
| 122 |
+
main()
|
pages/inertia.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from matplotlib.animation import FuncAnimation
|
| 5 |
+
|
| 6 |
+
def inertia_simulation():
|
| 7 |
+
st.title('Inertia Visualization Simulation')
|
| 8 |
+
|
| 9 |
+
# Sidebar controls
|
| 10 |
+
st.sidebar.header('Simulation Parameters')
|
| 11 |
+
|
| 12 |
+
# Object mass
|
| 13 |
+
mass = st.sidebar.slider('Object Mass (kg)', min_value=1, max_value=25, value=10)
|
| 14 |
+
|
| 15 |
+
# Initial velocity
|
| 16 |
+
initial_velocity = st.sidebar.slider('Initial Velocity (m/s)', min_value=0, max_value=20, value=10)
|
| 17 |
+
|
| 18 |
+
# Friction coefficient
|
| 19 |
+
friction = st.sidebar.slider('Friction Coefficient', min_value=0.0, max_value=1.0, value=0.2, step=0.1)
|
| 20 |
+
|
| 21 |
+
# Explanation
|
| 22 |
+
st.markdown("""
|
| 23 |
+
## Understanding Inertia
|
| 24 |
+
|
| 25 |
+
Inertia is an object's resistance to change in its state of motion.
|
| 26 |
+
This simulation shows how:
|
| 27 |
+
- Heavier objects maintain their motion longer
|
| 28 |
+
- Friction gradually reduces an object's speed
|
| 29 |
+
- Objects tend to continue moving unless stopped
|
| 30 |
+
""")
|
| 31 |
+
|
| 32 |
+
# Simulation calculations
|
| 33 |
+
def calculate_motion(mass, initial_velocity, friction, time):
|
| 34 |
+
# Gravitational acceleration
|
| 35 |
+
g = 9.8
|
| 36 |
+
|
| 37 |
+
# Deceleration due to friction
|
| 38 |
+
deceleration = friction * g *mass
|
| 39 |
+
|
| 40 |
+
# Calculate position and velocity
|
| 41 |
+
if time <= initial_velocity / deceleration:
|
| 42 |
+
# Object is still moving forward
|
| 43 |
+
position = initial_velocity * time - 0.5 * deceleration * time**2
|
| 44 |
+
velocity = initial_velocity - deceleration * time
|
| 45 |
+
else:
|
| 46 |
+
# Object has come to a stop
|
| 47 |
+
# Find the time when object stops
|
| 48 |
+
stop_time = initial_velocity / deceleration
|
| 49 |
+
|
| 50 |
+
# Calculate final position
|
| 51 |
+
final_position = initial_velocity * stop_time - 0.5 * deceleration * stop_time**2
|
| 52 |
+
|
| 53 |
+
# Position after stopping point remains constant
|
| 54 |
+
position = final_position
|
| 55 |
+
velocity = 0
|
| 56 |
+
|
| 57 |
+
return position, velocity
|
| 58 |
+
|
| 59 |
+
# Prepare plot
|
| 60 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
|
| 61 |
+
|
| 62 |
+
# Time array
|
| 63 |
+
time = np.linspace(0, 5, 100)
|
| 64 |
+
|
| 65 |
+
# Calculate motion
|
| 66 |
+
positions = []
|
| 67 |
+
velocities = []
|
| 68 |
+
for t in time:
|
| 69 |
+
pos, vel = calculate_motion(mass, initial_velocity, friction, t)
|
| 70 |
+
positions.append(pos)
|
| 71 |
+
velocities.append(vel)
|
| 72 |
+
|
| 73 |
+
# Position plot
|
| 74 |
+
ax1.plot(time, positions, label='Position')
|
| 75 |
+
ax1.set_title(f'Position over Time (Mass: {mass} kg)')
|
| 76 |
+
ax1.set_xlabel('Time (s)')
|
| 77 |
+
ax1.set_ylabel('Distance (m)')
|
| 78 |
+
ax1.legend()
|
| 79 |
+
ax1.grid(True)
|
| 80 |
+
|
| 81 |
+
# Velocity plot
|
| 82 |
+
ax2.plot(time, velocities, label='Velocity', color='red')
|
| 83 |
+
ax2.set_title(f'Velocity over Time (Initial Velocity: {initial_velocity} m/s)')
|
| 84 |
+
ax2.set_xlabel('Time (s)')
|
| 85 |
+
ax2.set_ylabel('Velocity (m/s)')
|
| 86 |
+
ax2.legend()
|
| 87 |
+
ax2.grid(True)
|
| 88 |
+
|
| 89 |
+
# Adjust layout and display
|
| 90 |
+
plt.tight_layout()
|
| 91 |
+
st.pyplot(fig)
|
| 92 |
+
|
| 93 |
+
# Detailed explanation
|
| 94 |
+
st.markdown(f"""
|
| 95 |
+
### Simulation Details
|
| 96 |
+
- **Mass of Object:** {mass} kg
|
| 97 |
+
- **Initial Velocity:** {initial_velocity} m/s
|
| 98 |
+
- **Friction Coefficient:** {friction}
|
| 99 |
+
|
| 100 |
+
#### Observation
|
| 101 |
+
The graphs show how:
|
| 102 |
+
1. Position changes over time (distance traveled)
|
| 103 |
+
2. Velocity decreases due to friction
|
| 104 |
+
""")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# Streamlit page configuration
|
| 109 |
+
if __name__ == '__main__':
|
| 110 |
+
# Run the simulation
|
| 111 |
+
inertia_simulation()
|
pages/interference.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
|
| 5 |
+
def interference_pattern(lambda_, d, grid_size=200, x_range=5):
|
| 6 |
+
"""
|
| 7 |
+
Compute the interference pattern for two point sources.
|
| 8 |
+
|
| 9 |
+
Parameters:
|
| 10 |
+
- lambda_: Wavelength of the waves (float).
|
| 11 |
+
- d: Separation distance between the two sources (float).
|
| 12 |
+
- grid_size: Number of points in the grid (int, default=200).
|
| 13 |
+
- x_range: Range of x and y coordinates (float, default=5).
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
- I: 2D array of interference intensity.
|
| 17 |
+
- x: 1D array of x coordinates.
|
| 18 |
+
- y: 1D array of y coordinates.
|
| 19 |
+
- S1: Position of the first source (list).
|
| 20 |
+
- S2: Position of the second source (list).
|
| 21 |
+
"""
|
| 22 |
+
# Create a 2D grid
|
| 23 |
+
x = np.linspace(-x_range, x_range, grid_size)
|
| 24 |
+
y = np.linspace(-x_range, x_range, grid_size)
|
| 25 |
+
X, Y = np.meshgrid(x, y)
|
| 26 |
+
|
| 27 |
+
# Define source positions
|
| 28 |
+
S1 = [-d/2, 0] # First source at (-d/2, 0)
|
| 29 |
+
S2 = [d/2, 0] # Second source at (d/2, 0)
|
| 30 |
+
|
| 31 |
+
# Calculate distances from each source to every point on the grid
|
| 32 |
+
r1 = np.sqrt((X - S1[0])**2 + (Y - S1[1])**2)
|
| 33 |
+
r2 = np.sqrt((X - S2[0])**2 + (Y - S2[1])**2)
|
| 34 |
+
|
| 35 |
+
# Compute path difference
|
| 36 |
+
delta = r2 - r1
|
| 37 |
+
|
| 38 |
+
# Calculate intensity (proportional to cos^2(π * Δ / λ))
|
| 39 |
+
I = np.cos(np.pi * delta / lambda_)**2
|
| 40 |
+
|
| 41 |
+
return I, x, y, S1, S2
|
| 42 |
+
|
| 43 |
+
# Streamlit app setup
|
| 44 |
+
st.title("Interference Pattern Simulation")
|
| 45 |
+
|
| 46 |
+
# Explanatory text
|
| 47 |
+
st.write("""
|
| 48 |
+
This simulation visualizes the interference pattern created by two point sources emitting waves.
|
| 49 |
+
- **Bright regions**: Constructive interference, where waves reinforce each other.
|
| 50 |
+
- **Dark regions**: Destructive interference, where waves cancel out.
|
| 51 |
+
Use the sliders below to adjust the **wavelength (λ)** and **source separation (d)** to see how the pattern changes.
|
| 52 |
+
""")
|
| 53 |
+
|
| 54 |
+
# Interactive sliders
|
| 55 |
+
lambda_ = st.slider("Wavelength λ", min_value=0.1, max_value=2.0, value=1.0, step=0.1)
|
| 56 |
+
d = st.slider("Source Separation d", min_value=0.5, max_value=5.0, value=2.0, step=0.1)
|
| 57 |
+
|
| 58 |
+
# Compute the interference pattern
|
| 59 |
+
I, x, y, S1, S2 = interference_pattern(lambda_, d)
|
| 60 |
+
|
| 61 |
+
# Create and display the plot
|
| 62 |
+
fig, ax = plt.subplots()
|
| 63 |
+
ax.imshow(I, cmap='hot', extent=[x.min(), x.max(), y.min(), y.max()], origin='lower')
|
| 64 |
+
ax.plot([S1[0], S2[0]], [S1[1], S2[1]], 'wo', markersize=5) # Mark sources with white dots
|
| 65 |
+
ax.set_xlabel("x")
|
| 66 |
+
ax.set_ylabel("y")
|
| 67 |
+
ax.set_title("Interference Pattern")
|
| 68 |
+
st.pyplot(fig)
|
pages/kinetic-theory-of-gas.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import plotly.graph_objects as go
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
# Streamlit app title and description
|
| 7 |
+
st.title("Kinetic Theory of Gases Simulation")
|
| 8 |
+
st.write("This simulation visualizes gas molecules as particles moving in a 2D container. Watch how their motion relates to temperature and pressure!")
|
| 9 |
+
|
| 10 |
+
# Simulation parameters
|
| 11 |
+
L = 1.0 # Container side length
|
| 12 |
+
N = 50 # Number of particles
|
| 13 |
+
sigma = 0.05 # Standard deviation of velocities (related to temperature)
|
| 14 |
+
dt = 0.01 # Time step for simulation
|
| 15 |
+
|
| 16 |
+
# Initialize particle positions and velocities
|
| 17 |
+
x = np.random.uniform(0, L, N)
|
| 18 |
+
y = np.random.uniform(0, L, N)
|
| 19 |
+
vx = np.random.normal(0, sigma, N)
|
| 20 |
+
vy = np.random.normal(0, sigma, N)
|
| 21 |
+
|
| 22 |
+
# Initialize momentum transfer for pressure calculation
|
| 23 |
+
total_momentum_transfer = 0
|
| 24 |
+
|
| 25 |
+
# Create placeholders for plot and metrics
|
| 26 |
+
plot_placeholder = st.empty()
|
| 27 |
+
metrics_placeholder = st.empty()
|
| 28 |
+
|
| 29 |
+
# Simulation loop
|
| 30 |
+
step = 0
|
| 31 |
+
while True:
|
| 32 |
+
# Update particle positions
|
| 33 |
+
x_new = x + vx * dt
|
| 34 |
+
y_new = y + vy * dt
|
| 35 |
+
|
| 36 |
+
# Handle collisions with walls and accumulate momentum transfer
|
| 37 |
+
for i in range(N):
|
| 38 |
+
if x_new[i] < 0:
|
| 39 |
+
x_new[i] = -x_new[i] # Reflect position
|
| 40 |
+
vx[i] = -vx[i] # Reverse velocity
|
| 41 |
+
total_momentum_transfer += 2 * abs(vx[i])
|
| 42 |
+
elif x_new[i] > L:
|
| 43 |
+
x_new[i] = 2 * L - x_new[i] # Reflect position
|
| 44 |
+
vx[i] = -vx[i] # Reverse velocity
|
| 45 |
+
total_momentum_transfer += 2 * abs(vx[i])
|
| 46 |
+
if y_new[i] < 0:
|
| 47 |
+
y_new[i] = -y_new[i] # Reflect position
|
| 48 |
+
vy[i] = -vy[i] # Reverse velocity
|
| 49 |
+
total_momentum_transfer += 2 * abs(vy[i])
|
| 50 |
+
elif y_new[i] > L:
|
| 51 |
+
y_new[i] = 2 * L - y_new[i] # Reflect position
|
| 52 |
+
vy[i] = -vy[i] # Reverse velocity
|
| 53 |
+
total_momentum_transfer += 2 * abs(vy[i])
|
| 54 |
+
|
| 55 |
+
# Update positions
|
| 56 |
+
x = x_new
|
| 57 |
+
y = y_new
|
| 58 |
+
|
| 59 |
+
# Increment step counter
|
| 60 |
+
step += 1
|
| 61 |
+
|
| 62 |
+
# Calculate and display temperature and pressure every 100 steps
|
| 63 |
+
if step % 100 == 0:
|
| 64 |
+
# Temperature: T = (1/2) * <v^2> in 2D (with k=1, m=1)
|
| 65 |
+
v_squared = vx**2 + vy**2
|
| 66 |
+
T = (1 / (2 * N)) * np.sum(v_squared)
|
| 67 |
+
|
| 68 |
+
# Pressure: P = momentum_transfer / (perimeter * time)
|
| 69 |
+
time_elapsed = 100 * dt
|
| 70 |
+
P = total_momentum_transfer / (4 * L * time_elapsed)
|
| 71 |
+
|
| 72 |
+
# Update metrics display
|
| 73 |
+
metrics_placeholder.write(f"Temperature: {T:.4f} | Pressure: {P:.4f}")
|
| 74 |
+
|
| 75 |
+
# Reset momentum transfer
|
| 76 |
+
total_momentum_transfer = 0
|
| 77 |
+
|
| 78 |
+
# Create and update the Plotly scatter plot
|
| 79 |
+
fig = go.Figure()
|
| 80 |
+
fig.add_trace(go.Scatter(x=x, y=y, mode='markers', marker=dict(size=5)))
|
| 81 |
+
fig.update_layout(
|
| 82 |
+
xaxis_range=[0, L],
|
| 83 |
+
yaxis_range=[0, L],
|
| 84 |
+
width=500,
|
| 85 |
+
height=500,
|
| 86 |
+
title="Gas Molecules in Motion"
|
| 87 |
+
)
|
| 88 |
+
plot_placeholder.plotly_chart(fig)
|
| 89 |
+
|
| 90 |
+
# Control animation speed
|
| 91 |
+
time.sleep(0.05)
|
pages/lenz-law.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
st.title("Lenz's Law Visual Simulation")
|
| 6 |
+
|
| 7 |
+
# --- Simulation Controls ---
|
| 8 |
+
st.header("Simulation Controls")
|
| 9 |
+
magnet_position = st.slider("Magnet Position", -5.0, 5.0, 0.0, step=0.1,
|
| 10 |
+
help="Control the horizontal position of the magnet.")
|
| 11 |
+
magnet_velocity = st.slider("Magnet Velocity (for change visualization)", -1.0, 1.0, 0.0, step=0.1,
|
| 12 |
+
help="Simulate magnet velocity to show change in flux (for visualization purposes only). A positive velocity means magnet is moving right, negative velocity means left.")
|
| 13 |
+
show_field_lines = st.checkbox("Show Magnetic Field Lines", value=True)
|
| 14 |
+
show_induced_current = st.checkbox("Show Induced Current", value=True)
|
| 15 |
+
show_forces = st.checkbox("Show Forces", value=True)
|
| 16 |
+
|
| 17 |
+
# --- Plotting Area ---
|
| 18 |
+
plot_placeholder = st.empty() # Placeholder for the plot
|
| 19 |
+
|
| 20 |
+
def draw_magnet(ax, x_magnet, magnet_width=1.0, magnet_height=0.5):
|
| 21 |
+
"""Draws the magnet on the plot."""
|
| 22 |
+
y_center = 0
|
| 23 |
+
ax.add_patch(plt.Rectangle((x_magnet - magnet_width / 2, y_center - magnet_height / 2), magnet_width, magnet_height,
|
| 24 |
+
facecolor='red', edgecolor='black'))
|
| 25 |
+
ax.text(x_magnet - magnet_width / 4, y_center, 'N', color='white', ha='center', va='center', fontweight='bold')
|
| 26 |
+
ax.text(x_magnet + magnet_width / 4, y_center, 'S', color='white', ha='center', va='center', fontweight='bold')
|
| 27 |
+
|
| 28 |
+
def draw_coil(ax, coil_x_center=0, coil_width=1.5, coil_height=1.0):
|
| 29 |
+
"""Draws the coil on the plot."""
|
| 30 |
+
y_center = 0
|
| 31 |
+
ax.add_patch(plt.Rectangle((coil_x_center - coil_width / 2, y_center - coil_height / 2), coil_width, coil_height,
|
| 32 |
+
facecolor='lightgray', edgecolor='black'))
|
| 33 |
+
ax.text(coil_x_center, y_center, 'Coil', color='black', ha='center', va='center')
|
| 34 |
+
|
| 35 |
+
def draw_magnetic_field_magnet(ax, x_magnet, field_strength=0.5, num_lines=15):
|
| 36 |
+
"""Draws simplified magnetic field lines from the magnet."""
|
| 37 |
+
y_center = 0
|
| 38 |
+
start_x = x_magnet - 0.5 # Approximate North pole position for field lines
|
| 39 |
+
end_x = x_magnet + 0.5 # Approximate South pole position
|
| 40 |
+
|
| 41 |
+
for i in range(num_lines):
|
| 42 |
+
y_offset = (i - num_lines // 2) * 0.2 * field_strength
|
| 43 |
+
if y_offset != 0: # Avoid drawing lines exactly on top of each other which can cause issues
|
| 44 |
+
ax.plot([start_x, end_x], [y_center + 0.2 + y_offset, y_center + 0.2 - y_offset], color='blue', linewidth=0.5, alpha=0.7) # Lines above
|
| 45 |
+
ax.plot([start_x, end_x], [y_center - 0.2 + y_offset, y_center - 0.2 - y_offset], color='blue', linewidth=0.5, alpha=0.7) # Lines below
|
| 46 |
+
|
| 47 |
+
def draw_induced_current(ax, coil_x_center, magnet_velocity):
|
| 48 |
+
"""Draws arrows indicating induced current direction based on magnet velocity."""
|
| 49 |
+
y_center = 0
|
| 50 |
+
current_direction = 0 # 0: No current, 1: Clockwise, -1: Counter-clockwise
|
| 51 |
+
|
| 52 |
+
if magnet_velocity > 0.1: # Magnet moving right (towards coil)
|
| 53 |
+
current_direction = 1 # Clockwise to oppose increasing flux into the page (assuming field is into the page when N pole approaches)
|
| 54 |
+
elif magnet_velocity < -0.1: # Magnet moving left (away from coil)
|
| 55 |
+
current_direction = -1 # Counter-clockwise to oppose decreasing flux into the page
|
| 56 |
+
|
| 57 |
+
if current_direction == 1: # Clockwise
|
| 58 |
+
ax.arrow(coil_x_center + 0.3, y_center + 0.4, 0, -0.3, head_width=0.1, head_length=0.1, fc='green', ec='green')
|
| 59 |
+
ax.arrow(coil_x_center - 0.3, y_center - 0.4, 0, 0.3, head_width=0.1, head_length=0.1, fc='green', ec='green')
|
| 60 |
+
ax.text(coil_x_center, y_center + 0.6, "Induced Current (Clockwise)", color='green', ha='center', va='center')
|
| 61 |
+
elif current_direction == -1: # Counter-clockwise
|
| 62 |
+
ax.arrow(coil_x_center + 0.3, y_center - 0.4, 0, 0.3, head_width=0.1, head_length=0.1, fc='green', ec='green')
|
| 63 |
+
ax.arrow(coil_x_center - 0.3, y_center + 0.4, 0, -0.3, head_width=0.1, head_length=0.1, fc='green', ec='green')
|
| 64 |
+
ax.text(coil_x_center, y_center + 0.6, "Induced Current (Counter-Clockwise)", color='green', ha='center', va='center')
|
| 65 |
+
else:
|
| 66 |
+
ax.text(coil_x_center, y_center + 0.6, "No Induced Current", color='gray', ha='center', va='center')
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def draw_force_arrow(ax, x_magnet, magnet_velocity):
|
| 70 |
+
"""Draws force arrows indicating repulsion or attraction based on Lenz's Law."""
|
| 71 |
+
force_direction = 0 # 0: No force, 1: Repulsion (away from coil), -1: Attraction (towards coil)
|
| 72 |
+
|
| 73 |
+
if magnet_velocity > 0.1: # Moving towards coil - Repulsion
|
| 74 |
+
force_direction = 1
|
| 75 |
+
elif magnet_velocity < -0.1: # Moving away from coil - Attraction
|
| 76 |
+
force_direction = -1
|
| 77 |
+
|
| 78 |
+
if force_direction == 1: # Repulsion - Force on Magnet to the Left
|
| 79 |
+
ax.arrow(x_magnet, 1.0, -0.5, 0, head_width=0.1, head_length=0.2, fc='purple', ec='purple')
|
| 80 |
+
ax.text(x_magnet - 0.25, 1.2, "Repulsive Force", color='purple', ha='center', va='center')
|
| 81 |
+
elif force_direction == -1: # Attraction - Force on Magnet to the Right
|
| 82 |
+
ax.arrow(x_magnet, 1.0, 0.5, 0, head_width=0.1, head_length=0.2, fc='purple', ec='purple')
|
| 83 |
+
ax.text(x_magnet + 0.25, 1.2, "Attractive Force", color='purple', ha='center', va='center')
|
| 84 |
+
else:
|
| 85 |
+
ax.text(x_magnet, 1.2, "No Force", color='gray', ha='center', va='center')
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# --- Main Plot Update Function ---
|
| 89 |
+
def update_plot(magnet_position, magnet_velocity, show_field_lines, show_induced_current, show_forces):
|
| 90 |
+
"""Updates the plot based on slider values and checkboxes."""
|
| 91 |
+
fig, ax = plt.subplots()
|
| 92 |
+
ax.set_xlim([-6, 6])
|
| 93 |
+
ax.set_ylim([-2, 2])
|
| 94 |
+
ax.set_aspect('equal') # Ensure circle is drawn as circle
|
| 95 |
+
ax.axis('off') # Hide axes
|
| 96 |
+
|
| 97 |
+
draw_coil(ax)
|
| 98 |
+
draw_magnet(ax, magnet_position)
|
| 99 |
+
|
| 100 |
+
if show_field_lines:
|
| 101 |
+
draw_magnetic_field_magnet(ax, magnet_position)
|
| 102 |
+
|
| 103 |
+
if show_induced_current:
|
| 104 |
+
draw_induced_current(ax, 0, magnet_velocity) # Coil is at x=0
|
| 105 |
+
|
| 106 |
+
if show_forces:
|
| 107 |
+
draw_force_arrow(ax, magnet_position, magnet_velocity)
|
| 108 |
+
|
| 109 |
+
plot_placeholder.pyplot(fig) # Update the plot in Streamlit
|
| 110 |
+
|
| 111 |
+
# --- Run the simulation and update plot on slider change ---
|
| 112 |
+
update_plot(magnet_position, magnet_velocity, show_field_lines, show_induced_current, show_forces)
|
pages/light-wave.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
# Title and description
|
| 7 |
+
st.title("Light Wave Simulation")
|
| 8 |
+
st.write("""
|
| 9 |
+
This simulation visualizes light as a transverse wave.
|
| 10 |
+
Adjust the wavelength and frequency to see how the wave changes.
|
| 11 |
+
Check the 'Animate' box to see the wave propagate over time.
|
| 12 |
+
Note: Units are arbitrary, and the wave speed is slowed down for visualization purposes.
|
| 13 |
+
""")
|
| 14 |
+
|
| 15 |
+
# Interactive sliders for wave parameters
|
| 16 |
+
wavelength = st.slider("Wavelength", min_value=0.1, max_value=10.0, value=1.0, step=0.1)
|
| 17 |
+
frequency = st.slider("Frequency", min_value=0.1, max_value=10.0, value=1.0, step=0.1)
|
| 18 |
+
|
| 19 |
+
# Checkbox to toggle animation
|
| 20 |
+
animate = st.checkbox("Animate")
|
| 21 |
+
|
| 22 |
+
# Define wave parameters
|
| 23 |
+
A = 1 # Amplitude (fixed at 1 for simplicity)
|
| 24 |
+
k = 2 * np.pi / wavelength # Wave number (k = 2π/λ)
|
| 25 |
+
omega = 2 * np.pi * frequency # Angular frequency (ω = 2πf)
|
| 26 |
+
|
| 27 |
+
# Spatial coordinates
|
| 28 |
+
x = np.linspace(0, 10, 1000) # Position array from 0 to 10 with 1000 points
|
| 29 |
+
|
| 30 |
+
# Animation or static plot based on user input
|
| 31 |
+
if animate:
|
| 32 |
+
# Create a placeholder for dynamic updates
|
| 33 |
+
placeholder = st.empty()
|
| 34 |
+
|
| 35 |
+
# Run animation for 100 frames
|
| 36 |
+
num_frames = 100
|
| 37 |
+
for frame in range(num_frames):
|
| 38 |
+
# Calculate time for this frame
|
| 39 |
+
t = frame * 0.1
|
| 40 |
+
|
| 41 |
+
# Compute wave amplitude at this time
|
| 42 |
+
y = A * np.sin(k * x - omega * t)
|
| 43 |
+
|
| 44 |
+
# Create and configure the plot
|
| 45 |
+
fig, ax = plt.subplots()
|
| 46 |
+
ax.plot(x, y)
|
| 47 |
+
ax.set_ylim(-1.5, 1.5) # Fixed y-axis limits to show wave clearly
|
| 48 |
+
ax.set_xlabel("Position")
|
| 49 |
+
ax.set_ylabel("Amplitude")
|
| 50 |
+
ax.set_title(f"Light Wave at t = {t:.1f}")
|
| 51 |
+
|
| 52 |
+
# Update the placeholder with the new plot
|
| 53 |
+
placeholder.pyplot(fig)
|
| 54 |
+
|
| 55 |
+
# Close the figure to prevent memory buildup
|
| 56 |
+
plt.close(fig)
|
| 57 |
+
|
| 58 |
+
# Control animation speed
|
| 59 |
+
time.sleep(0.1)
|
| 60 |
+
else:
|
| 61 |
+
# Display a static plot at t=0 when animation is off
|
| 62 |
+
t = 0
|
| 63 |
+
y = A * np.sin(k * x - omega * t)
|
| 64 |
+
fig, ax = plt.subplots()
|
| 65 |
+
ax.plot(x, y)
|
| 66 |
+
ax.set_ylim(-1.5, 1.5)
|
| 67 |
+
ax.set_xlabel("Position")
|
| 68 |
+
ax.set_ylabel("Amplitude")
|
| 69 |
+
ax.set_title("Light Wave at t = 0")
|
| 70 |
+
st.pyplot(fig)
|
pages/magnetic-field.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from matplotlib.patches import Circle
|
| 5 |
+
|
| 6 |
+
def magnetic_field_of_wire(x, y, z, current, wire_pos):
|
| 7 |
+
"""Calculate magnetic field due to infinite straight wire at position wire_pos"""
|
| 8 |
+
r_vec = np.array([x - wire_pos[0], y - wire_pos[1], 0])
|
| 9 |
+
r = np.sqrt(r_vec[0]**2 + r_vec[1]**2)
|
| 10 |
+
|
| 11 |
+
# Prevent division by zero
|
| 12 |
+
if r < 1e-10:
|
| 13 |
+
return np.array([0, 0, 0])
|
| 14 |
+
|
| 15 |
+
# Direction: cross product of z unit vector with r_vec
|
| 16 |
+
direction = np.array([-r_vec[1], r_vec[0], 0]) / r
|
| 17 |
+
|
| 18 |
+
# Magnitude: μ0 * I / (2π * r)
|
| 19 |
+
mu0 = 4 * np.pi * 1e-7 # magnetic permeability
|
| 20 |
+
magnitude = mu0 * current / (2 * np.pi * r)
|
| 21 |
+
|
| 22 |
+
return magnitude * direction
|
| 23 |
+
|
| 24 |
+
def magnetic_field_of_loop(x, y, z, current, loop_center, radius):
|
| 25 |
+
"""Calculate magnetic field due to a circular current loop in the xy-plane"""
|
| 26 |
+
# Distance from the point to loop center
|
| 27 |
+
r_vec = np.array([x - loop_center[0], y - loop_center[1], z - loop_center[2]])
|
| 28 |
+
r = np.sqrt(r_vec[0]**2 + r_vec[1]**2 + r_vec[2]**2)
|
| 29 |
+
|
| 30 |
+
# Prevent division by zero
|
| 31 |
+
if r < 1e-10:
|
| 32 |
+
return np.array([0, 0, 0])
|
| 33 |
+
|
| 34 |
+
# On-axis field calculation (simplified)
|
| 35 |
+
mu0 = 4 * np.pi * 1e-7 # magnetic permeability
|
| 36 |
+
|
| 37 |
+
if abs(r_vec[0]) < 1e-10 and abs(r_vec[1]) < 1e-10:
|
| 38 |
+
# On the z-axis
|
| 39 |
+
magnitude = mu0 * current * radius**2 / (2 * (radius**2 + r_vec[2]**2)**(3/2))
|
| 40 |
+
return np.array([0, 0, magnitude if r_vec[2] >= 0 else -magnitude])
|
| 41 |
+
|
| 42 |
+
# For off-axis points, return a more approximate field (full calculation is complex)
|
| 43 |
+
# This is a simplification that gives reasonable visualization
|
| 44 |
+
z_component = mu0 * current * radius**2 / (2 * (radius**2 + r**2)**(3/2))
|
| 45 |
+
r_component = mu0 * current * radius**2 * r_vec[2] / (4 * r * (radius**2 + r**2)**(3/2))
|
| 46 |
+
|
| 47 |
+
return np.array([
|
| 48 |
+
r_component * r_vec[0]/r,
|
| 49 |
+
r_component * r_vec[1]/r,
|
| 50 |
+
z_component
|
| 51 |
+
])
|
| 52 |
+
|
| 53 |
+
def magnetic_field_of_bar_magnet(x, y, z, magnet_center, magnet_length, magnet_strength):
|
| 54 |
+
"""Calculate magnetic field due to a simple bar magnet (dipole approximation)"""
|
| 55 |
+
r_vec = np.array([x - magnet_center[0], y - magnet_center[1], z - magnet_center[2]])
|
| 56 |
+
r = np.sqrt(r_vec[0]**2 + r_vec[1]**2 + r_vec[2]**2)
|
| 57 |
+
|
| 58 |
+
# Prevent division by zero
|
| 59 |
+
if r < 1e-10:
|
| 60 |
+
return np.array([0, 0, 0])
|
| 61 |
+
|
| 62 |
+
# Dipole moment in the z-direction
|
| 63 |
+
m_vec = np.array([0, 0, magnet_strength * magnet_length])
|
| 64 |
+
|
| 65 |
+
# Dipole field formula
|
| 66 |
+
mu0 = 4 * np.pi * 1e-7 # magnetic permeability
|
| 67 |
+
constant = mu0 / (4 * np.pi * r**5)
|
| 68 |
+
dot_product = np.dot(m_vec, r_vec)
|
| 69 |
+
|
| 70 |
+
field = constant * (3 * r_vec * dot_product - r**2 * m_vec)
|
| 71 |
+
|
| 72 |
+
return field
|
| 73 |
+
|
| 74 |
+
def plot_magnetic_field(field_type, parameters):
|
| 75 |
+
"""Generate the magnetic field plot"""
|
| 76 |
+
fig, ax = plt.subplots(figsize=(10, 8))
|
| 77 |
+
|
| 78 |
+
# Create grid of points
|
| 79 |
+
n = 20
|
| 80 |
+
x = np.linspace(-5, 5, n)
|
| 81 |
+
y = np.linspace(-5, 5, n)
|
| 82 |
+
X, Y = np.meshgrid(x, y)
|
| 83 |
+
Z = np.zeros_like(X) # Z=0 plane
|
| 84 |
+
|
| 85 |
+
# Calculate field at each point
|
| 86 |
+
U = np.zeros_like(X)
|
| 87 |
+
V = np.zeros_like(Y)
|
| 88 |
+
W = np.zeros_like(Z)
|
| 89 |
+
|
| 90 |
+
for i in range(n):
|
| 91 |
+
for j in range(n):
|
| 92 |
+
if field_type == "Wire":
|
| 93 |
+
field = magnetic_field_of_wire(
|
| 94 |
+
X[i, j], Y[i, j], Z[i, j],
|
| 95 |
+
parameters["current"],
|
| 96 |
+
parameters["position"]
|
| 97 |
+
)
|
| 98 |
+
elif field_type == "Loop":
|
| 99 |
+
field = magnetic_field_of_loop(
|
| 100 |
+
X[i, j], Y[i, j], Z[i, j],
|
| 101 |
+
parameters["current"],
|
| 102 |
+
parameters["center"],
|
| 103 |
+
parameters["radius"]
|
| 104 |
+
)
|
| 105 |
+
elif field_type == "Bar Magnet":
|
| 106 |
+
field = magnetic_field_of_bar_magnet(
|
| 107 |
+
X[i, j], Y[i, j], Z[i, j],
|
| 108 |
+
parameters["center"],
|
| 109 |
+
parameters["length"],
|
| 110 |
+
parameters["strength"]
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
# Normalize the field for better visualization
|
| 114 |
+
field_magnitude = np.sqrt(field[0]**2 + field[1]**2 + field[2]**2)
|
| 115 |
+
if field_magnitude > 0:
|
| 116 |
+
normalized_field = field / field_magnitude
|
| 117 |
+
else:
|
| 118 |
+
normalized_field = field
|
| 119 |
+
|
| 120 |
+
U[i, j] = normalized_field[0]
|
| 121 |
+
V[i, j] = normalized_field[1]
|
| 122 |
+
W[i, j] = normalized_field[2]
|
| 123 |
+
|
| 124 |
+
# Plot the magnetic field
|
| 125 |
+
ax.streamplot(X, Y, U, V, density=2, color='b', linewidth=1, arrowsize=1)
|
| 126 |
+
|
| 127 |
+
# Draw the source of the field
|
| 128 |
+
if field_type == "Wire":
|
| 129 |
+
ax.plot(parameters["position"][0], parameters["position"][1], 'ro', markersize=10)
|
| 130 |
+
ax.annotate('Current into page' if parameters["current"] > 0 else 'Current out of page',
|
| 131 |
+
xy=(parameters["position"][0], parameters["position"][1]),
|
| 132 |
+
xytext=(parameters["position"][0] + 0.5, parameters["position"][1] + 0.5))
|
| 133 |
+
elif field_type == "Loop":
|
| 134 |
+
circle = Circle(parameters["center"][:2], parameters["radius"], fill=False, color='r')
|
| 135 |
+
ax.add_patch(circle)
|
| 136 |
+
ax.annotate('Current loop',
|
| 137 |
+
xy=(parameters["center"][0], parameters["center"][1]),
|
| 138 |
+
xytext=(parameters["center"][0] + 0.5, parameters["center"][1] + 0.5))
|
| 139 |
+
elif field_type == "Bar Magnet":
|
| 140 |
+
half_length = parameters["length"] / 2
|
| 141 |
+
ax.plot([parameters["center"][0] - half_length, parameters["center"][0] + half_length],
|
| 142 |
+
[parameters["center"][1], parameters["center"][1]], 'r-', linewidth=4)
|
| 143 |
+
ax.text(parameters["center"][0] - half_length - 0.3, parameters["center"][1], 'S')
|
| 144 |
+
ax.text(parameters["center"][0] + half_length + 0.1, parameters["center"][1], 'N')
|
| 145 |
+
|
| 146 |
+
ax.set_xlim(-5, 5)
|
| 147 |
+
ax.set_ylim(-5, 5)
|
| 148 |
+
ax.set_xlabel('X')
|
| 149 |
+
ax.set_ylabel('Y')
|
| 150 |
+
ax.set_title(f'Magnetic Field of {field_type}')
|
| 151 |
+
ax.grid(True)
|
| 152 |
+
ax.set_aspect('equal')
|
| 153 |
+
|
| 154 |
+
return fig
|
| 155 |
+
|
| 156 |
+
def main():
|
| 157 |
+
st.title("Magnetic Field Visualization")
|
| 158 |
+
st.write("""
|
| 159 |
+
This app visualizes the magnetic field created by different sources.
|
| 160 |
+
Choose a magnetic field source below and adjust the parameters to see how the field changes.
|
| 161 |
+
""")
|
| 162 |
+
|
| 163 |
+
# Sidebar for parameters
|
| 164 |
+
st.sidebar.header("Field Source Parameters")
|
| 165 |
+
|
| 166 |
+
# Select field type
|
| 167 |
+
field_type = st.sidebar.selectbox(
|
| 168 |
+
"Select a magnetic field source:",
|
| 169 |
+
["Wire", "Loop", "Bar Magnet"]
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
# Parameters based on field type
|
| 173 |
+
if field_type == "Wire":
|
| 174 |
+
current = st.sidebar.slider("Current (A)", -10.0, 10.0, 5.0)
|
| 175 |
+
wire_x = st.sidebar.slider("Wire X Position", -3.0, 3.0, 0.0)
|
| 176 |
+
wire_y = st.sidebar.slider("Wire Y Position", -3.0, 3.0, 0.0)
|
| 177 |
+
|
| 178 |
+
parameters = {
|
| 179 |
+
"current": current,
|
| 180 |
+
"position": [wire_x, wire_y]
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
st.write("""
|
| 184 |
+
### Infinite Straight Wire
|
| 185 |
+
|
| 186 |
+
The magnetic field around a long straight wire forms concentric circles around the wire.
|
| 187 |
+
- Direction: determined by the right-hand rule (thumb points in the current direction)
|
| 188 |
+
- Magnitude: proportional to the current and inversely proportional to the distance from the wire
|
| 189 |
+
|
| 190 |
+
Mathematical formula: B = (μ₀ × I) / (2π × r)
|
| 191 |
+
""")
|
| 192 |
+
|
| 193 |
+
elif field_type == "Loop":
|
| 194 |
+
current = st.sidebar.slider("Current (A)", -10.0, 10.0, 5.0)
|
| 195 |
+
loop_x = st.sidebar.slider("Loop Center X", -3.0, 3.0, 0.0)
|
| 196 |
+
loop_y = st.sidebar.slider("Loop Center Y", -3.0, 3.0, 0.0)
|
| 197 |
+
loop_z = st.sidebar.slider("Loop Center Z", -3.0, 3.0, 0.0)
|
| 198 |
+
radius = st.sidebar.slider("Loop Radius", 0.5, 3.0, 1.0)
|
| 199 |
+
|
| 200 |
+
parameters = {
|
| 201 |
+
"current": current,
|
| 202 |
+
"center": [loop_x, loop_y, loop_z],
|
| 203 |
+
"radius": radius
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
st.write("""
|
| 207 |
+
### Current Loop
|
| 208 |
+
|
| 209 |
+
A current loop creates a magnetic field similar to a bar magnet.
|
| 210 |
+
- Near the center of the loop, the field lines are nearly parallel
|
| 211 |
+
- Far from the loop, the field approximates that of a dipole
|
| 212 |
+
|
| 213 |
+
This is the basic principle behind electromagnets and solenoids.
|
| 214 |
+
""")
|
| 215 |
+
|
| 216 |
+
elif field_type == "Bar Magnet":
|
| 217 |
+
magnet_x = st.sidebar.slider("Magnet Center X", -3.0, 3.0, 0.0)
|
| 218 |
+
magnet_y = st.sidebar.slider("Magnet Center Y", -3.0, 3.0, 0.0)
|
| 219 |
+
magnet_z = st.sidebar.slider("Magnet Center Z", -3.0, 3.0, 0.0)
|
| 220 |
+
length = st.sidebar.slider("Magnet Length", 0.5, 3.0, 2.0)
|
| 221 |
+
strength = st.sidebar.slider("Magnet Strength", 1.0, 10.0, 5.0)
|
| 222 |
+
|
| 223 |
+
parameters = {
|
| 224 |
+
"center": [magnet_x, magnet_y, magnet_z],
|
| 225 |
+
"length": length,
|
| 226 |
+
"strength": strength
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
st.write("""
|
| 230 |
+
### Bar Magnet
|
| 231 |
+
|
| 232 |
+
A bar magnet produces a dipole magnetic field.
|
| 233 |
+
- Field lines emerge from the north pole and enter the south pole
|
| 234 |
+
- The field strength decreases with the cube of the distance from the magnet
|
| 235 |
+
|
| 236 |
+
This simulation uses a simplified dipole approximation.
|
| 237 |
+
""")
|
| 238 |
+
|
| 239 |
+
# Generate and display plot
|
| 240 |
+
fig = plot_magnetic_field(field_type, parameters)
|
| 241 |
+
st.pyplot(fig)
|
| 242 |
+
|
| 243 |
+
# Additional explanations
|
| 244 |
+
st.write("""
|
| 245 |
+
### About Magnetic Fields
|
| 246 |
+
|
| 247 |
+
Magnetic fields are vector fields that describe the magnetic influence on moving electric charges, electric currents, and magnetic materials.
|
| 248 |
+
|
| 249 |
+
Key concepts:
|
| 250 |
+
- Magnetic field lines are continuous loops
|
| 251 |
+
- The density of field lines indicates the strength of the field
|
| 252 |
+
- The direction of the field is determined by the right-hand rule
|
| 253 |
+
|
| 254 |
+
The visualization shows a 2D slice (Z=0 plane) of the magnetic field, with arrows indicating the field direction.
|
| 255 |
+
""")
|
| 256 |
+
|
| 257 |
+
if __name__ == "__main__":
|
| 258 |
+
main()
|
pages/mass-energy-equivalence.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
# Define the speed of light constant (in m/s)
|
| 6 |
+
c = 3e8 # 300,000,000 m/s
|
| 7 |
+
|
| 8 |
+
# Set up the Streamlit app title and description.
|
| 9 |
+
st.title("Mass–Energy Equivalence Visualization")
|
| 10 |
+
st.write("""
|
| 11 |
+
Einstein's famous equation \(E = mc^2\) tells us that mass and energy are interchangeable.
|
| 12 |
+
Use the slider below to adjust the mass and see the equivalent energy.
|
| 13 |
+
""")
|
| 14 |
+
|
| 15 |
+
# Create a slider for selecting the mass (in kg)
|
| 16 |
+
mass = st.slider("Select mass (kg)", min_value=0.001, max_value=100.0, value=1.0, step=0.001)
|
| 17 |
+
|
| 18 |
+
# Calculate energy using E = m * c^2
|
| 19 |
+
energy = mass * c**2
|
| 20 |
+
|
| 21 |
+
# Display the calculated energy
|
| 22 |
+
st.write(f"**For a mass of {mass:.3f} kg, the equivalent energy is {energy:.3e} Joules.**")
|
| 23 |
+
|
| 24 |
+
# Additional explanation for perspective
|
| 25 |
+
st.write("""
|
| 26 |
+
For context, 1 kg of mass is equivalent to about 9 × 10^16 Joules of energy –
|
| 27 |
+
an amount that can power entire cities for days.
|
| 28 |
+
""")
|
| 29 |
+
|
| 30 |
+
# Plotting: Show a graph of Energy vs Mass for a range from 0 up to the selected mass.
|
| 31 |
+
st.subheader("Energy as a Function of Mass")
|
| 32 |
+
mass_values = np.linspace(0, mass, 100)
|
| 33 |
+
energy_values = mass_values * c**2
|
| 34 |
+
|
| 35 |
+
# Create a DataFrame for plotting
|
| 36 |
+
df = pd.DataFrame({
|
| 37 |
+
"Mass (kg)": mass_values,
|
| 38 |
+
"Energy (Joules)": energy_values
|
| 39 |
+
})
|
| 40 |
+
|
| 41 |
+
# Display the line chart
|
| 42 |
+
st.line_chart(df.set_index("Mass (kg)"))
|
pages/mass.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import plotly.graph_objs as go
|
| 5 |
+
import plotly.express as px
|
| 6 |
+
import pandas as pd
|
| 7 |
+
|
| 8 |
+
def introduction_to_mass():
|
| 9 |
+
"""
|
| 10 |
+
Provides an introductory explanation of mass
|
| 11 |
+
"""
|
| 12 |
+
st.header("Understanding Mass 🧊")
|
| 13 |
+
st.markdown("""
|
| 14 |
+
Mass is a fundamental property of matter that represents the amount of material in an object.
|
| 15 |
+
It is a measure of an object's resistance to acceleration when a force is applied.
|
| 16 |
+
|
| 17 |
+
Key Characteristics of Mass:
|
| 18 |
+
- Measured in kilograms (kg)
|
| 19 |
+
- Remains constant regardless of location (unlike weight)
|
| 20 |
+
- Determines an object's inertia
|
| 21 |
+
""")
|
| 22 |
+
|
| 23 |
+
def mass_vs_weight_simulation():
|
| 24 |
+
"""
|
| 25 |
+
Interactive simulation showing difference between mass and weight
|
| 26 |
+
"""
|
| 27 |
+
st.header("Mass vs Weight Comparison 🌍")
|
| 28 |
+
|
| 29 |
+
# Gravity values for different celestial bodies
|
| 30 |
+
gravity_dict = {
|
| 31 |
+
"Earth": 9.8,
|
| 32 |
+
"Moon": 1.62,
|
| 33 |
+
"Mars": 3.72,
|
| 34 |
+
"Jupiter": 24.79
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# User input for mass
|
| 38 |
+
mass = st.slider("Select Object Mass (kg)", min_value=1, max_value=100, value=50)
|
| 39 |
+
|
| 40 |
+
# Create DataFrame for visualization
|
| 41 |
+
data = []
|
| 42 |
+
for planet, gravity in gravity_dict.items():
|
| 43 |
+
weight = mass * gravity
|
| 44 |
+
data.append({"Celestial Body": planet, "Weight (N)": weight})
|
| 45 |
+
|
| 46 |
+
df = pd.DataFrame(data)
|
| 47 |
+
|
| 48 |
+
# Plotly bar chart
|
| 49 |
+
fig = px.bar(df, x="Celestial Body", y="Weight (N)",
|
| 50 |
+
title=f"Weight of a {mass} kg Object on Different Planets",
|
| 51 |
+
labels={"Weight (N)": "Weight (Newtons)"})
|
| 52 |
+
st.plotly_chart(fig)
|
| 53 |
+
|
| 54 |
+
# Explanation
|
| 55 |
+
st.markdown(f"""
|
| 56 |
+
🔍 For a {mass} kg object:
|
| 57 |
+
- Mass remains constant at {mass} kg everywhere
|
| 58 |
+
- Weight changes based on local gravitational acceleration
|
| 59 |
+
""")
|
| 60 |
+
|
| 61 |
+
def density_visualization():
|
| 62 |
+
"""
|
| 63 |
+
Visualization of mass density
|
| 64 |
+
"""
|
| 65 |
+
st.header("Mass Density Exploration 📊")
|
| 66 |
+
|
| 67 |
+
# Predefined materials and their densities
|
| 68 |
+
materials = {
|
| 69 |
+
"Air": 0.001225,
|
| 70 |
+
"Water": 1000,
|
| 71 |
+
"Wood": 700,
|
| 72 |
+
"Aluminum": 2700,
|
| 73 |
+
"Iron": 7874,
|
| 74 |
+
"Lead": 11340
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
# User selects volume
|
| 78 |
+
volume = st.slider("Select Volume (m³)", min_value=0.1, max_value=10.0, value=1.0, step=0.1)
|
| 79 |
+
|
| 80 |
+
# Calculate masses
|
| 81 |
+
masses = {material: density * volume for material, density in materials.items()}
|
| 82 |
+
|
| 83 |
+
# Create DataFrame
|
| 84 |
+
df = pd.DataFrame.from_dict(masses, orient='index', columns=['Mass (kg)'])
|
| 85 |
+
df.index.name = 'Material'
|
| 86 |
+
df = df.reset_index()
|
| 87 |
+
|
| 88 |
+
# Plotly bar chart
|
| 89 |
+
fig = px.bar(df, x='Material', y='Mass (kg)',
|
| 90 |
+
title=f"Mass of Different Materials (Volume: {volume} m³)",
|
| 91 |
+
labels={'Mass (kg)': 'Mass (kg)'})
|
| 92 |
+
st.plotly_chart(fig)
|
| 93 |
+
|
| 94 |
+
st.markdown("""
|
| 95 |
+
💡 Density determines how much mass is contained in a given volume:
|
| 96 |
+
- Low density: Less mass per volume (e.g., Air)
|
| 97 |
+
- High density: More mass per volume (e.g., Lead)
|
| 98 |
+
""")
|
| 99 |
+
|
| 100 |
+
def main():
|
| 101 |
+
"""
|
| 102 |
+
Main Streamlit app
|
| 103 |
+
"""
|
| 104 |
+
st.title("Mass Visualization Simulator 🔬")
|
| 105 |
+
|
| 106 |
+
# Sidebar for navigation
|
| 107 |
+
page = st.sidebar.selectbox("Choose a Visualization", [
|
| 108 |
+
"Introduction to Mass",
|
| 109 |
+
"Mass vs Weight",
|
| 110 |
+
"Density Exploration"
|
| 111 |
+
])
|
| 112 |
+
|
| 113 |
+
# Page routing
|
| 114 |
+
if page == "Introduction to Mass":
|
| 115 |
+
introduction_to_mass()
|
| 116 |
+
elif page == "Mass vs Weight":
|
| 117 |
+
mass_vs_weight_simulation()
|
| 118 |
+
elif page == "Density Exploration":
|
| 119 |
+
density_visualization()
|
| 120 |
+
|
| 121 |
+
if __name__ == "__main__":
|
| 122 |
+
main()
|
pages/maxwells-equations.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
st.title("Visualizing Maxwell's Equations")
|
| 7 |
+
st.write("An illustrative visualization of fundamental concepts related to Maxwell's Equations.")
|
| 8 |
+
|
| 9 |
+
equation_choice = st.selectbox("Choose a Maxwell's Equation Concept to Visualize:",
|
| 10 |
+
["Electric Field of a Point Charge (Gauss's Law for Electricity)",
|
| 11 |
+
"Magnetic Field of a Current-Carrying Wire (Ampere's Law)",
|
| 12 |
+
"Electromagnetic Plane Wave (Faraday's & Ampere-Maxwell - Simplified)"])
|
| 13 |
+
|
| 14 |
+
# --- Visualization Functions ---
|
| 15 |
+
|
| 16 |
+
def electric_field_point_charge(charge_pos_x, charge_pos_y, charge_magnitude, grid_size):
|
| 17 |
+
"""Visualizes the electric field of a point charge."""
|
| 18 |
+
x = np.linspace(-grid_size, grid_size, 30)
|
| 19 |
+
y = np.linspace(-grid_size, grid_size, 30)
|
| 20 |
+
X, Y = np.meshgrid(x, y)
|
| 21 |
+
|
| 22 |
+
Ex = np.zeros_like(X)
|
| 23 |
+
Ey = np.zeros_like(Y)
|
| 24 |
+
|
| 25 |
+
for i in range(X.shape[0]):
|
| 26 |
+
for j in range(X.shape[1]):
|
| 27 |
+
r_vec = np.array([X[i, j] - charge_pos_x, Y[i, j] - charge_pos_y])
|
| 28 |
+
r_mag = np.linalg.norm(r_vec)
|
| 29 |
+
if r_mag > 0.1: # Avoid singularity at charge location
|
| 30 |
+
E_direction = r_vec / r_mag
|
| 31 |
+
E_magnitude = charge_magnitude / (r_mag**2) #Simplified formula, ignoring constants for visualization
|
| 32 |
+
Ex[i, j] = E_magnitude * E_direction[0]
|
| 33 |
+
Ey[i, j] = E_magnitude * E_direction[1]
|
| 34 |
+
|
| 35 |
+
return X, Y, Ex, Ey
|
| 36 |
+
|
| 37 |
+
def magnetic_field_wire(wire_pos_x, wire_pos_y, current_magnitude, grid_size):
|
| 38 |
+
"""Visualizes the magnetic field around a long straight wire."""
|
| 39 |
+
x = np.linspace(-grid_size, grid_size, 30)
|
| 40 |
+
y = np.linspace(-grid_size, grid_size, 30)
|
| 41 |
+
X, Y = np.meshgrid(x, y)
|
| 42 |
+
|
| 43 |
+
Bx = np.zeros_like(X)
|
| 44 |
+
By = np.zeros_like(Y)
|
| 45 |
+
Bz = np.zeros_like(X) # Magnetic field in the Z direction (out of plane)
|
| 46 |
+
|
| 47 |
+
for i in range(X.shape[0]):
|
| 48 |
+
for j in range(X.shape[1]):
|
| 49 |
+
r_vec = np.array([X[i, j] - wire_pos_x, Y[i, j] - wire_pos_y])
|
| 50 |
+
r_mag = np.linalg.norm(r_vec)
|
| 51 |
+
if r_mag > 0.1: # Avoid singularity at wire location
|
| 52 |
+
B_direction = np.array([-r_vec[1], r_vec[0]]) / r_mag # Tangential direction (right-hand rule)
|
| 53 |
+
B_magnitude = current_magnitude / r_mag # Simplified, ignoring constants
|
| 54 |
+
Bx[i, j] = B_magnitude * B_direction[0]
|
| 55 |
+
By[i, j] = B_magnitude * B_direction[1]
|
| 56 |
+
Bz[i,j] = 0 # for 2D visualization, assume B is in xy plane
|
| 57 |
+
|
| 58 |
+
return X, Y, Bx, By, Bz
|
| 59 |
+
|
| 60 |
+
def electromagnetic_plane_wave(time_step, grid_size, wave_direction):
|
| 61 |
+
"""Illustrative plane EM wave propagation (simplified)."""
|
| 62 |
+
x = np.linspace(-grid_size, grid_size, 30)
|
| 63 |
+
y = np.linspace(-grid_size, grid_size, 30)
|
| 64 |
+
X, Y = np.meshgrid(x, y)
|
| 65 |
+
t = time_step
|
| 66 |
+
|
| 67 |
+
# Simplified Plane Wave along x-axis for illustration
|
| 68 |
+
if wave_direction == "x":
|
| 69 |
+
Ex = np.sin(X - t)
|
| 70 |
+
Ey = np.zeros_like(X)
|
| 71 |
+
Ez = np.zeros_like(X)
|
| 72 |
+
Bx = np.zeros_like(X)
|
| 73 |
+
By = np.zeros_like(X)
|
| 74 |
+
Bz = np.sin(X - t) # B field perpendicular to E and propagation direction (along z)
|
| 75 |
+
elif wave_direction == "y":
|
| 76 |
+
Ex = np.zeros_like(X)
|
| 77 |
+
Ey = np.sin(Y - t)
|
| 78 |
+
Ez = np.zeros_like(X)
|
| 79 |
+
Bx = np.sin(Y - t) # B perpendicular to E and prop. direction (along x)
|
| 80 |
+
By = np.zeros_like(X)
|
| 81 |
+
Bz = np.zeros_like(X)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
return X, Y, Ex, Ey, Ez, Bx, By, Bz
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# --- Streamlit UI based on equation choice ---
|
| 88 |
+
|
| 89 |
+
st.subheader("Visualization Parameters")
|
| 90 |
+
|
| 91 |
+
if equation_choice == "Electric Field of a Point Charge (Gauss's Law for Electricity)":
|
| 92 |
+
charge_pos_x = st.slider("Charge Position X", -5.0, 5.0, 0.0)
|
| 93 |
+
charge_pos_y = st.slider("Charge Position Y", -5.0, 5.0, 0.0)
|
| 94 |
+
charge_magnitude = st.slider("Charge Magnitude", -5.0, 5.0, 1.0)
|
| 95 |
+
grid_size = st.slider("Grid Size", 5, 15, 10)
|
| 96 |
+
|
| 97 |
+
X, Y, Ex, Ey = electric_field_point_charge(charge_pos_x, charge_pos_y, charge_magnitude, grid_size)
|
| 98 |
+
|
| 99 |
+
fig, ax = plt.subplots()
|
| 100 |
+
q = ax.quiver(X, Y, Ex, Ey, color='r', label='Electric Field (E)')
|
| 101 |
+
ax.plot(charge_pos_x, charge_pos_y, 'ro', markersize=10, label='Point Charge')
|
| 102 |
+
ax.set_title("Electric Field of a Point Charge")
|
| 103 |
+
ax.set_xlabel("x")
|
| 104 |
+
ax.set_ylabel("y")
|
| 105 |
+
ax.axis('equal')
|
| 106 |
+
ax.legend()
|
| 107 |
+
st.pyplot(fig)
|
| 108 |
+
|
| 109 |
+
elif equation_choice == "Magnetic Field of a Current-Carrying Wire (Ampere's Law)":
|
| 110 |
+
wire_pos_x = st.slider("Wire Position X", -5.0, 5.0, 0.0)
|
| 111 |
+
wire_pos_y = st.slider("Wire Position Y", -5.0, 5.0, 0.0)
|
| 112 |
+
current_magnitude = st.slider("Current Magnitude (out of plane)", -5.0, 5.0, 1.0)
|
| 113 |
+
grid_size = st.slider("Grid Size", 5, 15, 10)
|
| 114 |
+
|
| 115 |
+
X, Y, Bx, By, Bz = magnetic_field_wire(wire_pos_x, wire_pos_y, current_magnitude, grid_size)
|
| 116 |
+
|
| 117 |
+
fig, ax = plt.subplots()
|
| 118 |
+
q = ax.quiver(X, Y, Bx, By, color='b', label='Magnetic Field (B)')
|
| 119 |
+
ax.plot(wire_pos_x, wire_pos_y, 'ko', markersize=10, label='Wire (Current out of plane)')
|
| 120 |
+
ax.set_title("Magnetic Field of a Current-Carrying Wire")
|
| 121 |
+
ax.set_xlabel("x")
|
| 122 |
+
ax.set_ylabel("y")
|
| 123 |
+
ax.axis('equal')
|
| 124 |
+
ax.legend()
|
| 125 |
+
st.pyplot(fig)
|
| 126 |
+
|
| 127 |
+
elif equation_choice == "Electromagnetic Plane Wave (Faraday's & Ampere-Maxwell - Simplified)":
|
| 128 |
+
wave_direction = st.selectbox("Wave Propagation Direction", ["x", "y"])
|
| 129 |
+
grid_size = st.slider("Grid Size", 5, 15, 10)
|
| 130 |
+
|
| 131 |
+
placeholder = st.empty() # For animation
|
| 132 |
+
|
| 133 |
+
for t in range(0, 100): # Animation loop
|
| 134 |
+
X, Y, Ex, Ey, Ez, Bx, By, Bz = electromagnetic_plane_wave(t * 0.1, grid_size, wave_direction) # Time step
|
| 135 |
+
fig, ax = plt.subplots()
|
| 136 |
+
q_e = ax.quiver(X[::2, ::2], Y[::2, ::2], Ex[::2, ::2], Ey[::2, ::2], color='r', label='Electric Field (E)', scale=50) # Subsample for clarity
|
| 137 |
+
q_b = ax.quiver(X[::2, ::2], Y[::2, ::2], Bx[::2, ::2], By[::2, ::2], color='b', label='Magnetic Field (B)', scale=50) # Subsample
|
| 138 |
+
ax.set_title("Electromagnetic Plane Wave Propagation")
|
| 139 |
+
ax.set_xlabel("x")
|
| 140 |
+
ax.set_ylabel("y")
|
| 141 |
+
ax.axis('equal')
|
| 142 |
+
ax.legend()
|
| 143 |
+
placeholder.pyplot(fig) # Update plot in placeholder
|
| 144 |
+
time.sleep(0.1) # Control animation speed
|
| 145 |
+
plt.close(fig) # Clear figure for next frame
|
pages/mechanical-advantage.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
|
| 4 |
+
st.title("Mechanical Advantage Simulation: Lever")
|
| 5 |
+
|
| 6 |
+
st.write(
|
| 7 |
+
"""
|
| 8 |
+
This simulation visualizes the concept of mechanical advantage for a simple lever.
|
| 9 |
+
Adjust the parameters below to see how the input force is amplified.
|
| 10 |
+
"""
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
# Slider inputs for the simulation parameters
|
| 14 |
+
effort_arm = st.slider(
|
| 15 |
+
"Effort Arm Length (distance from fulcrum to applied force, in meters)",
|
| 16 |
+
min_value=0.1, max_value=10.0, value=5.0, step=0.1
|
| 17 |
+
)
|
| 18 |
+
load_arm = st.slider(
|
| 19 |
+
"Load Arm Length (distance from fulcrum to load, in meters)",
|
| 20 |
+
min_value=0.1, max_value=10.0, value=2.0, step=0.1
|
| 21 |
+
)
|
| 22 |
+
input_force = st.slider(
|
| 23 |
+
"Input Force (in Newtons)",
|
| 24 |
+
min_value=0.0, max_value=100.0, value=10.0, step=0.5
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Calculate mechanical advantage and output force
|
| 28 |
+
mechanical_advantage = effort_arm / load_arm
|
| 29 |
+
output_force = input_force * mechanical_advantage
|
| 30 |
+
|
| 31 |
+
st.markdown(f"### Mechanical Advantage: {mechanical_advantage:.2f}")
|
| 32 |
+
st.markdown(f"### Output Force: {output_force:.2f} N")
|
| 33 |
+
|
| 34 |
+
# Plotting a simple lever diagram
|
| 35 |
+
fig, ax = plt.subplots(figsize=(8, 3))
|
| 36 |
+
ax.set_xlim(-1, effort_arm + load_arm + 1)
|
| 37 |
+
ax.set_ylim(-3, 3)
|
| 38 |
+
ax.axhline(0, color="black", linewidth=2)
|
| 39 |
+
|
| 40 |
+
# Define positions for the fulcrum, effort, and load
|
| 41 |
+
fulcrum_pos = effort_arm
|
| 42 |
+
load_pos = fulcrum_pos + load_arm
|
| 43 |
+
|
| 44 |
+
# Plot the fulcrum
|
| 45 |
+
ax.plot(fulcrum_pos, 0, marker="v", markersize=15, color="red")
|
| 46 |
+
ax.annotate("Fulcrum", xy=(fulcrum_pos, 0), xytext=(fulcrum_pos, -0.5),
|
| 47 |
+
ha="center", color="red")
|
| 48 |
+
|
| 49 |
+
# Draw the lever as a horizontal line
|
| 50 |
+
ax.plot([0, load_pos], [0, 0], color="gray", linewidth=3)
|
| 51 |
+
|
| 52 |
+
# Plot effort force arrow
|
| 53 |
+
ax.arrow(0, 0, fulcrum_pos * 0.8, 1.0, head_width=0.3, head_length=0.3, fc="green", ec="green")
|
| 54 |
+
ax.text(0, 1.2, "Effort", color="green", ha="center")
|
| 55 |
+
|
| 56 |
+
# Plot load force arrow (direction reversed to indicate opposition)
|
| 57 |
+
ax.arrow(load_pos, 0, -load_arm * 0.8, -1.0, head_width=0.3, head_length=0.3, fc="blue", ec="blue")
|
| 58 |
+
ax.text(load_pos, -1.2, "Load", color="blue", ha="center")
|
| 59 |
+
|
| 60 |
+
# Remove axes for clarity
|
| 61 |
+
ax.axis("off")
|
| 62 |
+
|
| 63 |
+
st.pyplot(fig)
|
pages/moment-of-inertia.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
# Title and Introduction
|
| 6 |
+
st.title("Moment of Inertia Simulation")
|
| 7 |
+
st.markdown("### What is Moment of Inertia?")
|
| 8 |
+
st.write("Moment of Inertia (I) measures an object's resistance to rotational motion. It depends on the mass and its distribution relative to the axis of rotation. In this simulation, adjust the masses, their positions, and the axis to see how I changes.")
|
| 9 |
+
|
| 10 |
+
# Sidebar for Number of Masses
|
| 11 |
+
st.sidebar.title("Settings")
|
| 12 |
+
num_masses = st.sidebar.slider("Number of masses", 1, 5, 1)
|
| 13 |
+
|
| 14 |
+
# Input Sliders for Masses and Positions
|
| 15 |
+
masses = []
|
| 16 |
+
positions = []
|
| 17 |
+
for i in range(num_masses):
|
| 18 |
+
st.subheader(f"Mass {i+1}")
|
| 19 |
+
col1, col2 = st.columns(2)
|
| 20 |
+
with col1:
|
| 21 |
+
m = st.slider(f"Mass (kg)", 0.1, 10.0, 1.0, key=f"m{i}")
|
| 22 |
+
with col2:
|
| 23 |
+
r = st.slider(f"Position (m)", 0.0, 1.0, 0.5, key=f"r{i}")
|
| 24 |
+
masses.append(m)
|
| 25 |
+
positions.append(r)
|
| 26 |
+
|
| 27 |
+
# Axis of Rotation Slider
|
| 28 |
+
axis_pos = st.slider("Axis of rotation position (m)", 0.0, 1.0, 0.0)
|
| 29 |
+
|
| 30 |
+
# Calculations
|
| 31 |
+
total_mass = sum(masses)
|
| 32 |
+
x_cm = sum(m * r for m, r in zip(masses, positions)) / total_mass if total_mass > 0 else 0
|
| 33 |
+
I = sum(m * (r - axis_pos)**2 for m, r in zip(masses, positions))
|
| 34 |
+
|
| 35 |
+
# Display Formula and Results
|
| 36 |
+
st.write("The Moment of Inertia is calculated as:")
|
| 37 |
+
st.latex(r"I = \sum_{i} m_i (r_i - a)^2")
|
| 38 |
+
st.write(f"where *a* is the axis position: {axis_pos} m")
|
| 39 |
+
st.write("**Calculation details:**")
|
| 40 |
+
for i, (m, r) in enumerate(zip(masses, positions)):
|
| 41 |
+
contribution = m * (r - axis_pos)**2
|
| 42 |
+
st.write(f"Mass {i+1}: {m} kg at {r} m, distance to axis: {abs(r - axis_pos):.2f} m, contribution: {contribution:.2f} kg m²")
|
| 43 |
+
st.write(f"**Total Moment of Inertia: {I:.2f} kg m²**")
|
| 44 |
+
|
| 45 |
+
# Plot Rod with Masses
|
| 46 |
+
fig, ax = plt.subplots()
|
| 47 |
+
ax.plot([0, 1], [0, 0], 'k-', linewidth=2, label="Rod")
|
| 48 |
+
for r in positions:
|
| 49 |
+
ax.plot(r, 0, 'ro', markersize=10) # Masses as red circles
|
| 50 |
+
ax.axvline(x=axis_pos, color='b', linestyle='--', label="Axis of Rotation")
|
| 51 |
+
ax.plot(x_cm, 0, 'g*', markersize=15, label="Center of Mass")
|
| 52 |
+
ax.set_xlim(-0.1, 1.1)
|
| 53 |
+
ax.set_ylim(-0.1, 0.1)
|
| 54 |
+
ax.set_xlabel("Position (m)")
|
| 55 |
+
ax.set_title("Rod with Masses")
|
| 56 |
+
ax.legend()
|
| 57 |
+
st.pyplot(fig)
|
| 58 |
+
|
| 59 |
+
# Plot I vs. Axis Position
|
| 60 |
+
st.subheader("Moment of Inertia vs. Axis Position")
|
| 61 |
+
axis_positions = np.linspace(0, 1, 100)
|
| 62 |
+
I_values = [sum(m * (r - a)**2 for m, r in zip(masses, positions)) for a in axis_positions]
|
| 63 |
+
fig2, ax2 = plt.subplots()
|
| 64 |
+
ax2.plot(axis_positions, I_values, label="I vs. Axis Position")
|
| 65 |
+
ax2.axvline(x=x_cm, color='g', linestyle='--', label="Center of Mass")
|
| 66 |
+
ax2.set_xlabel("Axis Position (m)")
|
| 67 |
+
ax2.set_ylabel("Moment of Inertia (kg m²)")
|
| 68 |
+
ax2.legend()
|
| 69 |
+
st.pyplot(fig2)
|
| 70 |
+
|
| 71 |
+
# Torque and Angular Acceleration
|
| 72 |
+
st.subheader("Apply Torque")
|
| 73 |
+
torque = st.number_input("Torque (N m)", 0.0, 100.0, 1.0)
|
| 74 |
+
if I > 0:
|
| 75 |
+
alpha = torque / I
|
| 76 |
+
st.write(f"Angular acceleration: {alpha:.2f} rad/s²")
|
| 77 |
+
else:
|
| 78 |
+
st.write("Moment of Inertia is zero, cannot calculate angular acceleration.")
|
pages/momentum.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import matplotlib.animation as animation
|
| 5 |
+
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
|
| 6 |
+
|
| 7 |
+
class MomentumSimulation:
|
| 8 |
+
def __init__(self, mass1, velocity1, mass2, velocity2):
|
| 9 |
+
"""
|
| 10 |
+
Initialize the momentum simulation with two objects
|
| 11 |
+
"""
|
| 12 |
+
self.mass1 = mass1
|
| 13 |
+
self.velocity1 = velocity1
|
| 14 |
+
self.mass2 = mass2
|
| 15 |
+
self.velocity2 = velocity2
|
| 16 |
+
|
| 17 |
+
def calculate_momentum(self):
|
| 18 |
+
"""
|
| 19 |
+
Calculate initial and final momentum
|
| 20 |
+
"""
|
| 21 |
+
initial_momentum = self.mass1 * self.velocity1 + self.mass2 * self.velocity2
|
| 22 |
+
|
| 23 |
+
# Simplified elastic collision calculation
|
| 24 |
+
v1_final = ((self.mass1 - self.mass2) * self.velocity1 +
|
| 25 |
+
2 * self.mass2 * self.velocity2) / (self.mass1 + self.mass2)
|
| 26 |
+
|
| 27 |
+
v2_final = ((self.mass2 - self.mass1) * self.velocity2 +
|
| 28 |
+
2 * self.mass1 * self.velocity1) / (self.mass1 + self.mass2)
|
| 29 |
+
|
| 30 |
+
final_momentum = self.mass1 * v1_final + self.mass2 * v2_final
|
| 31 |
+
|
| 32 |
+
return initial_momentum, final_momentum, v1_final, v2_final
|
| 33 |
+
|
| 34 |
+
def visualize_collision(self):
|
| 35 |
+
"""
|
| 36 |
+
Create an animation of the collision
|
| 37 |
+
"""
|
| 38 |
+
# Create figure and axis
|
| 39 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
|
| 40 |
+
fig.suptitle('Momentum Collision Simulation')
|
| 41 |
+
|
| 42 |
+
# Initial momentum plot
|
| 43 |
+
initial_momentum, final_momentum, v1_final, v2_final = self.calculate_momentum()
|
| 44 |
+
|
| 45 |
+
# Before collision plot
|
| 46 |
+
ax1.set_title('Before Collision')
|
| 47 |
+
ax1.set_xlim(-10, 10)
|
| 48 |
+
ax1.set_ylim(0, max(self.mass1, self.mass2) + 1)
|
| 49 |
+
rect1 = plt.Rectangle((-5, 1), 1, self.mass1,
|
| 50 |
+
fc='blue', label=f'Mass 1: {self.mass1} kg')
|
| 51 |
+
rect2 = plt.Rectangle((5 - 1, 1), 1, self.mass2,
|
| 52 |
+
fc='red', label=f'Mass 2: {self.mass2} kg')
|
| 53 |
+
|
| 54 |
+
ax1.add_patch(rect1)
|
| 55 |
+
ax1.add_patch(rect2)
|
| 56 |
+
|
| 57 |
+
# Velocity annotations
|
| 58 |
+
ax1.annotate(f'v1: {self.velocity1} m/s', xy=(-4.5, 0.5),
|
| 59 |
+
xytext=(-4.5, 0.5), color='blue')
|
| 60 |
+
ax1.annotate(f'v2: {self.velocity2} m/s', xy=(5.5, 0.5),
|
| 61 |
+
xytext=(5.5, 0.5), color='red')
|
| 62 |
+
|
| 63 |
+
ax1.legend()
|
| 64 |
+
ax1.set_xlabel('Position (m)')
|
| 65 |
+
ax1.set_ylabel('Mass')
|
| 66 |
+
|
| 67 |
+
# After collision plot
|
| 68 |
+
ax2.set_title('After Collision')
|
| 69 |
+
ax2.set_xlim(-10, 10)
|
| 70 |
+
ax2.set_ylim(0, max(self.mass1, self.mass2) + 1)
|
| 71 |
+
rect1_final = plt.Rectangle((-5, 1), 1, self.mass1,
|
| 72 |
+
fc='blue', label=f'Mass 1: {self.mass1} kg')
|
| 73 |
+
rect2_final = plt.Rectangle((5 - 1, 1), 1, self.mass2,
|
| 74 |
+
fc='red', label=f'Mass 2: {self.mass2} kg')
|
| 75 |
+
|
| 76 |
+
ax2.add_patch(rect1_final)
|
| 77 |
+
ax2.add_patch(rect2_final)
|
| 78 |
+
|
| 79 |
+
# Final velocity annotations
|
| 80 |
+
ax2.annotate(f'v1 final: {v1_final:.2f} m/s', xy=(-4.5, 0.5),
|
| 81 |
+
xytext=(-4.5, 0.5), color='blue')
|
| 82 |
+
ax2.annotate(f'v2 final: {v2_final:.2f} m/s', xy=(5.5, 0.5),
|
| 83 |
+
xytext=(5.5, 0.5), color='red')
|
| 84 |
+
|
| 85 |
+
ax2.legend()
|
| 86 |
+
ax2.set_xlabel('Position (m)')
|
| 87 |
+
ax2.set_ylabel('Mass')
|
| 88 |
+
|
| 89 |
+
return fig
|
| 90 |
+
|
| 91 |
+
def main():
|
| 92 |
+
st.title('Momentum Collision Simulator')
|
| 93 |
+
|
| 94 |
+
# Sidebar for input
|
| 95 |
+
st.sidebar.header('Collision Parameters')
|
| 96 |
+
|
| 97 |
+
# Mass inputs
|
| 98 |
+
mass1 = st.sidebar.number_input('Mass of Object 1 (kg)',
|
| 99 |
+
min_value=0.1,
|
| 100 |
+
max_value=100.0,
|
| 101 |
+
value=10.0)
|
| 102 |
+
mass2 = st.sidebar.number_input('Mass of Object 2 (kg)',
|
| 103 |
+
min_value=0.1,
|
| 104 |
+
max_value=100.0,
|
| 105 |
+
value=5.0)
|
| 106 |
+
|
| 107 |
+
# Velocity inputs
|
| 108 |
+
velocity1 = st.sidebar.number_input('Initial Velocity of Object 1 (m/s)',
|
| 109 |
+
min_value=-50.0,
|
| 110 |
+
max_value=50.0,
|
| 111 |
+
value=2.0)
|
| 112 |
+
velocity2 = st.sidebar.number_input('Initial Velocity of Object 2 (m/s)',
|
| 113 |
+
min_value=-50.0,
|
| 114 |
+
max_value=50.0,
|
| 115 |
+
value=-1.0)
|
| 116 |
+
|
| 117 |
+
# Create simulation
|
| 118 |
+
simulation = MomentumSimulation(mass1, velocity1, mass2, velocity2)
|
| 119 |
+
|
| 120 |
+
# Calculate momentum
|
| 121 |
+
initial_momentum, final_momentum, v1_final, v2_final = simulation.calculate_momentum()
|
| 122 |
+
|
| 123 |
+
# Display momentum calculations
|
| 124 |
+
st.subheader('Momentum Analysis')
|
| 125 |
+
col1, col2 = st.columns(2)
|
| 126 |
+
|
| 127 |
+
with col1:
|
| 128 |
+
st.metric('Initial Momentum', f'{initial_momentum:.2f} kg·m/s')
|
| 129 |
+
|
| 130 |
+
with col2:
|
| 131 |
+
st.metric('Final Momentum', f'{final_momentum:.2f} kg·m/s')
|
| 132 |
+
|
| 133 |
+
# Visualization
|
| 134 |
+
st.subheader('Collision Visualization')
|
| 135 |
+
fig = simulation.visualize_collision()
|
| 136 |
+
st.pyplot(fig)
|
| 137 |
+
|
| 138 |
+
# Additional explanation
|
| 139 |
+
st.markdown("""
|
| 140 |
+
### Understanding Momentum
|
| 141 |
+
|
| 142 |
+
**Momentum** is the product of an object's mass and velocity (p = mv).
|
| 143 |
+
In this simulation:
|
| 144 |
+
- The total momentum before and after the collision remains constant (Conservation of Momentum)
|
| 145 |
+
- The objects exchange velocities based on their masses
|
| 146 |
+
- Positive velocity indicates movement to the right
|
| 147 |
+
- Negative velocity indicates movement to the left
|
| 148 |
+
""")
|
| 149 |
+
|
| 150 |
+
if __name__ == '__main__':
|
| 151 |
+
main()
|