diff --git a/pages/ac-vs-dc.py b/pages/ac-vs-dc.py new file mode 100644 index 0000000000000000000000000000000000000000..ee7e2f293ac8b53c1923dc573267e0684dc74ca1 --- /dev/null +++ b/pages/ac-vs-dc.py @@ -0,0 +1,369 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation +import io +from PIL import Image + +# Set page config +st.set_page_config(page_title="AC vs DC Electricity Visualization", layout="wide") + +# Title and description +st.title("AC vs DC Electricity Visualization") +st.write(""" +This simulation demonstrates the key differences between Alternating Current (AC) and Direct Current (DC). +- **AC**: Current periodically changes direction, voltage oscillates between positive and negative values +- **DC**: Current flows in a single direction, voltage remains constant +""") + +# Sidebar controls +st.sidebar.header("Simulation Controls") + +# AC parameters +st.sidebar.subheader("AC Parameters") +ac_amplitude = st.sidebar.slider("AC Amplitude (Volts)", 1.0, 10.0, 5.0, 0.1) +ac_frequency = st.sidebar.slider("AC Frequency (Hz)", 0.1, 2.0, 1.0, 0.1) + +# DC parameters +st.sidebar.subheader("DC Parameters") +dc_voltage = st.sidebar.slider("DC Voltage (Volts)", 0.0, 10.0, 5.0, 0.1) + +# Animation speed +animation_speed = st.sidebar.slider("Animation Speed", 0.1, 2.0, 1.0, 0.1) + +# Time parameters +duration = 5 # seconds to simulate +fps = 30 # frames per second + +# Function to create the simulation animation +def create_animation(ac_amplitude, ac_frequency, dc_voltage, duration, fps, animation_speed): + # Create figure and axes + fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12)) + plt.tight_layout(pad=4.0) + + # Time array + t = np.linspace(0, duration, int(duration * fps)) + dt = t[1] - t[0] + + # Calculate the AC and DC signals + ac_signal = ac_amplitude * np.sin(2 * np.pi * ac_frequency * t) + dc_signal = np.ones_like(t) * dc_voltage + + # Set up plots + # Voltage plots + ax1.set_xlim(0, duration) + ax1.set_ylim(-max(ac_amplitude, dc_voltage) * 1.2, max(ac_amplitude, dc_voltage) * 1.2) + ax1.set_ylabel('Voltage (V)') + ax1.set_title('Voltage vs Time') + ax1.grid(True) + + ac_line, = ax1.plot([], [], 'r-', label='AC') + dc_line, = ax1.plot([], [], 'b-', label='DC') + ax1.legend(loc='upper right') + + # AC electron flow + ax2.set_xlim(-1, 1) + ax2.set_ylim(-1, 1) + ax2.set_aspect('equal') + ax2.set_title('AC Electron Flow') + ax2.grid(True) + ax2.set_xticks([]) + ax2.set_yticks([]) + + # Draw a wire for AC + wire_length = 1.8 + wire_thickness = 0.1 + wire_rect = plt.Rectangle((-wire_length/2, -wire_thickness/2), wire_length, wire_thickness, + fc='gray', ec='black') + ax2.add_patch(wire_rect) + + # Electrons for AC + num_electrons = 20 + ac_electrons = [] + for i in range(num_electrons): + electron = plt.Circle((0, 0), 0.03, fc='blue', ec='black') + ax2.add_patch(electron) + ac_electrons.append(electron) + + # DC electron flow + ax3.set_xlim(-1, 1) + ax3.set_ylim(-1, 1) + ax3.set_aspect('equal') + ax3.set_title('DC Electron Flow') + ax3.grid(True) + ax3.set_xticks([]) + ax3.set_yticks([]) + + # Draw a wire for DC + wire_rect = plt.Rectangle((-wire_length/2, -wire_thickness/2), wire_length, wire_thickness, + fc='gray', ec='black') + ax3.add_patch(wire_rect) + + # Electrons for DC + dc_electrons = [] + for i in range(num_electrons): + electron = plt.Circle((0, 0), 0.03, fc='blue', ec='black') + ax3.add_patch(electron) + dc_electrons.append(electron) + + # Initialization function for animation + def init(): + ac_line.set_data([], []) + dc_line.set_data([], []) + + # Initialize AC electrons positions + for i, electron in enumerate(ac_electrons): + pos = -wire_length/2 + i * wire_length / (num_electrons - 1) + electron.center = (pos, 0) + + # Initialize DC electrons positions + for i, electron in enumerate(dc_electrons): + pos = -wire_length/2 + i * wire_length / (num_electrons - 1) + electron.center = (pos, 0) + + return ac_line, dc_line, *ac_electrons, *dc_electrons + + # Animation function + def animate(frame): + frame = int(frame * animation_speed) % len(t) + current_time = t[:frame+1] + + # Update voltage plots + ac_line.set_data(current_time, ac_signal[:frame+1]) + dc_line.set_data(current_time, dc_signal[:frame+1]) + + # Update AC electron positions + ac_current_value = ac_signal[frame] + ac_speed = ac_current_value / ac_amplitude # Normalized speed factor + + for i, electron in enumerate(ac_electrons): + # Base position + base_pos = -wire_length/2 + i * wire_length / (num_electrons - 1) + # Add oscillation based on AC signal + oscillation = 0.05 * np.sin(2 * np.pi * ac_frequency * t[frame] - i * np.pi / 10) + electron.center = (base_pos + oscillation, 0) + + # Change electron color based on direction + if ac_current_value > 0: + electron.set_facecolor('blue') + else: + electron.set_facecolor('red') + + # Update DC electron positions + for i, electron in enumerate(dc_electrons): + # Base position plus movement based on time + pos = (-wire_length/2 + (i + frame/20 * animation_speed) % num_electrons * wire_length / num_electrons) + if pos > wire_length/2: + pos = -wire_length/2 + electron.center = (pos, 0) + + return ac_line, dc_line, *ac_electrons, *dc_electrons + + # Create animation + anim = FuncAnimation(fig, animate, frames=len(t), init_func=init, blit=True) + + plt.tight_layout() + return anim, fig + +# Create separate plots for static visualization instead of animation +def create_static_plots(): + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # Time array + t = np.linspace(0, duration, 1000) + + # Calculate the AC and DC signals + ac_signal = ac_amplitude * np.sin(2 * np.pi * ac_frequency * t) + dc_signal = np.ones_like(t) * dc_voltage + + # AC plot + ax1.plot(t, ac_signal, 'r-', label=f'AC ({ac_frequency} Hz, {ac_amplitude}V)') + ax1.set_ylabel('Voltage (V)') + ax1.set_title('Alternating Current (AC)') + ax1.grid(True) + ax1.legend() + + # DC plot + ax2.plot(t, dc_signal, 'b-', label=f'DC ({dc_voltage}V)') + ax2.set_xlabel('Time (s)') + ax2.set_ylabel('Voltage (V)') + ax2.set_title('Direct Current (DC)') + ax2.grid(True) + ax2.legend() + + plt.tight_layout() + return fig + +# Display static plots +st.header("Voltage Over Time") +static_fig = create_static_plots() +st.pyplot(static_fig) + +# Visualize electron flow differences +st.header("Electron Flow Visualization") +st.write(""" +The visualization below shows how electrons move in AC and DC circuits: +- In the AC circuit (top), electrons oscillate back and forth, changing direction periodically +- In the DC circuit (bottom), electrons flow continuously in one direction +""") + +# Instead of animation, create a visual representation with multiple frames +st.write("**Animation visualization replaced with static representation**") + +# Create frames to show electron movement +def create_electron_flow_images(): + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6)) + + # Set up axes + for ax, title in zip([ax1, ax2], ['AC Electron Flow', 'DC Electron Flow']): + ax.set_xlim(-1, 1) + ax.set_ylim(-1, 1) + ax.set_aspect('equal') + ax.set_title(title) + ax.grid(True) + ax.set_xticks([]) + ax.set_yticks([]) + + # Draw wires + wire_length = 1.8 + wire_thickness = 0.1 + wire_rect = plt.Rectangle((-wire_length/2, -wire_thickness/2), wire_length, wire_thickness, + fc='gray', ec='black') + ax.add_patch(wire_rect) + + plt.tight_layout() + return fig + +# Show different phases of electron movement +phases = ['Phase 1: Initial', 'Phase 2: Movement', 'Phase 3: Direction Change (AC only)'] + +for i, phase in enumerate(phases): + st.subheader(phase) + + fig = create_electron_flow_images() + num_electrons = 10 + + # Get the axes from the figure + ax1, ax2 = fig.axes + + # Draw electrons for AC + for j in range(num_electrons): + # Base position + base_pos = -0.9 + j * 1.8 / (num_electrons - 1) + + # Different positions for different phases + if i == 0: # Initial phase + pos = base_pos + color = 'blue' + elif i == 1: # Movement phase + pos = base_pos + 0.1 # Slightly moved to the right + color = 'blue' + else: # Direction change (for AC) + pos = base_pos - 0.1 # Now moved to the left + color = 'red' # Changed color to indicate reversed direction + + electron = plt.Circle((pos, 0), 0.03, fc=color, ec='black') + ax1.add_patch(electron) + + # Draw electrons for DC (always moving in one direction) + for j in range(num_electrons): + base_pos = -0.9 + j * 1.8 / (num_electrons - 1) + + # Different positions for different phases, but always moving right + if i == 0: # Initial phase + pos = base_pos + elif i == 1: # Movement phase + pos = base_pos + 0.1 # Moved to the right + else: # Continued movement + pos = base_pos + 0.2 # Moved further right + + # If electron moves beyond wire, wrap around + if pos > 0.9: + pos = -0.9 + (pos - 0.9) + + electron = plt.Circle((pos, 0), 0.03, fc='blue', ec='black') + ax2.add_patch(electron) + + st.pyplot(fig) + + # Explain each phase + if i == 0: + st.write("Initial state: Electrons are evenly distributed along both wires.") + elif i == 1: + st.write("Both AC and DC electrons begin moving to the right as current flows.") + else: + 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).") + +# Explanations +st.header("Key Differences Between AC and DC") + +col1, col2 = st.columns(2) + +with col1: + st.subheader("Alternating Current (AC)") + st.write(""" + - **Direction**: Periodically changes direction + - **Waveform**: Typically sinusoidal + - **Voltage**: Oscillates between positive and negative values + - **Generation**: Produced by alternators and generators + - **Transmission**: Can be efficiently transmitted over long distances + - **Usage**: Power grid, household appliances, transformers + """) + +with col2: + st.subheader("Direct Current (DC)") + st.write(""" + - **Direction**: Flows in one direction only + - **Waveform**: Constant (or with minimal variation) + - **Voltage**: Maintains consistent polarity + - **Generation**: Produced by batteries, solar cells, and power supplies + - **Transmission**: Loses power over long distances + - **Usage**: Electronics, batteries, LED lighting, computers + """) + +st.header("Real-World Applications") +st.write(""" +- **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. +- **DC** is used in electronic devices, mobile phones, computers, and electric vehicles because these devices require a stable, consistent current. +- 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! +""") + +# Advantages and disadvantages +st.header("Advantages and Disadvantages") + +adv_col1, adv_col2 = st.columns(2) + +with adv_col1: + st.subheader("AC Advantages") + st.write(""" + - Easy to transform voltage levels + - Less energy loss in transmission + - Easy to generate with rotating machines + - Self-extinguishing arcs (helpful for circuit breakers) + """) + + st.subheader("AC Disadvantages") + st.write(""" + - More complex circuits needed + - Skin effect in conductors + - Inductive and capacitive losses + - Not suitable for sensitive electronics without conversion + """) + +with adv_col2: + st.subheader("DC Advantages") + st.write(""" + - Simple to understand and use + - Steady power delivery + - No frequency considerations + - Better for electronic components + - Less dangerous at equal voltages + """) + + st.subheader("DC Disadvantages") + st.write(""" + - Difficult to change voltage levels + - Higher transmission losses over distance + - Cannot use transformers directly + - Difficult to interrupt (arc issues) + """) \ No newline at end of file diff --git a/pages/air-resistance.py b/pages/air-resistance.py new file mode 100644 index 0000000000000000000000000000000000000000..791acf465ea0fb30eb0c9c83fd944c43f0da4a02 --- /dev/null +++ b/pages/air-resistance.py @@ -0,0 +1,159 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +class FallingObject: + def __init__(self, mass, drag_coefficient, cross_sectional_area, + initial_height, initial_velocity=0, air_density=1.225): + """ + Simulate a falling object with air resistance + + Parameters: + - mass: mass of the object (kg) + - drag_coefficient: coefficient of drag + - cross_sectional_area: frontal area of the object (m²) + - initial_height: starting height of the object (m) + - initial_velocity: initial velocity (m/s), default is 0 + - air_density: density of air (kg/m³), default is sea level air density + """ + self.mass = mass + self.drag_coefficient = drag_coefficient + self.cross_sectional_area = cross_sectional_area + self.height = initial_height + self.velocity = initial_velocity + self.air_density = air_density + self.gravity = 9.81 # m/s² + + def calculate_trajectory(self, time_step=0.01, max_time=10): + """ + Calculate the trajectory of the falling object + + Returns: + - times: list of time points + - heights: list of corresponding heights + - velocities: list of corresponding velocities + """ + times = [0] + heights = [self.height] + velocities = [self.velocity] + + current_time = 0 + current_height = self.height + current_velocity = self.velocity + + while current_height > 0 and current_time < max_time: + # Calculate drag force + drag_force = 0.5 * self.drag_coefficient * self.air_density * \ + self.cross_sectional_area * abs(current_velocity)**2 + + # Determine drag direction (opposite to velocity) + drag_direction = 1 if current_velocity > 0 else -1 + + # Calculate net force + net_force = self.mass * self.gravity - drag_direction * drag_force + + # Calculate acceleration + acceleration = net_force / self.mass + + # Update velocity + current_velocity += acceleration * time_step + + # Update height (now decreasing) + current_height -= current_velocity * time_step + + # Update time + current_time += time_step + + # Store results + times.append(current_time) + heights.append(max(0, current_height)) + velocities.append(current_velocity) + + return times, heights, velocities + +def plot_trajectory(times, heights, velocities): + """ + Create a matplotlib figure with two subplots + """ + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # Height vs Time plot + ax1.plot(times, heights, label='Height') + ax1.set_xlabel('Time (s)') + ax1.set_ylabel('Height (m)') + ax1.set_title('Object Height over Time') + ax1.invert_yaxis() # Invert y-axis to show falling + ax1.legend() + ax1.grid(True) + + # Velocity vs Time plot + ax2.plot(times, velocities, label='Velocity', color='r') + ax2.set_xlabel('Time (s)') + ax2.set_ylabel('Velocity (m/s)') + ax2.set_title('Velocity over Time') + ax2.legend() + ax2.grid(True) + + return fig + +def main(): + st.title('Air Resistance Simulation') + + # Sidebar for input parameters + st.sidebar.header('Object Properties') + + # Input sliders + mass = st.sidebar.slider('Mass (kg)', min_value=0.1, max_value=100.0, value=1.0, step=0.1) + initial_height = st.sidebar.slider('Initial Height (m)', min_value=1, max_value=500, value=100) + drag_coefficient = st.sidebar.slider('Drag Coefficient', min_value=0.1, max_value=2.0, value=0.47, step=0.01) + cross_sectional_area = st.sidebar.slider('Cross-Sectional Area (m²)', + min_value=0.01, max_value=10.0, value=0.1, step=0.01) + + # Create falling object + falling_object = FallingObject( + mass=mass, + drag_coefficient=drag_coefficient, + cross_sectional_area=cross_sectional_area, + initial_height=initial_height + ) + + # Calculate trajectory + times, heights, velocities = falling_object.calculate_trajectory() + + # Plot results + fig = plot_trajectory(times, heights, velocities) + st.pyplot(fig) + + # Display some key metrics + st.subheader('Simulation Results') + col1, col2, col3 = st.columns(3) + + with col1: + st.metric('Fall Time', f'{times[-1]:.2f} s') + + with col2: + st.metric('Max Velocity', f'{max(abs(v) for v in velocities):.2f} m/s') + + with col3: + terminal_velocity = np.sqrt((2 * mass * 9.81) / (drag_coefficient * 1.225 * cross_sectional_area)) + st.metric('Terminal Velocity', f'{terminal_velocity:.2f} m/s') + + # Explanation + st.markdown(""" + ### How Air Resistance Works + + This simulation demonstrates how air resistance affects a falling object: + + - **Drag Force**: Opposes the motion of the object through the air + - **Factors Affecting Drag**: + * Object's mass + * Drag coefficient + * Cross-sectional area + * Air density + + As the object falls, air resistance increases with velocity, eventually + leading to terminal velocity where gravitational force equals air resistance. + """) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pages/angular-momentum.py b/pages/angular-momentum.py new file mode 100644 index 0000000000000000000000000000000000000000..8851fa307d612f7f1005b5d634ed82c0f509662d --- /dev/null +++ b/pages/angular-momentum.py @@ -0,0 +1,114 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Title of the app +st.title("Angular Momentum Visualization") + +# Let the user choose the type of motion +motion_type = st.selectbox("Select Motion Type", ["Linear Motion", "Circular Motion"]) + +# Scenario 1: Linear Motion +if motion_type == "Linear Motion": + st.subheader("Linear Motion") + st.write("A particle moves in a straight line. Adjust the initial position and velocity to see how Angular Momentum behaves.") + + # Input sliders for initial position and velocity + x0 = st.slider("Initial x-position (x₀)", -5.0, 5.0, 0.0) + y0 = st.slider("Initial y-position (y₀)", -5.0, 5.0, 0.0) + v_x = st.slider("Velocity x-component (vₓ)", -2.0, 2.0, 1.0) + v_y = st.slider("Velocity y-component (vᵧ)", -2.0, 2.0, 0.0) + t = st.slider("Time (t)", 0.0, 10.0, 0.0) + m = 1.0 # Mass set to 1 for simplicity + + # Calculate position at time t + x = x0 + v_x * t + y = y0 + v_y * t + + # Calculate Angular Momentum (z-component) + L_z = m * (x * v_y - y * v_x) + + # Create the plot + fig, ax = plt.subplots() + ax.set_aspect('equal') # Equal scaling for x and y axes + ax.set_xlim(-10, 10) + ax.set_ylim(-10, 10) + ax.grid(True) + + # Plot the origin + ax.plot(0, 0, 'ko', label='Origin') + + # Plot the particle's path (straight line) + if v_x != 0 or v_y != 0: + # Define the line extending beyond plot limits + s = 100 + x1 = x0 + v_x * (-s) + y1 = y0 + v_y * (-s) + x2 = x0 + v_x * s + y2 = y0 + v_y * s + ax.plot([x1, x2], [y1, y2], 'g--', label='Path') + + # Plot position vector (blue) + ax.arrow(0, 0, x, y, head_width=0.5, head_length=0.5, fc='blue', ec='blue', label='Position Vector') + # Plot velocity vector (red) + ax.arrow(x, y, v_x, v_y, head_width=0.5, head_length=0.5, fc='red', ec='red', label='Velocity Vector') + # Plot particle position + ax.plot(x, y, 'ro', label='Particle') + + # Display Angular Momentum + ax.text(-9, 9, f'L_z = {L_z:.2f}', fontsize=12) + + # Add legend + ax.legend(loc='upper right') + st.pyplot(fig) + +# Scenario 2: Circular Motion +elif motion_type == "Circular Motion": + st.subheader("Circular Motion") + st.write("A particle moves in a circle around the origin. Adjust the radius and angular velocity to observe Angular Momentum.") + + # Input sliders for radius and angular velocity + r = st.slider("Radius (r)", 0.5, 5.0, 2.0) + omega = st.slider("Angular Velocity (ω)", 0.1, 2.0, 1.0) + t = st.slider("Time (t)", 0.0, 10.0, 0.0) + m = 1.0 # Mass set to 1 + + # Calculate position and velocity using circular motion equations + theta = omega * t + x = r * np.cos(theta) + y = r * np.sin(theta) + v_x = -r * omega * np.sin(theta) + v_y = r * omega * np.cos(theta) + + # Calculate Angular Momentum (z-component) + L_z = m * (x * v_y - y * v_x) + + # Create the plot + fig, ax = plt.subplots() + ax.set_aspect('equal') + ax.set_xlim(-10, 10) + ax.set_ylim(-10, 10) + ax.grid(True) + + # Plot the origin + ax.plot(0, 0, 'ko', label='Origin') + + # Plot the circular path + theta_vals = np.linspace(0, 2 * np.pi, 100) + x_path = r * np.cos(theta_vals) + y_path = r * np.sin(theta_vals) + ax.plot(x_path, y_path, 'g--', label='Path') + + # Plot position vector (blue) + ax.arrow(0, 0, x, y, head_width=0.5, head_length=0.5, fc='blue', ec='blue', label='Position Vector') + # Plot velocity vector (red) + ax.arrow(x, y, v_x, v_y, head_width=0.5, head_length=0.5, fc='red', ec='red', label='Velocity Vector') + # Plot particle position + ax.plot(x, y, 'ro', label='Particle') + + # Display Angular Momentum + ax.text(-9, 9, f'L_z = {L_z:.2f}', fontsize=12) + + # Add legend + ax.legend(loc='upper right') + st.pyplot(fig) \ No newline at end of file diff --git a/pages/atomic-structure.py b/pages/atomic-structure.py new file mode 100644 index 0000000000000000000000000000000000000000..47c6a34cc3d38cd2094d97a10b3b8de4ff100a1c --- /dev/null +++ b/pages/atomic-structure.py @@ -0,0 +1,148 @@ +import streamlit as st +import plotly.graph_objects as go +import numpy as np + +# Dictionary of elements (atomic number: (name, symbol)) +elements = { + 1: ("Hydrogen", "H"), + 2: ("Helium", "He"), + 3: ("Lithium", "Li"), + 4: ("Beryllium", "Be"), + 5: ("Boron", "B"), + 6: ("Carbon", "C"), + 7: ("Nitrogen", "N"), + 8: ("Oxygen", "O"), + 9: ("Fluorine", "F"), + 10: ("Neon", "Ne"), + # Add more elements as needed +} + +# Function to calculate electron shells +def get_electron_shells(electrons): + shells = [] + n = 1 + while electrons > 0: + max_electrons = 2 * n**2 + if electrons >= max_electrons: + shells.append(max_electrons) + electrons -= max_electrons + else: + shells.append(electrons) + electrons = 0 + n += 1 + return shells + +# Streamlit app +st.title("Atomic Structure Visualizer") + +# User inputs +col1, col2, col3 = st.columns(3) +with col1: + Z = st.number_input("Number of protons (Z)", min_value=1, max_value=118, value=6) +with col2: + N = st.number_input("Number of neutrons (N)", min_value=0, value=6) +with col3: + E = st.number_input("Number of electrons (E)", min_value=0, value=Z) + +# Calculate atomic properties +A = Z + N # Mass number +charge = Z - E # Charge of the ion +name, symbol = elements.get(Z, ("Unknown", "?")) + +# Display atomic information +st.subheader("Atomic Information") +st.write(f"**Element**: {name} ({symbol})") +st.write(f"**Atomic Number**: {Z}") +st.write(f"**Mass Number**: {A}") +st.write(f"**Number of Electrons**: {E}") +st.write(f"**Charge**: {'+' if charge > 0 else ''}{charge}" if charge != 0 else "Neutral atom") + +# Calculate electron shells +shells = get_electron_shells(E) + +# Nucleus parameters +nucleus_radius = 0.1 + +# Generate proton positions +proton_theta = np.random.uniform(0, 2 * np.pi, Z) +proton_r = np.random.uniform(0, nucleus_radius, Z) +proton_x = proton_r * np.cos(proton_theta) +proton_y = proton_r * np.sin(proton_theta) + +# Generate neutron positions +neutron_theta = np.random.uniform(0, 2 * np.pi, N) +neutron_r = np.random.uniform(0, nucleus_radius, N) +neutron_x = neutron_r * np.cos(neutron_theta) +neutron_y = neutron_r * np.sin(neutron_theta) + +# Generate electron positions +electron_x = [] +electron_y = [] +r_shell = 1.0 # Radius increment per shell +for n, k in enumerate(shells, start=1): + r = n * r_shell + angles = np.linspace(0, 2 * np.pi, k, endpoint=False) + for angle in angles: + electron_x.append(r * np.cos(angle)) + electron_y.append(r * np.sin(angle)) + +# Create Plotly figure +fig = go.Figure() + +# Add protons +fig.add_trace(go.Scatter( + x=proton_x, y=proton_y, + mode='markers', + name='Protons', + marker=dict(color='red', size=8) +)) + +# Add neutrons +fig.add_trace(go.Scatter( + x=neutron_x, y=neutron_y, + mode='markers', + name='Neutrons', + marker=dict(color='blue', size=8) +)) + +# Add electrons +fig.add_trace(go.Scatter( + x=electron_x, y=electron_y, + mode='markers', + name='Electrons', + marker=dict(color='green', size=10) +)) + +# Add shell circles +for n in range(1, len(shells) + 1): + fig.add_shape( + type="circle", + x0=-n * r_shell, y0=-n * r_shell, + x1=n * r_shell, y1=n * r_shell, + line=dict(color="gray", width=1, dash="dash") + ) + +# Set plot limits and aspect ratio +max_r = max(0.5, len(shells) * r_shell) +fig.update_xaxes(range=[-max_r, max_r]) +fig.update_yaxes(range=[-max_r, max_r], scaleanchor="x", scaleratio=1) + +# Update layout +fig.update_layout( + title="Atomic Structure", + width=600, + height=600, + showlegend=True +) + +# Display plot in Streamlit +st.plotly_chart(fig) + +# Instructions +st.markdown(""" +### How to Use +- **Protons (Z)**: Defines the element (e.g., Z=6 for Carbon). +- **Neutrons (N)**: Affects the mass number (A = Z + N). +- **Electrons (E)**: Determines if it's an ion (Charge = Z - E). +- Adjust the inputs to visualize different atoms or ions! +""") \ No newline at end of file diff --git a/pages/basic-circuit-analysis.py b/pages/basic-circuit-analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..75565a1bd1a43b158acb57e8718f0be1da92b3a3 --- /dev/null +++ b/pages/basic-circuit-analysis.py @@ -0,0 +1,388 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle, Circle, FancyArrowPatch + +st.set_page_config(page_title="Circuit Analysis Simulator", layout="wide") + +st.title("Interactive Circuit Analysis Simulator") +st.write("Explore basic circuit concepts including Ohm's Law, series and parallel circuits.") + +# Sidebar for navigation +page = st.sidebar.radio("Choose a Simulation", + ["Ohm's Law", "Series Circuit", "Parallel Circuit"]) + +if page == "Ohm's Law": + st.header("Ohm's Law: V = I × R") + st.write(""" + Ohm's Law describes the relationship between voltage (V), current (I), and resistance (R). + Adjust the sliders to see how changing one parameter affects the others. + """) + + col1, col2 = st.columns(2) + + with col1: + # Let user control two of the variables and calculate the third + calculation_mode = st.radio( + "What do you want to calculate?", + ["Voltage", "Current", "Resistance"] + ) + + if calculation_mode == "Voltage": + current = st.slider("Current (A)", 0.1, 10.0, 1.0, 0.1) + resistance = st.slider("Resistance (Ω)", 1.0, 100.0, 10.0, 1.0) + voltage = current * resistance + st.success(f"Calculated Voltage: {voltage:.2f} V") + + elif calculation_mode == "Current": + voltage = st.slider("Voltage (V)", 1.0, 220.0, 12.0, 1.0) + resistance = st.slider("Resistance (Ω)", 1.0, 100.0, 10.0, 1.0) + current = voltage / resistance if resistance != 0 else float('inf') + st.success(f"Calculated Current: {current:.2f} A") + + else: # Resistance + voltage = st.slider("Voltage (V)", 1.0, 220.0, 12.0, 1.0) + current = st.slider("Current (A)", 0.1, 10.0, 1.0, 0.1) + resistance = voltage / current if current != 0 else float('inf') + st.success(f"Calculated Resistance: {resistance:.2f} Ω") + + with col2: + # Create visualization + fig, ax = plt.subplots(figsize=(8, 6)) + + # Draw battery + battery_height = 1.5 + battery_x = 1 + battery_y = 3 + + # Long line + ax.plot([battery_x, battery_x + 6], [battery_y + battery_height, battery_y + battery_height], 'k-', lw=2) + # Bottom line + ax.plot([battery_x, battery_x + 6], [battery_y, battery_y], 'k-', lw=2) + + # Battery + ax.plot([battery_x, battery_x], [battery_y, battery_y + battery_height], 'k-', lw=2) + ax.plot([battery_x + 0.2, battery_x + 0.2], [battery_y + 0.25, battery_y + battery_height - 0.25], 'k-', lw=4) + ax.plot([battery_x + 0.5, battery_x + 0.5], [battery_y + 0.5, battery_y + battery_height - 0.5], 'k-', lw=2) + + # Resistor (zigzag) + res_x = battery_x + 3 + res_y = battery_y + battery_height + zigzag_width = 1.5 + zigzag_height = 0.5 + zigzag_pts = 7 + x_pts = np.linspace(res_x, res_x + zigzag_width, zigzag_pts) + y_pts = np.array([0, 1, 0, 1, 0, 1, 0]) * zigzag_height + res_y + ax.plot(x_pts, y_pts, 'k-', lw=2) + + # Add current direction arrow + if calculation_mode == "Current" or calculation_mode == "Resistance": + current_value = current + else: + current_value = current + + # Scale arrow size based on current + arrow_scale = min(1, current_value / 5) + arrow_width = 0.15 * (0.5 + arrow_scale) + arrow = FancyArrowPatch((battery_x + 4.5, battery_y + 0.5), + (battery_x + 3, battery_y + 0.5), + arrowstyle='->', + color='blue', + lw=2, + mutation_scale=20) + ax.add_patch(arrow) + ax.text(battery_x + 3.8, battery_y + 0.3, f"{current:.1f} A", color='blue') + + # Add voltage label + if calculation_mode == "Voltage" or calculation_mode == "Resistance": + voltage_value = voltage + else: + voltage_value = voltage + + ax.text(battery_x + 0.7, battery_y + 0.75, f"{voltage:.1f} V", color='red') + + # Add resistance label + if calculation_mode == "Resistance" or calculation_mode == "Voltage": + resistance_value = resistance + else: + resistance_value = resistance + + ax.text(res_x + 0.2, res_y + 0.75, f"{resistance:.1f} Ω", color='green') + + ax.set_xlim(0, 8) + ax.set_ylim(1, 6) + ax.axis('equal') + ax.axis('off') + + st.pyplot(fig) + + # Show the formula and calculation + st.write("### Ohm's Law Formula") + if calculation_mode == "Voltage": + st.latex(r"V = I \times R") + st.latex(f"V = {current:.2f} \, A \times {resistance:.2f} \, \Omega = {voltage:.2f} \, V") + elif calculation_mode == "Current": + st.latex(r"I = \frac{V}{R}") + st.latex(f"I = \\frac{{{voltage:.2f} \, V}}{{{resistance:.2f} \, \Omega}} = {current:.2f} \, A") + else: # Resistance + st.latex(r"R = \frac{V}{I}") + st.latex(f"R = \\frac{{{voltage:.2f} \, V}}{{{current:.2f} \, A}} = {resistance:.2f} \, \Omega") + +elif page == "Series Circuit": + st.header("Series Circuit Analysis") + st.write(""" + In a series circuit, the same current flows through each component, and the total voltage is the sum of the voltages across each component. + """) + + col1, col2 = st.columns(2) + + with col1: + # User inputs + voltage_source = st.slider("Voltage Source (V)", 1.0, 24.0, 12.0, 0.1) + + st.subheader("Resistors in Series") + num_resistors = st.slider("Number of Resistors", 1, 5, 3) + + resistances = [] + for i in range(num_resistors): + r = st.slider(f"R{i+1} (Ω)", 1.0, 100.0, 10.0 * (i+1), 1.0, key=f"series_r{i}") + resistances.append(r) + + # Calculations + total_resistance = sum(resistances) + current = voltage_source / total_resistance if total_resistance > 0 else 0 + voltage_drops = [current * r for r in resistances] + power_dissipations = [current**2 * r for r in resistances] + + st.subheader("Circuit Analysis Results") + st.write(f"Total Resistance: {total_resistance:.2f} Ω") + st.write(f"Current: {current:.2f} A") + + # Display voltage drops and power for each resistor + for i in range(num_resistors): + st.write(f"R{i+1}: Voltage Drop = {voltage_drops[i]:.2f} V, Power = {power_dissipations[i]:.2f} W") + + with col2: + # Create visualization of series circuit + fig, ax = plt.subplots(figsize=(8, 6)) + + # Draw circuit + start_x, start_y = 1, 5 + + # Draw battery + battery_height = 1.5 + battery_x = start_x + battery_y = start_y - battery_height/2 + + # Battery + ax.plot([battery_x, battery_x], [battery_y, battery_y + battery_height], 'k-', lw=2) + ax.plot([battery_x + 0.2, battery_x + 0.2], [battery_y + 0.25, battery_y + battery_height - 0.25], 'k-', lw=4) + ax.plot([battery_x + 0.5, battery_x + 0.5], [battery_y + 0.5, battery_y + battery_height - 0.5], 'k-', lw=2) + + # Label voltage source + ax.text(battery_x + 0.7, battery_y + 0.75, f"{voltage_source:.1f} V", color='red') + + # Draw wires and resistors in series + line_length = 10 / (num_resistors + 1) + + # Top line from battery to first resistor + 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) + + # Draw resistors in series + for i in range(num_resistors): + res_x = battery_x + line_length * (i + 1) + res_y = battery_y + battery_height - 0.5 + + # Draw resistor (zigzag) + zigzag_width = 1.0 + zigzag_height = 0.5 + zigzag_pts = 7 + x_pts = np.linspace(res_x, res_x + zigzag_width, zigzag_pts) + y_pts = np.array([0, 1, 0, 1, 0, 1, 0]) * zigzag_height + res_y + ax.plot(x_pts, y_pts, 'k-', lw=2) + + # Label resistor + 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) + + # Connect to next resistor if not the last one + if i < num_resistors - 1: + ax.plot([res_x + zigzag_width, res_x + line_length], [res_y, res_y], 'k-', lw=2) + + # Bottom line back to battery + ax.plot([battery_x + line_length * (num_resistors + 1) - line_length + zigzag_width, battery_x + 0.5], + [res_y, battery_y + 0.5], 'k-', lw=2) + + # Add current direction arrow + arrow = FancyArrowPatch((battery_x + 0.5, battery_y + 0.5), + (battery_x + line_length/2, battery_y + 0.5), + arrowstyle='->', + color='blue', + lw=2, + mutation_scale=15) + ax.add_patch(arrow) + ax.text(battery_x + line_length/4, battery_y + 0.2, f"{current:.1f}A", color='blue', fontsize=9) + + ax.set_xlim(0, 12) + ax.set_ylim(2, 8) + ax.axis('equal') + ax.axis('off') + + st.pyplot(fig) + + # Show formulas + st.write("### Series Circuit Formulas") + st.latex(r"R_{total} = R_1 + R_2 + ... + R_n") + st.latex(f"R_{{total}} = {' + '.join([f'{r:.1f}' for r in resistances])} = {total_resistance:.2f} \, \Omega") + st.latex(r"I = \frac{V_{source}}{R_{total}}") + st.latex(f"I = \\frac{{{voltage_source:.2f}}}{{{total_resistance:.2f}}} = {current:.2f} \, A") + st.latex(r"V_n = I \times R_n") + +elif page == "Parallel Circuit": + st.header("Parallel Circuit Analysis") + st.write(""" + 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. + """) + + col1, col2 = st.columns(2) + + with col1: + # User inputs + voltage_source = st.slider("Voltage Source (V)", 1.0, 24.0, 12.0, 0.1) + + st.subheader("Resistors in Parallel") + num_resistors = st.slider("Number of Resistors", 1, 5, 3) + + resistances = [] + for i in range(num_resistors): + r = st.slider(f"R{i+1} (Ω)", 1.0, 100.0, float(10.0 * (i+1)), 1.0, key=f"parallel_r{i}") + resistances.append(r) + + # Calculations + inverse_resistance_sum = sum(1/r for r in resistances) if resistances else 0 + total_resistance = 1/inverse_resistance_sum if inverse_resistance_sum > 0 else float('inf') + + branch_currents = [voltage_source / r for r in resistances] + total_current = sum(branch_currents) + power_dissipations = [(voltage_source ** 2) / r for r in resistances] + total_power = voltage_source * total_current + + st.subheader("Circuit Analysis Results") + st.write(f"Total Resistance: {total_resistance:.2f} Ω") + st.write(f"Total Current: {total_current:.2f} A") + + # Display current and power for each resistor + for i in range(num_resistors): + st.write(f"R{i+1}: Current = {branch_currents[i]:.2f} A, Power = {power_dissipations[i]:.2f} W") + + with col2: + # Create visualization of parallel circuit + fig, ax = plt.subplots(figsize=(8, 6)) + + # Draw circuit + start_x, start_y = 1, 5 + end_x = 9 + + # Draw battery + battery_height = 1.5 + battery_x = start_x + battery_y = start_y - battery_height/2 + + # Battery + ax.plot([battery_x, battery_x], [battery_y, battery_y + battery_height], 'k-', lw=2) + ax.plot([battery_x + 0.2, battery_x + 0.2], [battery_y + 0.25, battery_y + battery_height - 0.25], 'k-', lw=4) + ax.plot([battery_x + 0.5, battery_x + 0.5], [battery_y + 0.5, battery_y + battery_height - 0.5], 'k-', lw=2) + + # Label voltage source + ax.text(battery_x + 0.7, battery_y + 0.75, f"{voltage_source:.1f} V", color='red') + + # Top horizontal line + ax.plot([battery_x + 0.5, end_x], [battery_y + battery_height - 0.5, battery_y + battery_height - 0.5], 'k-', lw=2) + + # Bottom horizontal line + ax.plot([battery_x + 0.5, end_x], [battery_y + 0.5, battery_y + 0.5], 'k-', lw=2) + + # Add main current arrow + arrow = FancyArrowPatch((battery_x + 1.5, battery_y + battery_height - 0.5), + (battery_x + 2.5, battery_y + battery_height - 0.5), + arrowstyle='->', + color='blue', + lw=2, + mutation_scale=15) + ax.add_patch(arrow) + ax.text(battery_x + 1.5, battery_y + battery_height - 0.8, f"{total_current:.1f}A", color='blue') + + # Draw resistors in parallel + spacing = 5.0 / (num_resistors + 1) + for i in range(num_resistors): + res_x = battery_x + 2.5 + i * spacing + res_y_top = battery_y + battery_height - 0.5 + res_y_bottom = battery_y + 0.5 + + # Vertical lines to resistor + ax.plot([res_x, res_x], [res_y_top, res_y_top - 1], 'k-', lw=2) + ax.plot([res_x, res_x], [res_y_bottom, res_y_bottom + 1], 'k-', lw=2) + + # Draw resistor (zigzag horizontal) + zigzag_height = 1.0 + zigzag_width = 0.5 + zigzag_pts = 7 + y_pts = np.linspace(res_y_top - 1, res_y_bottom + 1, zigzag_pts) + x_pts = np.array([0, 1, 0, 1, 0, 1, 0]) * zigzag_width + res_x + ax.plot(x_pts, y_pts, 'k-', lw=2) + + # Branch current arrow + branch_arrow = FancyArrowPatch((res_x - 0.3, res_y_top - 0.5), + (res_x - 0.3, res_y_top - 1.5), + arrowstyle='->', + color='blue', + lw=2, + mutation_scale=15) + ax.add_patch(branch_arrow) + + # Label resistor and current + 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) + + ax.set_xlim(0, 12) + ax.set_ylim(2, 8) + ax.axis('equal') + ax.axis('off') + + st.pyplot(fig) + + # Show formulas + st.write("### Parallel Circuit Formulas") + st.latex(r"\frac{1}{R_{total}} = \frac{1}{R_1} + \frac{1}{R_2} + ... + \frac{1}{R_n}") + + # Create the fractions part without f-strings + fractions_parts = [] + for r in resistances: + fractions_parts.append(r"\frac{1}{" + f"{r:.1f}" + "}") + fractions_text = " + ".join(fractions_parts) + + st.latex(r"\frac{1}{R_{total}} = " + fractions_text + f" = {inverse_resistance_sum:.4f}") + st.latex(r"R_{total} = \frac{1}{" + f"{inverse_resistance_sum:.4f}" + r"} = " + f"{total_resistance:.2f} \, \Omega") + st.latex(r"I_{total} = I_1 + I_2 + ... + I_n") + st.latex(f"I_{{total}} = {' + '.join([f'{i:.2f}' for i in branch_currents])} = {total_current:.2f} \, A") + st.latex(r"I_n = \frac{V}{R_n}") + +st.sidebar.markdown('''File "D:\projects\physics-simulations\pages\basic-circuit-analysis.py", line 359 + fractions_parts.append(r""\frac{1}{""" + f"{r:.1f}" + r""}""") + ^ +SyntaxError: unexpected character after line continuation character +### Basic Circuit Concepts + +**Ohm's Law:** V = IR +- V: Voltage (Volts) +- I: Current (Amperes) +- R: Resistance (Ohms) + +**Series Circuit Properties:** +- Same current through all components +- Rtotal = R1 + R2 + ... + Rn +- Vtotal = V1 + V2 + ... + Vn + +**Parallel Circuit Properties:** +- Same voltage across all components +- 1/Rtotal = 1/R1 + 1/R2 + ... + 1/Rn +- Itotal = I1 + I2 + ... + In +''') \ No newline at end of file diff --git a/pages/blackbody-radiation.py b/pages/blackbody-radiation.py new file mode 100644 index 0000000000000000000000000000000000000000..e5801833d8aefcaf2193849b40b5db534ac57cac --- /dev/null +++ b/pages/blackbody-radiation.py @@ -0,0 +1,85 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Physical constants +h = 6.62607015e-34 # Planck's constant (J·s) +c = 299792458 # Speed of light (m/s) +k_B = 1.380649e-23 # Boltzmann constant (J/K) +b = 2.897e-3 # Wien's displacement constant (m·K) + +# Planck's law function: Spectral radiance as a function of wavelength (in meters) and temperature +def planck(lambda_m, T): + """Calculate spectral radiance B(lambda, T) in W/m²/sr/m.""" + return (2 * h * c**2 / lambda_m**5) / (np.exp(h * c / (lambda_m * k_B * T)) - 1) + +# Streamlit app +st.title("Blackbody Radiation Simulation") + +# Educational explanation +st.write(""" +### Understanding Blackbody Radiation +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**: + +\[ B(\lambda, T) = \frac{2hc^2}{\lambda^5} \cdot \frac{1}{e^{\frac{hc}{\lambda k_B T}} - 1} \] + +- \( \lambda \): Wavelength (m) +- \( T \): Temperature (K) +- \( h \): Planck's constant +- \( c \): Speed of light +- \( k_B \): Boltzmann constant + +The **peak wavelength** (\( \lambda_{\text{peak}} \)) shifts with temperature according to **Wien's displacement law**: + +\[ \lambda_{\text{peak}} = \frac{b}{T} \] + +where \( b \approx 2.897 \times 10^{-3} \, \text{m·K} \). This simulation plots the spectrum and marks the peak. +""") + +# User input +T = st.slider("Temperature (K)", min_value=300, max_value=10000, value=5000, step=100, + help="Adjust the temperature to see how the radiation spectrum changes.") +normalize = st.checkbox("Normalize to maximum", value=True, + help="Normalize the curve to its peak value for shape comparison.") + +# Wavelength array (in meters, from 50 nm to 50,000 nm) +lambda_m = np.linspace(5e-8, 5e-5, 1000) +# Calculate spectral radiance in W/m²/sr/m +B_m = planck(lambda_m, T) +# Convert to nanometers for plotting +lambda_nm = lambda_m * 1e9 +# Convert spectral radiance to W/m²/sr/nm +B_nm = B_m / 1e9 + +# Prepare data for plotting +if normalize: + B_plot = B_nm / B_nm.max() + y_label = "Normalized Spectral Radiance" +else: + B_plot = B_nm + y_label = "Spectral Radiance (W/m²/sr/nm)" + +# Create the plot +fig, ax = plt.subplots(figsize=(10, 6)) +ax.plot(lambda_nm, B_plot, label=f"T = {T} K") +ax.set_xlabel("Wavelength (nm)") +ax.set_ylabel(y_label) +ax.set_title(f"Blackbody Radiation Spectrum at T = {T} K") +ax.grid(True) + +# Highlight visible spectrum (400 nm to 700 nm) +ax.axvspan(400, 700, color='yellow', alpha=0.3, label="Visible Range") + +# Calculate and mark peak wavelength +lambda_peak_m = b / T +lambda_peak_nm = lambda_peak_m * 1e9 +ax.axvline(lambda_peak_nm, color='red', linestyle='--', label=f"Peak: {lambda_peak_nm:.2f} nm") + +# Add legend +ax.legend() + +# Display the plot in Streamlit +st.pyplot(fig) + +# Display peak wavelength +st.write(f"**Peak wavelength**: {lambda_peak_nm:.2f} nm") \ No newline at end of file diff --git a/pages/capacitance.py b/pages/capacitance.py new file mode 100644 index 0000000000000000000000000000000000000000..20292839cd055f04dee48d38476338e75c96bcfc --- /dev/null +++ b/pages/capacitance.py @@ -0,0 +1,65 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Constants +epsilon_0 = 8.85e-12 # Permittivity of free space in F/m + +# Streamlit UI +st.title("Capacitance Simulation") +st.markdown(""" +This simulation demonstrates the capacitance of a parallel plate capacitor. +- **Capacitance (C)** is calculated as \( C = \epsilon_0 \frac{A}{d} \), where \(\epsilon_0 = 8.85 \times 10^{-12} \, \text{F/m}\). +- **Charge (Q)** is calculated as \( Q = C \times V \). +Adjust the sliders below to explore how \(A\), \(d\), and \(V\) affect \(C\) and \(Q\). +""") + +# Inputs +A = st.slider("Plate Area (m²)", 0.01, 1.0, 0.1, step=0.01) +d = st.slider("Plate Separation (m)", 0.001, 0.1, 0.01, step=0.001) +V = st.slider("Voltage (V)", 0.0, 10.0, 5.0, step=0.1) + +# Calculations +C = epsilon_0 * A / d +Q = C * V + +# Display results +st.write(f"**Capacitance (C):** {C:.2e} F") +st.write(f"**Charge (Q):** {Q:.2e} C") + +# Visualizations +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6)) + +# Capacitor diagram +h = 0.05 # Fixed height of plates +# Scale width based on sqrt(A) for visual representation +w = 0.1 + 0.4 * (np.sqrt(A) - np.sqrt(0.01)) / (np.sqrt(1) - np.sqrt(0.01)) +# Scale separation based on d +d_scaled = 0.1 + 0.4 * (d - 0.001) / (0.1 - 0.001) + +# Draw bottom plate +ax1.add_patch(plt.Rectangle((0.5 - w/2, 0), w, h, color='blue')) +# Draw top plate +ax1.add_patch(plt.Rectangle((0.5 - w/2, d_scaled), w, h, color='red')) +ax1.set_xlim(0, 1) +ax1.set_ylim(0, 1) +ax1.set_aspect('equal') +ax1.set_title("Parallel Plate Capacitor") +ax1.axis('off') +# Add labels +ax1.text(0.5, -0.1, f'A = {A:.2f} m²', ha='center') +ax1.text(0.5, d_scaled/2, f'd = {d:.3f} m', ha='center', va='center') + +# Q vs V plot +V_range = np.linspace(0, 10, 100) +Q_range = C * V_range +ax2.plot(V_range, Q_range, label=f'C = {C:.2e} F') +ax2.plot(V, Q, 'ro', label='Current point') +ax2.set_xlabel('Voltage (V)') +ax2.set_ylabel('Charge (C)') +ax2.set_title('Q vs V') +ax2.legend() +ax2.grid(True) + +# Display the plot +st.pyplot(fig) \ No newline at end of file diff --git a/pages/center-of-gravity.py b/pages/center-of-gravity.py new file mode 100644 index 0000000000000000000000000000000000000000..9cbdbdcb9f461dbbbe47840cfc0415f5d052da9c --- /dev/null +++ b/pages/center-of-gravity.py @@ -0,0 +1,83 @@ +import streamlit as st +import matplotlib.pyplot as plt +from shapely.geometry import Polygon +import numpy as np + +# App title and introduction +st.title("Center of Gravity Visualization") +st.markdown(""" +The **center of gravity** is the point where the weight of an object is evenly distributed in all directions. +This simulation lets you explore the center of gravity for different shapes by inputting their parameters. +""") + +# Sidebar for shape selection +st.sidebar.title("Shape Selection") +shape = st.sidebar.selectbox("Select a shape", ["Rectangle", "Triangle", "Circle", "Custom Polygon"]) + +# Input fields and shape definition based on selection +if shape == "Rectangle": + st.markdown("### Rectangle") + width = st.number_input("Width", min_value=0.0, value=1.0, step=0.1) + height = st.number_input("Height", min_value=0.0, value=1.0, step=0.1) + # Define rectangle points starting at (0,0) + points = [(0, 0), (width, 0), (width, height), (0, height)] + +elif shape == "Triangle": + st.markdown("### Triangle") + st.markdown("Enter the coordinates of the three vertices:") + x1 = st.number_input("X1", value=0.0, step=0.1) + y1 = st.number_input("Y1", value=0.0, step=0.1) + x2 = st.number_input("X2", value=1.0, step=0.1) + y2 = st.number_input("Y2", value=0.0, step=0.1) + x3 = st.number_input("X3", value=0.5, step=0.1) + y3 = st.number_input("Y3", value=1.0, step=0.1) + points = [(x1, y1), (x2, y2), (x3, y3)] + +elif shape == "Circle": + st.markdown("### Circle") + radius = st.number_input("Radius", min_value=0.0, value=1.0, step=0.1) + # Approximate circle as a polygon with 100 points + num_points = 100 + points = [(radius * np.cos(theta), radius * np.sin(theta)) + for theta in np.linspace(0, 2 * np.pi, num_points)] + +elif shape == "Custom Polygon": + st.markdown("### Custom Polygon") + st.markdown("Enter the coordinates of the vertices (one per line, e.g., 'x y') in order:") + points_str = st.text_area("Points", value="0 0\n1 0\n1 1\n0 1") + points = [] + for line in points_str.split('\n'): + if line.strip(): + try: + x, y = map(float, line.split()) + points.append((x, y)) + except ValueError: + st.error("Invalid input: Please enter numbers separated by a space (e.g., '1 2').") + st.stop() + if len(points) < 3: + st.error("Need at least 3 points to form a polygon.") + st.stop() + +# Create Polygon object and compute centroid +poly = Polygon(points) +centroid = poly.centroid + +# Plotting +st.markdown(f"### {shape} and its Center of Gravity") +fig, ax = plt.subplots() +poly_patch = plt.Polygon(points, fill=False, edgecolor='blue') +ax.add_artist(poly_patch) +ax.plot(centroid.x, centroid.y, 'ro', label='Center of Gravity') +ax.set_aspect('equal') # Ensure the shape isn't distorted +# Set plot limits with some padding +xs = [p[0] for p in points] +ys = [p[1] for p in points] +min_x, max_x = min(xs), max(xs) +min_y, max_y = min(ys), max(ys) +ax.set_xlim(min_x - 1, max_x + 1) +ax.set_ylim(min_y - 1, max_y + 1) +ax.legend() +st.pyplot(fig) + +# Display centroid coordinates +st.write(f"**Center of Gravity:** ({centroid.x:.2f}, {centroid.y:.2f})") \ No newline at end of file diff --git a/pages/center-of-mass.py b/pages/center-of-mass.py new file mode 100644 index 0000000000000000000000000000000000000000..8851238b67c5a31e301a4041ae0475de4e27313b --- /dev/null +++ b/pages/center-of-mass.py @@ -0,0 +1,75 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Sidebar for user input +st.sidebar.title("Input Particles") +instructions = """ +Enter particles in the format: mass,x,y +One particle per line. +For example: +1,0,0 +2,3,4 +1.5,2,1 +""" +st.sidebar.markdown(instructions) + +# Prefill with an example for immediate visualization +example_input = "1,0,0\n2,3,4\n1.5,2,1" +input_text = st.sidebar.text_area("Particles", value=example_input, height=200) + +# Parse the input +particles = [] +lines = input_text.strip().split('\n') +for line in lines: + if line.strip() == '': # Skip empty lines + continue + try: + m, x, y = map(float, line.split(',')) + particles.append((m, x, y)) + except ValueError: + st.error(f"Invalid input: {line}") + particles = [] # Reset on error + break + +# Process and visualize if there are valid particles +if particles: + # Extract masses and coordinates + masses = np.array([p[0] for p in particles]) + x_coords = np.array([p[1] for p in particles]) + y_coords = np.array([p[2] for p in particles]) + total_mass = np.sum(masses) + + if total_mass == 0: + st.write("Total mass is zero. Cannot compute Center of Mass.") + else: + # Calculate Center of Mass + x_com = np.sum(masses * x_coords) / total_mass + y_com = np.sum(masses * y_coords) / total_mass + + # Create the plot + fig, ax = plt.subplots() + ax.plot(x_coords, y_coords, 'bo', label='Particles') # Blue dots for particles + ax.plot(x_com, y_com, 'r*', markersize=15, label='Center of Mass') # Red star for COM + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_title("Center of Mass Visualization") + ax.legend() + ax.grid(True) # Add grid for readability + ax.set_aspect('equal') # Equal aspect ratio for accurate distances + + # Set plot limits dynamically + if len(particles) > 1: + x_min, x_max = np.min(x_coords), np.max(x_coords) + y_min, y_max = np.min(y_coords), np.max(y_coords) + ax.set_xlim(x_min - 1, x_max + 1) + ax.set_ylim(y_min - 1, y_max + 1) + else: + ax.set_xlim(-5, 5) + ax.set_ylim(-5, 5) + + # Display the plot in Streamlit + st.pyplot(fig) + st.write(f"Center of Mass: ({x_com:.2f}, {y_com:.2f})") +else: + st.write("Please enter at least one particle.") \ No newline at end of file diff --git a/pages/centripetal-force.py b/pages/centripetal-force.py new file mode 100644 index 0000000000000000000000000000000000000000..f8262bde19652bef89a24a33e86d4f8ae18a32b4 --- /dev/null +++ b/pages/centripetal-force.py @@ -0,0 +1,59 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("Centripetal Force Simulation") + +# Sidebar parameters for tuning the simulation +mass = st.sidebar.slider("Mass (kg)", 0.1, 10.0, 1.0, 0.1) +speed = st.sidebar.slider("Speed (m/s)", 0.1, 10.0, 5.0, 0.1) +radius = st.sidebar.slider("Radius (m)", 0.5, 10.0, 3.0, 0.1) + +# Calculate centripetal force: F = m * v^2 / r +centripetal_force = mass * (speed ** 2) / radius +st.sidebar.write("Centripetal Force (N):", round(centripetal_force, 2)) + +st.write(""" +This simulation visualizes an object in uniform circular motion. +The object follows a circular path and the green arrow indicates the centripetal force, +pointing inward toward the center of the circle. +""") + +if st.button("Start Simulation"): + plot_area = st.empty() # placeholder for updating plot + + # Time parameters for simulation + t = 0.0 + dt = 0.05 # time step in seconds + simulation_duration = 20 # seconds + + while t < simulation_duration: + # Compute angular position (theta = omega * t, where omega = speed/radius) + theta = (speed / radius) * t + x = radius * np.cos(theta) + y = radius * np.sin(theta) + + # Set up the plot + fig, ax = plt.subplots(figsize=(6,6)) + # Draw the circular path + circle = plt.Circle((0, 0), radius, color='blue', fill=False, linestyle='--') + ax.add_artist(circle) + # Plot the moving object as a red dot + ax.plot(x, y, 'ro', markersize=10) + # Draw the centripetal force arrow (from the object pointing to the center) + ax.arrow(x, y, -x, -y, head_width=0.2, head_length=0.3, fc='green', ec='green') + + # Set plot limits and formatting + ax.set_xlim(-radius - 1, radius + 1) + ax.set_ylim(-radius - 1, radius + 1) + ax.set_aspect('equal', 'box') + ax.set_title("Centripetal Force Simulation") + ax.set_xlabel("x (m)") + ax.set_ylabel("y (m)") + + # Update the plot in the Streamlit app + plot_area.pyplot(fig) + + t += dt + time.sleep(0.05) diff --git a/pages/chaos-theory.py b/pages/chaos-theory.py new file mode 100644 index 0000000000000000000000000000000000000000..6f274f94ca59be964a427261019a78611c11c9b3 --- /dev/null +++ b/pages/chaos-theory.py @@ -0,0 +1,50 @@ +import streamlit as st +import numpy as np +from scipy.integrate import odeint +import matplotlib.pyplot as plt + +# Define the Lorenz system +def lorenz(state, t, sigma, beta, rho): + x, y, z = state + dx = sigma * (y - x) + dy = x * (rho - z) - y + dz = x * y - beta * z + return [dx, dy, dz] + +# Title and description +st.title("Lorenz Attractor - Chaos Theory Visualization") +st.write( + "This simulation visualizes the Lorenz attractor, a system that exhibits chaotic behavior. " + "Adjust the parameters using the sidebar to see how the system's trajectory changes." +) + +# Sidebar for interactive parameter controls +st.sidebar.header("Simulation Parameters") +sigma = st.sidebar.slider("Sigma (σ)", 0.1, 20.0, 10.0) +beta = st.sidebar.slider("Beta (β)", 0.1, 10.0, 8.0/3.0, step=0.1) +rho = st.sidebar.slider("Rho (ρ)", 0.1, 50.0, 28.0, step=0.1) + +initial_x = st.sidebar.number_input("Initial x", value=1.0) +initial_y = st.sidebar.number_input("Initial y", value=1.0) +initial_z = st.sidebar.number_input("Initial z", value=1.0) + +t_max = st.sidebar.number_input("Simulation Time", value=40.0, min_value=1.0) +num_points = st.sidebar.number_input("Number of Points", value=10000, min_value=100, step=100) + +# Time array for simulation +t = np.linspace(0, t_max, num_points) + +# Initial state and integration of the Lorenz equations +state0 = [initial_x, initial_y, initial_z] +states = odeint(lorenz, state0, t, args=(sigma, beta, rho)) + +# Plotting the Lorenz attractor +fig = plt.figure(figsize=(10, 6)) +ax = fig.add_subplot(111, projection='3d') +ax.plot(states[:, 0], states[:, 1], states[:, 2], lw=0.5) +ax.set_xlabel("X") +ax.set_ylabel("Y") +ax.set_zlabel("Z") +ax.set_title("Lorenz Attractor") + +st.pyplot(fig) diff --git a/pages/conservation-of-angular-momentum.py b/pages/conservation-of-angular-momentum.py new file mode 100644 index 0000000000000000000000000000000000000000..68af5ea05d879442127abd08165700c6e7406bf8 --- /dev/null +++ b/pages/conservation-of-angular-momentum.py @@ -0,0 +1,66 @@ +import streamlit as st +import matplotlib.pyplot as plt +import numpy as np +import time + +# Set initial parameters +m = 1.0 # mass (kg) +r0 = 1.0 # initial radius (m) +ω0 = 1.0 # initial angular velocity (rad/s) +L = m * r0**2 * ω0 # angular momentum (kg·m²/s, constant) + +# Streamlit app title and explanation +st.title("Conservation of Angular Momentum Simulation") +st.write(""" +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. +""") + +# Slider for radius r +r = st.slider("Radius (r)", min_value=0.5, max_value=2.0, value=1.0, step=0.1) + +# Compute moment of inertia I and angular velocity ω +I = m * r**2 +ω = L / I + +# Display current values +st.write(f"**Mass (m):** {m} kg") +st.write(f"**Initial radius (r₀):** {r0} m") +st.write(f"**Initial angular velocity (ω₀):** {ω0} rad/s") +st.write(f"**Angular momentum (L):** {L} kg·m²/s") +st.write(f"**Current radius (r):** {r} m") +st.write(f"**Current moment of inertia (I):** {I} kg·m²") +st.write(f"**Current angular velocity (ω):** {ω:.2f} rad/s") +st.write(f"**Current L = I · ω:** {I * ω:.2f} kg·m²/s") + +# Animation setup +placeholder = st.empty() +dt = 0.1 # time step for animation (s) +num_frames = 100 # number of frames + +for frame in range(num_frames): + t = frame * dt + θ = (ω * t) % (2 * np.pi) # current angle, wrapped to 0-2π + x = r * np.cos(θ) + y = r * np.sin(θ) + + # Create figure + fig, ax = plt.subplots() + # Plot the circular path + circle = plt.Circle((0, 0), r, color='blue', fill=False) + ax.add_artist(circle) + # Plot the mass + ax.plot(x, y, 'ro', markersize=10) + # Plot a line from center to mass + ax.plot([0, x], [0, y], 'r-') + # Set axis limits and aspect + ax.set_xlim(-2.5, 2.5) + ax.set_ylim(-2.5, 2.5) + ax.set_aspect('equal') + ax.set_title(f"ω = {ω:.2f} rad/s") + + # Update the placeholder with the new figure + placeholder.pyplot(fig) + plt.close(fig) # Free memory + + # Control animation speed + time.sleep(0.05) \ No newline at end of file diff --git a/pages/conservation-of-energy.py b/pages/conservation-of-energy.py new file mode 100644 index 0000000000000000000000000000000000000000..9e6b38204c4da35941e40526e9045879c796b786 --- /dev/null +++ b/pages/conservation-of-energy.py @@ -0,0 +1,137 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas + +class EnergyConservationSimulation: + def __init__(self, mass=1.0, gravity=9.8, initial_height=10.0): + """ + Initialize the simulation parameters + + :param mass: Mass of the object (kg) + :param gravity: Acceleration due to gravity (m/s^2) + :param initial_height: Starting height of the object (m) + """ + self.mass = mass + self.gravity = gravity + self.initial_height = initial_height + + def calculate_energies(self): + """ + Calculate potential and kinetic energies at different heights + + :return: Tuple of lists (heights, potential_energies, kinetic_energies, total_energies) + """ + # Create array of heights from initial height to 0 + heights = np.linspace(0, self.initial_height, 100) + + # Calculate potential energy at each height + potential_energies = self.mass * self.gravity * heights + + # Calculate kinetic energy at each height (using conservation of energy principle) + kinetic_energies = self.mass * self.gravity * self.initial_height - potential_energies + + # Total energy remains constant + total_energies = np.full_like(heights, self.mass * self.gravity * self.initial_height) + + return heights, potential_energies, kinetic_energies, total_energies + + def create_energy_plot(self): + """ + Create a matplotlib figure showing energy transformations + + :return: matplotlib figure + """ + # Calculate energies + heights, pe, ke, te = self.calculate_energies() + + # Create the plot + fig, ax = plt.subplots(figsize=(10, 6)) + + # Plot the energy curves + ax.plot(heights, pe, label='Potential Energy', color='blue') + ax.plot(heights, ke, label='Kinetic Energy', color='red') + ax.plot(heights, te, label='Total Energy', color='green', linestyle='--') + + # Customize the plot + ax.set_title('Conservation of Energy: Energy vs Height', fontsize=15) + ax.set_xlabel('Height (m)', fontsize=12) + ax.set_ylabel('Energy (J)', fontsize=12) + ax.legend() + ax.grid(True, linestyle=':', alpha=0.7) + + return fig + +def main(): + # Streamlit app title and description + st.title('Conservation of Energy Simulation') + st.write(""" + ## Exploring Energy Transformations + + This interactive simulation demonstrates the principle of Conservation of Energy + for a falling object. As the object falls: + - Potential Energy decreases + - Kinetic Energy increases + - Total Energy remains constant + """) + + # Sidebar for simulation parameters + st.sidebar.header('Simulation Parameters') + + # Mass input + mass = st.sidebar.slider('Mass of Object (kg)', + min_value=0.1, + max_value=10.0, + value=1.0, + step=0.1) + + # Initial height input + initial_height = st.sidebar.slider('Initial Height (m)', + min_value=1.0, + max_value=20.0, + value=10.0, + step=0.5) + + # Gravity (with option to modify) + gravity = st.sidebar.number_input('Gravity (m/s²)', + min_value=0.1, + max_value=20.0, + value=9.8, + step=0.1) + + # Create simulation instance + sim = EnergyConservationSimulation( + mass=mass, + gravity=gravity, + initial_height=initial_height + ) + + # Generate and display the plot + fig = sim.create_energy_plot() + st.pyplot(fig) + + # Additional information section + st.markdown("### Key Insights") + st.markdown(""" + - 🔵 Blue Line: Potential Energy - Decreases as height decreases + - 🔴 Red Line: Kinetic Energy - Increases as height decreases + - 🟢 Green Dashed Line: Total Energy - Remains constant + + The simulation shows how energy is transformed from potential to kinetic + energy while maintaining a constant total energy. + """) + + # Calculate and display some specific values + heights, pe, ke, te = sim.calculate_energies() + col1, col2, col3 = st.columns(3) + + with col1: + st.metric("Initial Potential Energy", f"{pe[0]:.2f} J") + with col2: + st.metric("Final Kinetic Energy", f"{ke[-1]:.2f} J") + with col3: + st.metric("Total Energy", f"{te[0]:.2f} J") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pages/conservation-of-momentum.py b/pages/conservation-of-momentum.py new file mode 100644 index 0000000000000000000000000000000000000000..1be9708ec850f61409072abce3b338f176844abe --- /dev/null +++ b/pages/conservation-of-momentum.py @@ -0,0 +1,110 @@ +import streamlit as st +import matplotlib.pyplot as plt +import numpy as np +import time + +# Title of the app +st.title("Conservation of Momentum Simulation") + +# Sidebar for user inputs +st.sidebar.title("Parameters") +m1 = st.sidebar.slider("Mass of Object 1 (kg)", 0.1, 10.0, 1.0, step=0.1) +m2 = st.sidebar.slider("Mass of Object 2 (kg)", 0.1, 10.0, 1.0, step=0.1) +u1 = st.sidebar.slider("Initial Velocity of Object 1 (m/s)", -10.0, 10.0, 5.0, step=0.1) +u2 = st.sidebar.slider("Initial Velocity of Object 2 (m/s)", -10.0, 10.0, -3.0, step=0.1) +initial_distance = st.sidebar.slider("Initial Distance Between Objects (m)", 1.0, 20.0, 10.0, step=0.1) + +# Initial positions +x1 = 0.0 # Object 1 starts at origin +x2 = initial_distance # Object 2 starts at the initial distance + +# Determine if and when a collision occurs +# For x1 < x2, collision happens if u1 > u2 (Object 1 catches up to Object 2) +if u1 > u2: + t_collision = (x2 - x1) / (u1 - u2) + collision_message = f"Time to collision: {t_collision:.2f} s" +else: + t_collision = float('inf') + collision_message = "Objects will not collide with these velocities." + +st.write(collision_message) + +# Calculate final velocities after elastic collision (if it occurs) +if t_collision < float('inf'): + v1 = (u1 * (m1 - m2) + 2 * m2 * u2) / (m1 + m2) + v2 = (u2 * (m2 - m1) + 2 * m1 * u1) / (m1 + m2) +else: + v1 = u1 # No collision, velocities remain unchanged + v2 = u2 + +# Simulation parameters +dt = 0.1 # Time step (s) +total_time = 10.0 # Total simulation time (s) + +# Initialize simulation variables +pos1 = x1 +pos2 = x2 +vel1 = u1 +vel2 = u2 +collided = False +times = [] +momenta1 = [] +momenta2 = [] +total_momenta = [] + +# Placeholder for the dynamic plot +placeholder = st.empty() + +# Run the simulation +t = 0.0 +while t < total_time: + # Check for collision + if not collided and t >= t_collision: + vel1 = v1 + vel2 = v2 + collided = True + + # Update positions + pos1 += vel1 * dt + pos2 += vel2 * dt + + # Calculate momenta + momentum1 = m1 * vel1 + momentum2 = m2 * vel2 + total_momentum = momentum1 + momentum2 + + # Store data for plotting + times.append(t) + momenta1.append(momentum1) + momenta2.append(momentum2) + total_momenta.append(total_momentum) + + # Create the visualization + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # Top plot: Positions of the objects + ax1.plot([pos1], [0], 'ro', markersize=10, label='Object 1 (Red)') + ax1.plot([pos2], [0], 'bo', markersize=10, label='Object 2 (Blue)') + ax1.set_xlim(-5, initial_distance + 5) + ax1.set_ylim(-1, 1) + ax1.set_xlabel('Position (m)') + ax1.set_title(f'Time: {t:.1f} s') + ax1.legend() + ax1.grid(True) + + # Bottom plot: Momentum over time + ax2.plot(times, momenta1, 'b-', label='Momentum of Object 1') + ax2.plot(times, momenta2, 'g-', label='Momentum of Object 2') + ax2.plot(times, total_momenta, 'r-', label='Total Momentum') + ax2.set_xlabel('Time (s)') + ax2.set_ylabel('Momentum (kg m/s)') + ax2.legend() + ax2.grid(True) + + # Update the plot in Streamlit + placeholder.pyplot(fig) + plt.close(fig) + + # Increment time and add a delay for animation + t += dt + time.sleep(0.1) \ No newline at end of file diff --git a/pages/coulombs-law.py b/pages/coulombs-law.py new file mode 100644 index 0000000000000000000000000000000000000000..545a562be4e607a6692249cda8d6a2744eebe7cc --- /dev/null +++ b/pages/coulombs-law.py @@ -0,0 +1,61 @@ +import streamlit as st +import matplotlib.pyplot as plt + +# Set up the Streamlit app +st.title("Coulomb's Law Visualization") +st.write("Adjust the charges Q1, Q2, and the distance r to see the force between them.") + +# Input sliders for Q1, Q2, and r +Q1 = st.slider("Charge Q1", min_value=-10.0, max_value=10.0, value=1.0, step=0.1) +Q2 = st.slider("Charge Q2", min_value=-10.0, max_value=10.0, value=1.0, step=0.1) +r = st.slider("Distance r", min_value=0.1, max_value=10.0, value=1.0, step=0.1) + +# Calculate the force (k=1 for simplicity) +F = (Q1 * Q2) / r**2 + +# Function to determine color based on charge sign +def get_color(Q): + if Q > 0: + return 'blue' # Positive charge + elif Q < 0: + return 'red' # Negative charge + else: + return 'gray' # Zero charge + +# Assign colors to charges +colors = [get_color(Q1), get_color(Q2)] + +# Create the plot +fig, ax = plt.subplots() +ax.set_xlim(-1, r + 1) # Adjust x-axis based on distance r +ax.set_ylim(-1, 1) # Fixed y-axis for a 1D representation + +# Plot the charges as points +ax.scatter([0, r], [0, 0], c=colors, s=100) +ax.text(-0.5, 0.5, f'Q1 = {Q1:.1f}') # Label for Q1 +ax.text(r + 0.5, 0.5, f'Q2 = {Q2:.1f}') # Label for Q2 + +# Draw an arrow to represent the force direction on Q1 +if F != 0: + # If Q1*Q2 > 0 (like charges), force is repulsive (to the left) + # If Q1*Q2 < 0 (unlike charges), force is attractive (to the right) + dx = -0.5 if Q1 * Q2 > 0 else 0.5 + ax.quiver(0, 0, dx, 0, scale=1, scale_units='xy', angles='xy', color='green') + +# Customize the plot +ax.set_xlabel('Distance') +ax.set_title("Coulomb's Law") + +# Display the plot in Streamlit +st.pyplot(fig) + +# Display force magnitude and direction +st.write(f"**Force magnitude:** {abs(F):.2f} units") +if Q1 * Q2 > 0: + direction = "to the left (repulsive)" +elif Q1 * Q2 < 0: + direction = "to the right (attractive)" +else: + direction = "zero" +st.write(f"**Force direction on Q1:** {direction}") +st.write("**Note:** Coulomb's constant k is set to 1 for simplicity.") \ No newline at end of file diff --git a/pages/diffraction.py b/pages/diffraction.py new file mode 100644 index 0000000000000000000000000000000000000000..373343fd3f89eaf027a2f41f5a692e4989fb09c2 --- /dev/null +++ b/pages/diffraction.py @@ -0,0 +1,56 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Title and description +st.title("Single-Slit Diffraction Simulation") +st.write("Adjust the parameters below to explore how the diffraction pattern changes.") + +# Theory section +st.markdown("### Theory") +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:") +st.latex(r"I(y) = I_0 \left( \frac{\sin \beta}{\beta} \right)^2") +st.write("where") +st.latex(r"\beta = \frac{\pi a y}{\lambda D}") +st.write("- \(I_0\): maximum intensity at the center (set to 1 for relative intensity)") +st.write("- \(\lambda\): wavelength of the light") +st.write("- \(a\): slit width") +st.write("- \(D\): distance from slit to screen") +st.write("- \(y\): position on the screen") +st.write("The minima (dark bands) occur at:") +st.latex(r"y = \pm \frac{m \lambda D}{a}, \quad m = 1, 2, 3, \dots") +st.write("This simulation plots \(I(y)\) vs. \(y\), showing how the pattern spreads or narrows based on your inputs.") + +# Parameter sliders +lambda_nm = st.slider("Wavelength λ (nm)", 300, 800, 500, help="Wavelength of light in nanometers (visible range: 400-700 nm)") +a_um = st.slider("Slit width a (μm)", 10, 1000, 100, help="Width of the slit in micrometers") +D_m = st.slider("Distance to screen D (m)", 0.5, 5.0, 1.0, help="Distance from slit to screen in meters") + +# Convert units to meters +lambda_m = lambda_nm * 1e-9 # nm to m +a_m = a_um * 1e-6 # μm to m +D_m = D_m # already in m + +# Generate position array (y) in meters +y_m = np.linspace(-0.02, 0.02, 1000) # -20 mm to 20 mm in meters + +# Compute the diffraction parameter β and intensity I(y) +z = (a_m * y_m) / (lambda_m * D_m) +I = (np.sinc(z))**2 # np.sinc(x) = sin(πx)/(πx), and I = [sin(β)/β]^2 + +# Convert y to millimeters for plotting +y_mm = y_m * 1e3 # m to mm + +# Create the plot +fig, ax = plt.subplots(figsize=(10, 6)) +ax.plot(y_mm, I, color='blue') +ax.set_xlabel("Position on Screen (mm)") +ax.set_ylabel("Relative Intensity") +ax.set_ylim(0, 1.05) # Intensity from 0 to just above 1 +ax.grid(True) +st.pyplot(fig) + +# Calculate and display the position of the first minimum +y_min_m = lambda_m * D_m / a_m # in meters +y_min_mm = y_min_m * 1e3 # in millimeters +st.write(f"First minimum at y = ± {y_min_mm:.2f} mm") \ No newline at end of file diff --git a/pages/dimensional-analysis.py b/pages/dimensional-analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..2757401e86f83bb5aed84dd75f8f200c9350a7c1 --- /dev/null +++ b/pages/dimensional-analysis.py @@ -0,0 +1,110 @@ +import streamlit as st +import pint +import math + +# Initialize the unit registry from Pint +ureg = pint.UnitRegistry() + +st.title("Dimensional Analysis Visualizer") +st.markdown(""" +This interactive simulation demonstrates how dimensional analysis works for different physics equations. +Select an example from the sidebar, adjust the parameters, and see how the units come together to verify that the equation is dimensionally consistent. +""") + +# Sidebar to select the example +option = st.sidebar.selectbox("Select a physics concept", + ["Gravitational Force", "Pendulum Period", "Kinetic Energy"]) + +if option == "Gravitational Force": + st.header("Gravitational Force: F = G * m₁ * m₂ / r²") + st.markdown(""" + **Concept:** The gravitational force between two masses is given by Newton’s law of gravitation. + + **Units Check:** + - **G (Gravitational constant):** m³/(kg·s²) + - **m₁ and m₂ (Masses):** kg + - **r (Distance):** m + When you combine these, the result should have the units of force (Newton): kg·m/s². + """) + # User inputs for masses and distance + m1_val = st.number_input("Mass 1 (kg)", value=5.0, min_value=0.0) + m2_val = st.number_input("Mass 2 (kg)", value=5.0, min_value=0.0) + r_val = st.number_input("Distance (m)", value=1.0, min_value=0.1) + + # Define constants and variables with units + G_val = 6.67430e-11 # Gravitational constant (SI units) + G = G_val * ureg("meter**3/(kilogram*second**2)") + m1 = m1_val * ureg.kilogram + m2 = m2_val * ureg.kilogram + r = r_val * ureg.meter + + # Compute gravitational force using dimensional quantities + F = G * m1 * m2 / (r**2) + + st.write("**Computed Gravitational Force:**", F) + st.write("**Dimensionality of F:**", F.dimensionality) + # Expected dimension for force (Newton) + expected = ureg.newton + st.write("**Expected Dimensionality (Newton):**", expected.dimensionality) + if F.dimensionality == expected.dimensionality: + st.success("Dimensional analysis checks out!") + else: + st.error("There is an inconsistency in the dimensions.") + +elif option == "Pendulum Period": + st.header("Pendulum Period: T = 2π √(L/g)") + st.markdown(""" + **Concept:** The period of a simple pendulum (for small oscillations) depends on its length L and the gravitational acceleration g. + + **Units Check:** + - **L (Length):** m + - **g (Gravitational acceleration):** m/s² + 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. + """) + # User inputs for pendulum length and gravitational acceleration + L_val = st.number_input("Length L (m)", value=1.0, min_value=0.0) + g_val = st.number_input("Gravitational acceleration g (m/s²)", value=9.8, min_value=0.1) + + L = L_val * ureg.meter + g = g_val * ureg("meter/second**2") + + # Calculate the period numerically (2π is dimensionless) + T_numeric = 2 * math.pi * math.sqrt(L_val / g_val) + st.write("**Computed Pendulum Period T:**", f"{T_numeric:.3f} seconds") + + # Dimensional analysis using Pint: sqrt(L/g) must have the dimension of time + time_dim = (L / g)**0.5 + st.write("**Dimensionality of √(L/g):**", time_dim.dimensionality) + expected = ureg.second + st.write("**Expected Dimensionality (second):**", expected.dimensionality) + if time_dim.dimensionality == expected.dimensionality: + st.success("Dimensional analysis checks out!") + else: + st.error("There is an inconsistency in the dimensions.") + +elif option == "Kinetic Energy": + st.header("Kinetic Energy: E = ½ m v²") + st.markdown(""" + **Concept:** The kinetic energy of a moving object is given by one-half the product of its mass and the square of its velocity. + + **Units Check:** + - **m (Mass):** kg + - **v (Velocity):** m/s + Therefore, m·v² has units kg·(m²/s²), which are the units of energy (Joule). + """) + # User inputs for mass and velocity + m_val = st.number_input("Mass m (kg)", value=1.0, min_value=0.0) + v_val = st.number_input("Velocity v (m/s)", value=1.0, min_value=0.0) + + m = m_val * ureg.kilogram + v = v_val * ureg("meter/second") + E = 0.5 * m * (v**2) + + st.write("**Computed Kinetic Energy E:**", E) + st.write("**Dimensionality of E:**", E.dimensionality) + expected = ureg.joule + st.write("**Expected Dimensionality (Joule):**", expected.dimensionality) + if E.dimensionality == expected.dimensionality: + st.success("Dimensional analysis checks out!") + else: + st.error("There is an inconsistency in the dimensions.") diff --git a/pages/distance.py b/pages/distance.py new file mode 100644 index 0000000000000000000000000000000000000000..397583d2a2eced635b194fad5500ef2eb317c8d9 --- /dev/null +++ b/pages/distance.py @@ -0,0 +1,142 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import math + +def calculate_euclidean_distance(x1, y1, x2, y2): + """Calculate Euclidean distance between two points.""" + return math.sqrt((x2 - x1)**2 + (y2 - y1)**2) + +def create_distance_plot(x1, y1, x2, y2): + """Create a matplotlib plot showing distance between two points.""" + fig, ax = plt.subplots(figsize=(8, 6)) + ax.plot(x1, y1, 'ro', label='Point 1') # Red dot for first point + ax.plot(x2, y2, 'bo', label='Point 2') # Blue dot for second point + ax.plot([x1, x2], [y1, y2], 'g--', label='Distance') # Green dashed line + ax.set_title('Distance Between Two Points') + ax.set_xlabel('X-axis') + ax.set_ylabel('Y-axis') + ax.grid(True) + ax.legend() + return fig + +def create_manhattan_distance_plot(x1, y1, x2, y2): + """Create a matplotlib plot showing Manhattan distance.""" + fig, ax = plt.subplots(figsize=(8, 6)) + ax.plot(x1, y1, 'ro', label='Point 1') # Red dot for first point + ax.plot(x2, y2, 'bo', label='Point 2') # Blue dot for second point + + # Draw Manhattan distance path + ax.plot([x1, x2], [y1, y1], 'g--') # Horizontal path + ax.plot([x2, x2], [y1, y2], 'g--') # Vertical path + + ax.set_title('Manhattan Distance Visualization') + ax.set_xlabel('X-axis') + ax.set_ylabel('Y-axis') + ax.grid(True) + ax.legend() + return fig + +def create_distance_over_time_plot(velocity, time): + """Create a matplotlib plot showing distance over time.""" + fig, ax = plt.subplots(figsize=(10, 6)) + + # Distance vs Time plot + t = np.linspace(0, time, 100) + d = velocity * t + + ax.plot(t, d, 'b-', label='Distance') + ax.set_title('Distance Traveled vs Time') + ax.set_xlabel('Time (seconds)') + ax.set_ylabel('Distance (units)') + ax.grid(True) + ax.legend() + + # Annotate final distance + distance = velocity * time + ax.annotate(f'Final Distance: {distance} units', + xy=(time, distance), + xytext=(time/2, distance/2), + arrowprops=dict(facecolor='black', shrink=0.05)) + + return fig + +def main(): + st.title('Distance Visualization') + + # Sidebar for navigation + app_mode = st.sidebar.selectbox('Choose Visualization Mode', + ['Euclidean Distance', 'Manhattan Distance', 'Distance Over Time']) + + if app_mode == 'Euclidean Distance': + st.header('Euclidean Distance Calculator') + + # Input for coordinates + col1, col2 = st.columns(2) + with col1: + x1 = st.number_input('X1 Coordinate', value=0.0, key='x1_euc') + y1 = st.number_input('Y1 Coordinate', value=0.0, key='y1_euc') + + with col2: + x2 = st.number_input('X2 Coordinate', value=3.0, key='x2_euc') + y2 = st.number_input('Y2 Coordinate', value=4.0, key='y2_euc') + + # Calculate distance + distance = calculate_euclidean_distance(x1, y1, x2, y2) + + # Display results + st.write(f'Euclidean Distance: {distance:.2f} units') + + # Create and display plot + fig = create_distance_plot(x1, y1, x2, y2) + st.pyplot(fig) + plt.close(fig) + + elif app_mode == 'Manhattan Distance': + st.header('Manhattan Distance Calculator') + + # Input for coordinates + col1, col2 = st.columns(2) + with col1: + x1 = st.number_input('X1 Coordinate', value=0.0, key='x1_man') + y1 = st.number_input('Y1 Coordinate', value=0.0, key='y1_man') + + with col2: + x2 = st.number_input('X2 Coordinate', value=3.0, key='x2_man') + y2 = st.number_input('Y2 Coordinate', value=4.0, key='y2_man') + + # Calculate Manhattan distance + manhattan_distance = abs(x2 - x1) + abs(y2 - y1) + + # Display results + st.write(f'Manhattan Distance: {manhattan_distance:.2f} units') + + # Create Manhattan distance visualization + fig = create_manhattan_distance_plot(x1, y1, x2, y2) + st.pyplot(fig) + plt.close(fig) + + elif app_mode == 'Distance Over Time': + st.header('Distance Traveled Over Time') + + # Velocity input + velocity = st.slider('Velocity (units/second)', 1, 20, 5) + + # Time input + time = st.slider('Time (seconds)', 1, 60, 10) + + # Calculate distance + distance = velocity * time + + # Visualization + fig = create_distance_over_time_plot(velocity, time) + st.pyplot(fig) + plt.close(fig) + + # Display numerical results + st.write(f'Velocity: {velocity} units/second') + st.write(f'Time: {time} seconds') + st.write(f'Total Distance: {distance} units') + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pages/electic-charge.py b/pages/electic-charge.py new file mode 100644 index 0000000000000000000000000000000000000000..987cfda4659a71a2945cd1043431331515680161 --- /dev/null +++ b/pages/electic-charge.py @@ -0,0 +1,86 @@ +import streamlit as st +import plotly.graph_objects as go +import numpy as np + +# Initialize session state to store charges +if 'charges' not in st.session_state: + st.session_state.charges = [] + +# User interface +st.title("Electric Charge Simulation") +st.write("Add charges and visualize the forces between them.") + +# Input fields for adding a charge +x = st.number_input("X position", min_value=-10.0, max_value=10.0, value=0.0) +y = st.number_input("Y position", min_value=-10.0, max_value=10.0, value=0.0) +q = st.selectbox("Charge", [1, -1]) + +# Button to add a charge +if st.button("Add Charge"): + st.session_state.charges.append({'x': x, 'y': y, 'q': q}) + +# Button to clear all charges +if st.button("Clear All Charges"): + st.session_state.charges = [] + +# Slider to adjust force arrow scale +force_scale = st.slider("Force scale", min_value=0.1, max_value=10.0, value=1.0) + +# Create Plotly figure +fig = go.Figure() + +# Plot all charges +for charge in st.session_state.charges: + fig.add_trace(go.Scatter( + x=[charge['x']], + y=[charge['y']], + mode='markers+text', + text=[str(charge['q'])], + textposition='top right', + marker=dict(size=10, color='red' if charge['q'] > 0 else 'blue') + )) + +# Calculate and plot forces +k = 1.0 # Coulomb's constant (simplified for visualization) +for charge in st.session_state.charges: + Fx, Fy = 0, 0 + for other in st.session_state.charges: + if other != charge: # Skip self-interaction + dx = other['x'] - charge['x'] + dy = other['y'] - charge['y'] + r = np.sqrt(dx**2 + dy**2) + if r > 0: # Avoid division by zero + # Force magnitude: F = k * q1 * q2 / r^2 + F = k * charge['q'] * other['q'] / (r**2) + # Force components along unit vector from other to charge + Fx += F * (-dx / r) + Fy += F * (-dy / r) + # Draw force arrow + arrow_dx = Fx * force_scale + arrow_dy = Fy * force_scale + if np.sqrt(arrow_dx**2 + arrow_dy**2) > 0: # Only draw if force exists + fig.add_annotation( + x=charge['x'] + arrow_dx, + y=charge['y'] + arrow_dy, + ax=charge['x'], + ay=charge['y'], + xref='x', yref='y', axref='x', ayref='y', + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor='green' + ) + +# Configure plot layout +fig.update_layout( + xaxis_range=[-10, 10], + yaxis_range=[-10, 10], + width=600, + height=600, + showlegend=False, + title="Charge Interactions" +) + +# Display the plot in Streamlit +st.plotly_chart(fig) \ No newline at end of file diff --git a/pages/electic-field.py b/pages/electic-field.py new file mode 100644 index 0000000000000000000000000000000000000000..9d2ac09a45b35f52fa24fe20ed0a1bbdd49a2b1f --- /dev/null +++ b/pages/electic-field.py @@ -0,0 +1,102 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Title of the Streamlit app +st.title("Electric Field Visualization") +st.write("Add charges to see how the electric field changes! Positive charges are red, negative charges are blue.") + +# Initialize session state to store charges +if 'charges' not in st.session_state: + st.session_state.charges = [] + +# Function to calculate the electric field on a grid +def calculate_electric_field(charges, X, Y, k=1, epsilon=1e-6): + """ + Calculate the electric field components (E_x, E_y) at each point on the grid. + + Parameters: + - charges: List of dictionaries with 'x', 'y', and 'q' for each charge + - X, Y: 2D arrays from np.meshgrid representing grid coordinates + - k: Coulomb's constant (set to 1 for simplicity) + - epsilon: Small value to avoid division by zero at charge locations + + Returns: + - E_x, E_y: Electric field components as 2D arrays + """ + E_x_total = np.zeros_like(X) + E_y_total = np.zeros_like(Y) + for charge in charges: + dx = X - charge['x'] + dy = Y - charge['y'] + r = np.sqrt(dx**2 + dy**2 + epsilon) # Distance with epsilon to avoid singularity + # E = k * q * r̂ / r^2, where r̂ is the unit vector (dx/r, dy/r) + # So, E_x = k * q * dx / r^3, E_y = k * q * dy / r^3 + E_x_charge = k * charge['q'] * dx / r**3 + E_y_charge = k * charge['q'] * dy / r**3 + E_x_total += E_x_charge + E_y_total += E_y_charge + return E_x_total, E_y_total + +# UI to add a charge +st.subheader("Add a Charge") +x = st.slider("X position", -5.0, 5.0, 0.0, key="x_add") +y = st.slider("Y position", -5.0, 5.0, 0.0, key="y_add") +q = st.selectbox("Charge", [1, -1], format_func=lambda val: "+1" if val > 0 else "-1", key="q_add") +if st.button("Add Charge", key="add_charge"): + st.session_state.charges.append({'x': x, 'y': y, 'q': q}) + st.success(f"Added charge q={q} at ({x}, {y})") + +# UI to display and remove charges +st.subheader("Current Charges") +if st.session_state.charges: + for i, charge in enumerate(st.session_state.charges): + st.write(f"Charge {i}: x={charge['x']}, y={charge['y']}, q={'+' if charge['q'] > 0 else '-'}1") + + # Select and remove a charge + charge_to_remove = st.selectbox( + "Select charge to remove", + options=range(len(st.session_state.charges)), + 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", + key="charge_to_remove" + ) + if st.button("Remove Selected Charge", key="remove_charge"): + del st.session_state.charges[charge_to_remove] + st.success("Charge removed!") +else: + st.write("No charges added yet. Add some to see the electric field.") + +# Create the plot if there are charges +if st.session_state.charges: + # Define the grid for calculation + x_grid = np.linspace(-5, 5, 20) + y_grid = np.linspace(-5, 5, 20) + X, Y = np.meshgrid(x_grid, y_grid) + + # Calculate the electric field + E_x, E_y = calculate_electric_field(st.session_state.charges, X, Y) + + # Create the plot + fig, ax = plt.subplots(figsize=(8, 8)) + ax.streamplot(X, Y, E_x, E_y, color='black', linewidth=1, density=1.5) + + # Plot each charge + for charge in st.session_state.charges: + color = 'red' if charge['q'] > 0 else 'blue' + ax.plot(charge['x'], charge['y'], 'o', color=color, markersize=10, label=f"q={charge['q']}") + + # Set plot limits and labels + ax.set_xlim(-5, 5) + ax.set_ylim(-5, 5) + ax.set_xlabel("X") + ax.set_ylabel("Y") + ax.set_title("Electric Field Lines") + ax.grid(True) + + # Display the plot in Streamlit + st.pyplot(fig) +else: + st.info("Add some charges above to visualize the electric field!") + +# Footer +st.write("Note: The field lines point away from positive charges and toward negative charges, following the direction a positive test charge would move.") \ No newline at end of file diff --git a/pages/electomagnetic-induction.py b/pages/electomagnetic-induction.py new file mode 100644 index 0000000000000000000000000000000000000000..b4cdebc30c5c325258ded3b1a95b0fb904467d11 --- /dev/null +++ b/pages/electomagnetic-induction.py @@ -0,0 +1,298 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation +import matplotlib.patches as patches +from matplotlib.collections import PatchCollection +import matplotlib.gridspec as gridspec +from io import BytesIO +import base64 + +# Set page config +st.set_page_config( + page_title="Electromagnetic Induction Simulator", + layout="wide" +) + +# Title and description +st.title("Electromagnetic Induction Simulator") +st.markdown(""" +This simulation demonstrates the principle of electromagnetic induction, where a changing magnetic field +induces an electric current in a conductor. You can adjust various parameters to see how they affect the induced voltage. +""") + +# Create two columns for controls and visualization +col1, col2 = st.columns([1, 3]) + +# Control parameters +with col1: + st.subheader("Controls") + + # Magnet properties + st.markdown("### Magnet Properties") + magnet_strength = st.slider("Magnetic Field Strength (T)", 0.1, 2.0, 1.0, 0.1) + magnet_speed = st.slider("Magnet Speed", 0.5, 3.0, 1.5, 0.1) + magnet_direction = st.radio("Magnet Motion", ["Moving In", "Moving Out", "Oscillating"]) + + # Coil properties + st.markdown("### Coil Properties") + coil_turns = st.slider("Number of Coil Turns", 5, 50, 20, 5) + coil_radius = st.slider("Coil Radius (cm)", 2.0, 8.0, 5.0, 0.5) + coil_resistance = st.slider("Coil Resistance (Ω)", 1.0, 20.0, 10.0, 1.0) + +# Main visualization +with col2: + st.markdown("### Simulation") + + # Create a placeholder for our animation + plot_placeholder = st.empty() + + # Create a static visualization that updates with parameters + fig = plt.figure(figsize=(10, 8)) + gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1]) + + # Top plot: Physical setup + ax1 = fig.add_subplot(gs[0]) + ax1.set_xlim(-10, 10) + ax1.set_ylim(-10, 10) + ax1.set_aspect('equal') + ax1.set_title('Magnet and Coil Configuration') + ax1.set_xlabel('Position (cm)') + ax1.set_ylabel('Position (cm)') + + # Draw the coil (circle) + coil = plt.Circle((0, 0), coil_radius, fill=False, color='black', linewidth=2) + ax1.add_artist(coil) + + # Add coil turns visualization + theta = np.linspace(0, 2*np.pi, coil_turns+1)[:-1] + coil_points_x = coil_radius * np.cos(theta) + coil_points_y = coil_radius * np.sin(theta) + + for i in range(coil_turns): + ax1.plot(coil_points_x[i], coil_points_y[i], 'ko', markersize=3) + + # Initial magnet position depends on the selected motion + if magnet_direction == "Moving In": + magnet_pos = -9 # Start far left + elif magnet_direction == "Moving Out": + magnet_pos = 0 # Start at center + else: # Oscillating + magnet_pos = -5 # Start slightly to the left + + # Draw the magnet (rectangle) + magnet_width, magnet_height = 2, 4 + magnet = patches.Rectangle((magnet_pos-magnet_width/2, -magnet_height/2), + magnet_width, magnet_height, + linewidth=1, edgecolor='r', facecolor='r', alpha=0.7) + ax1.add_patch(magnet) + + # Add N and S labels to the magnet + ax1.text(magnet_pos, 1, "N", fontsize=12, ha='center', color='white', fontweight='bold') + ax1.text(magnet_pos, -1, "S", fontsize=12, ha='center', color='white', fontweight='bold') + + # Add magnetic field lines + def plot_magnetic_field(pos_x, strength): + # Clear previous field lines + for line in ax1.lines[:]: + if line not in coil_points: + ax1.lines.remove(line) + + # Field strength indicator by number and spread of field lines + num_field_lines = int(strength * 5) + + # Field around the magnet + for i in range(-num_field_lines, num_field_lines+1, 2): + y_offset = i * 0.3 + + # Field lines from N to S pole (outside the magnet) + if abs(y_offset) > magnet_height/2: + x = np.linspace(pos_x, pos_x, 20) + y = np.linspace(magnet_height/2, -magnet_height/2, 20) + ax1.plot(x, y + y_offset, 'b-', linewidth=0.7, alpha=0.6) + + # Field lines extending outward (further out based on strength) + extent = 2 + strength * 3 + + # Top field lines (from North pole) + x_top = np.linspace(pos_x - extent, pos_x + extent, 20) + y_top = np.zeros_like(x_top) + y_curvature = 4 * strength * (1 - ((x_top - pos_x) / extent) ** 2) + ax1.plot(x_top, magnet_height/2 + y_curvature, 'b-', linewidth=0.7, alpha=0.6) + + # Bottom field lines (from South pole) + x_bottom = np.linspace(pos_x - extent, pos_x + extent, 20) + y_bottom = np.zeros_like(x_bottom) + y_curvature = 4 * strength * (1 - ((x_bottom - pos_x) / extent) ** 2) + ax1.plot(x_bottom, -magnet_height/2 - y_curvature, 'b-', linewidth=0.7, alpha=0.6) + + # Store coil points reference + coil_points = ax1.lines.copy() + + # Initialize magnetic field lines + plot_magnetic_field(magnet_pos, magnet_strength) + + # Add a current indicator around the coil + current_indicator = ax1.text(0, coil_radius + 1.5, "Current: 0 mA", + ha='center', fontsize=10, bbox=dict(facecolor='white', alpha=0.5)) + + # Bottom plot: Induced voltage over time + ax2 = fig.add_subplot(gs[1]) + ax2.set_xlim(0, 100) + ax2.set_ylim(-10, 10) + ax2.set_title('Induced Voltage vs. Time') + ax2.set_xlabel('Time') + ax2.set_ylabel('Voltage (mV)') + ax2.grid(True) + + # Initialize voltage data + voltage_data = np.zeros(100) + time_data = np.arange(100) + voltage_line, = ax2.plot(time_data, voltage_data, 'g-') + + # Function to calculate induced voltage based on parameters + def calculate_induced_voltage(pos, prev_pos, strength, turns, radius): + # Flux change calculation based on position change + area = np.pi * (radius ** 2) + + # Distance from coil center affects field strength (inverse square law approximation) + distance_factor = max(0.1, 1 / (1 + abs(pos) ** 2)) + + # Flux through coil + current_flux = strength * area * distance_factor + + # Flux from previous position + prev_distance_factor = max(0.1, 1 / (1 + abs(prev_pos) ** 2)) + previous_flux = strength * area * prev_distance_factor + + # Change in flux + d_flux = current_flux - previous_flux + + # Induced voltage (Faraday's law: E = -N * dΦ/dt) + # We approximate dt as 1 time unit + induced_voltage = -turns * d_flux + + # Add sign based on approach/retreat + if magnet_direction == "Moving In": + if pos < 0: # Approaching from left + induced_voltage *= -1 + elif magnet_direction == "Moving Out": + if pos > 0: # Moving away to the right + induced_voltage *= -1 + + return induced_voltage + + # Function to simulate current direction + def show_current_direction(voltage, ax, coil_r): + current = voltage / coil_resistance # I = V/R + + # Update current text + current_indicator.set_text(f"Current: {current*1000:.2f} mA") + + # Remove any existing current direction indicators + for artist in ax.artists: + if hasattr(artist, 'current_indicator'): + artist.remove() + + # If current is significant, show direction + if abs(current) > 0.01: + # Define arrow properties based on current direction + if current > 0: + color = 'g' + rotation = 90 # Clockwise + else: + color = 'r' + rotation = -90 # Counter-clockwise + + # Current magnitude affects arrow size + arrow_size = min(1.5, max(0.5, abs(current) * 10)) + + # Add arrow around the coil + arrow = patches.FancyArrowPatch((-coil_r, 0), (coil_r, 0), + connectionstyle=f"arc3,rad={rotation/180}", + arrowstyle=f"fancy,head_width={arrow_size},head_length={arrow_size*1.5}", + fc=color, ec=color, alpha=0.7) + arrow.current_indicator = True + arrow.set_transform(ax.transData) + + ax.add_patch(arrow) + + # Animation state variables + animation_state = { + 'magnet_pos': magnet_pos, + 'previous_pos': magnet_pos, + 'frame_count': 0 + } + + def update(): + magnet_pos = animation_state['magnet_pos'] + previous_pos = animation_state['previous_pos'] + frame_count = animation_state['frame_count'] + + # Update magnet position based on selected motion + if magnet_direction == "Moving In": + magnet_pos = min(0, magnet_pos + magnet_speed * 0.2) + elif magnet_direction == "Moving Out": + magnet_pos = min(9, magnet_pos + magnet_speed * 0.2) + else: # Oscillating + magnet_pos = 5 * np.sin(frame_count * 0.1 * magnet_speed) + + # Update magnet position + magnet.set_x(magnet_pos - magnet_width/2) + + # Update magnetic field + plot_magnetic_field(magnet_pos, magnet_strength) + + # Calculate induced voltage + induced_voltage = calculate_induced_voltage( + magnet_pos, previous_pos, magnet_strength, coil_turns, coil_radius/10) # Convert cm to dm + + # Scale for display + display_voltage = induced_voltage * 5 + + # Update voltage data + voltage_data[:-1] = voltage_data[1:] + voltage_data[-1] = display_voltage + voltage_line.set_ydata(voltage_data) + + # Update current indicator + show_current_direction(induced_voltage, ax1, coil_radius) + + # Update previous position for next frame + # Update state for next frame + animation_state['magnet_pos'] = magnet_pos + animation_state['previous_pos'] = magnet_pos + animation_state['frame_count'] = frame_count + 1 + # Convert plot to image + buf = BytesIO() + fig.tight_layout() + fig.savefig(buf, format='png', dpi=100) + buf.seek(0) + img_str = base64.b64encode(buf.read()).decode('utf-8') + + return f'data:image/png;base64,{img_str}' + + # Create a loop to update the plot repeatedly + while True: + img_data = update() + plot_placeholder.image(img_data, use_column_width=True) + # Sleep briefly to control animation speed + import time + time.sleep(0.1) + +# Additional explanations +st.markdown(""" +### How Electromagnetic Induction Works + +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. + +2. **Key Factors Affecting Induced Voltage**: + - Magnetic field strength: Stronger fields create greater induced voltages + - Number of coil turns: More turns create more induced voltage + - Rate of change: Faster movement of the magnet creates higher voltage + - Coil area: Larger coils intercept more magnetic flux + +3. **Lenz's Law**: The induced current creates its own magnetic field that opposes the change that produced it. + +Try moving the magnet at different speeds and directions to see how the induced voltage changes! +""") diff --git a/pages/electric-potential.py b/pages/electric-potential.py new file mode 100644 index 0000000000000000000000000000000000000000..68fb7ea7729f098758018789b227061fd415031a --- /dev/null +++ b/pages/electric-potential.py @@ -0,0 +1,49 @@ +import streamlit as st +import numpy as np +import plotly.graph_objects as go +from scipy.constants import k as coulomb_k + +# Title of the app +st.title("Electric Potential Visualization") + +# Sidebar for user inputs +st.sidebar.header("Charge Configuration") +num_charges = st.sidebar.selectbox("Number of charges", [1, 2, 3, 4, 5]) + +charges = [] +for i in range(num_charges): + st.sidebar.subheader(f"Charge {i+1}") + # Sliders for charge magnitude and position + 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}") + x = st.sidebar.slider(f"x_{i+1}", min_value=-5.0, max_value=5.0, value=0.0, key=f"x{i}") + y = st.sidebar.slider(f"y_{i+1}", min_value=-5.0, max_value=5.0, value=0.0, key=f"y{i}") + charges.append({'Q': Q, 'x': x, 'y': y}) + +# Define the 2D grid for calculation +x = np.linspace(-10, 10, 100) +y = np.linspace(-10, 10, 100) +X, Y = np.meshgrid(x, y) + +# Calculate total electric potential +V = np.zeros_like(X) +for charge in charges: + r = np.sqrt((X - charge['x'])**2 + (Y - charge['y'])**2) + # Avoid division by zero at charge location + r = np.where(r == 0, 1e-6, r) + V += coulomb_k * charge['Q'] / r + +# Create interactive Plotly contour plot +fig = go.Figure(data=go.Contour(x=x, y=y, z=V, colorscale='RdBu', colorbar=dict(title='Potential'))) + +# Add charge markers +for charge in charges: + color = 'red' if charge['Q'] > 0 else 'blue' + fig.add_trace(go.Scatter(x=[charge['x']], y=[charge['y']], mode='markers', + marker=dict(color=color, size=10))) + +# Customize plot layout +fig.update_layout(title='Electric Potential', xaxis_title='x', yaxis_title='y') +fig.update_yaxes(scaleanchor="x", scaleratio=1) # Equal aspect ratio + +# Display the plot in Streamlit +st.plotly_chart(fig) \ No newline at end of file diff --git a/pages/electromagnetc-waves.py b/pages/electromagnetc-waves.py new file mode 100644 index 0000000000000000000000000000000000000000..1bf1b462df51f85c8af14ae78d42f62f2e1df074 --- /dev/null +++ b/pages/electromagnetc-waves.py @@ -0,0 +1,42 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation + +# Set up the figure and axis +fig, ax = plt.subplots(figsize=(10, 6)) +ax.set_xlim(0, 4*np.pi) +ax.set_ylim(-2, 2) +ax.set_xlabel("Position (m)") +ax.set_ylabel("Field Strength") +ax.grid(True) + +# Initialize empty lines for E and B fields +e_line, = ax.plot([], [], lw=2, label='Electric Field (E)') +b_line, = ax.plot([], [], lw=2, label='Magnetic Field (B)') +ax.legend() + +# Widgets in sidebar +st.sidebar.header("Wave Parameters") +frequency = st.sidebar.slider("Frequency (Hz)", 1, 20, 5) +wavelength = st.sidebar.slider("Wavelength (m)", 1, 10, 5) +amplitude = st.sidebar.slider("Amplitude (V/m)", 0.1, 2.0, 1.0) + +# Animation function +def animate(i): + x = np.linspace(0, 4*np.pi, 1000) + phase = 2 * np.pi * frequency * (i/30) + + # Electric field (y-direction) + e = amplitude * np.sin((2*np.pi*x)/wavelength - phase) + + # Magnetic field (z-direction, scaled by 1/c) + b = (amplitude/3e8) * np.sin((2*np.pi*x)/wavelength - phase) + + e_line.set_data(x, e) + b_line.set_data(x, b) + return e_line, b_line + +# Create and display animation +ani = FuncAnimation(fig, animate, frames=100, interval=50, blit=True) +st.pyplot(fig) diff --git a/pages/electromagnetic-spectrum.py b/pages/electromagnetic-spectrum.py new file mode 100644 index 0000000000000000000000000000000000000000..41ef30040f39c4f4c40d61e1315797e40a6d8b39 --- /dev/null +++ b/pages/electromagnetic-spectrum.py @@ -0,0 +1,89 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import plotly.express as px + +# Constants +SPEED_OF_LIGHT = 3e8 # m/s + +# Configure app +st.set_page_config(page_title="EM Spectrum Visualizer", layout="wide") +st.title("Electromagnetic Spectrum Visualization") +st.markdown("Explore different regions of the electromagnetic spectrum") + +# Sidebar controls +with st.sidebar: + st.header("Configuration") + wavelength = st.slider("Wavelength (meters)", + 1e-12, 100.0, + value=5e-7, + format="%.1e") + + region = st.selectbox("Predefined Regions", [ + "Radio Waves", "Microwaves", "Infrared", + "Visible Light", "Ultraviolet", "X-Rays", "Gamma Rays" + ]) + +# Calculate frequency +frequency = SPEED_OF_LIGHT / wavelength + +# Main display +tab1, tab2 = st.tabs(["Wave Visualization", "Spectrum Diagram"]) + +with tab1: + # Create wave visualization + x = np.linspace(0, 4*np.pi, 1000) + y_electric = np.sin(x * wavelength * 100) # Scaled for visualization + y_magnetic = np.cos(x * wavelength * 100) + + fig, ax = plt.subplots(figsize=(10, 4)) + ax.plot(x, y_electric, label='Electric Field (E)') + ax.plot(x, y_magnetic, label='Magnetic Field (B)') + ax.set_title(f"EM Wave Visualization\nλ = {wavelength:.2e} m | ν = {frequency:.2e} Hz") + ax.set_xlabel("Propagation Direction") + ax.legend() + ax.grid(True) + st.pyplot(fig) + +with tab2: + # Create spectrum diagram + spectrum_data = { + "Region": ["Gamma", "X-Ray", "UV", "Visible", "IR", "Microwave", "Radio"], + "Start (m)": [1e-12, 1e-10, 4e-7, 4e-7, 7e-7, 1e-3, 1e-1], + "End (m)": [1e-10, 1e-8, 4e-7, 7e-7, 1e-3, 1e-1, 1e4], + "Color": ["purple", "blue", "violet", "#FF00FF", "red", "orange", "yellow"] + } + + fig = px.bar(spectrum_data, + x=["Gamma", "X-Ray", "UV", "Visible", "IR", "Microwave", "Radio"], + y=[1,1,1,1,1,1,1], + color="Region", + color_discrete_map={r: c for r,c in zip(spectrum_data["Region"], + spectrum_data["Color"])}, + orientation="h", + log_x=True, + height=400) + + fig.update_layout(title="Electromagnetic Spectrum Regions", + xaxis_title="Wavelength (meters)", + yaxis_visible=False, + showlegend=False) + st.plotly_chart(fig, use_container_width=True) + +# Region information +region_info = { + "Radio Waves": {"range": "1 mm - 100 km", "uses": "Communications, broadcasting"}, + "Microwaves": {"range": "1 mm - 1 m", "uses": "Radar, cooking"}, + "Infrared": {"range": "700 nm - 1 mm", "uses": "Thermal imaging"}, + "Visible Light": {"range": "400-700 nm", "uses": "Human vision, photography"}, + "Ultraviolet": {"range": "10-400 nm", "uses": "Sterilization, astronomy"}, + "X-Rays": {"range": "0.01-10 nm", "uses": "Medical imaging"}, + "Gamma Rays": {"range": "<0.01 nm", "uses": "Cancer treatment"} +} + +st.subheader(f"Region Information: {region}") +col1, col2 = st.columns(2) +with col1: + st.metric("Wavelength Range", region_info[region]["range"]) +with col2: + st.metric("Common Applications", region_info[region]["uses"]) diff --git a/pages/energy.py b/pages/energy.py new file mode 100644 index 0000000000000000000000000000000000000000..99a60ff8e3c51955bda8a0aa9757149b3858db97 --- /dev/null +++ b/pages/energy.py @@ -0,0 +1,235 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import plotly.graph_objs as go +import plotly.express as px + +class EnergyVisualization: + def __init__(self): + # Energy types and their descriptions + self.energy_types = { + "Kinetic Energy": { + "description": "Energy of motion. Depends on mass and velocity.", + "formula": "KE = 1/2 * m * v²", + "example": "A moving car, a rolling ball" + }, + "Potential Energy": { + "description": "Stored energy due to position or configuration.", + "subtypes": { + "Gravitational": "Energy due to height in a gravitational field.", + "Elastic": "Energy stored in stretched or compressed springs.", + "Chemical": "Energy stored in chemical bonds" + }, + "formula": "PE = m * g * h (for gravitational)", + "example": "A book on a high shelf, a stretched rubber band" + }, + "Thermal Energy": { + "description": "Energy associated with the motion of particles.", + "formula": "Q = m * c * ΔT", + "example": "Heat from a fire, warmth of your body" + }, + "Electrical Energy": { + "description": "Energy from electric charges and their movement.", + "formula": "E = V * Q", + "example": "Lightning, electricity in your home" + }, + "Chemical Energy": { + "description": "Energy stored in chemical bonds between atoms and molecules.", + "formula": "ΔH (Change in Enthalpy)", + "example": "Batteries, food, fuel" + } + } + + def display_energy_overview(self): + """Create an overview of energy types""" + st.title("Energy Visualization and Exploration") + + # Sidebar for energy type selection + selected_energy = st.sidebar.selectbox( + "Select Energy Type", + list(self.energy_types.keys()) + ) + + # Display details of selected energy type + energy_info = self.energy_types[selected_energy] + + st.header(f"{selected_energy}") + st.write(energy_info["description"]) + st.write(f"**Formula:** {energy_info['formula']}") + st.write(f"**Example:** {energy_info['example']}") + + # Check for subtypes + if "subtypes" in energy_info: + st.subheader("Subtypes") + for subtype, sub_desc in energy_info["subtypes"].items(): + st.write(f"- **{subtype}:** {sub_desc}") + + # Interactive visualization based on energy type + self._visualize_energy_type(selected_energy) + + def _visualize_energy_type(self, energy_type): + """Create interactive visualizations for different energy types""" + if energy_type == "Kinetic Energy": + self._kinetic_energy_visualization() + elif energy_type == "Potential Energy": + self._potential_energy_visualization() + elif energy_type == "Thermal Energy": + self._thermal_energy_visualization() + elif energy_type == "Electrical Energy": + self._electrical_energy_visualization() + elif energy_type == "Chemical Energy": + self._chemical_energy_visualization() + + def _kinetic_energy_visualization(self): + """Visualization for Kinetic Energy""" + st.subheader("Kinetic Energy Interactive Simulation") + + # Mass and velocity sliders + mass = st.slider("Mass (kg)", 1.0, 100.0, 10.0, 0.1) + velocity = st.slider("Velocity (m/s)", 0.0, 50.0, 10.0, 0.1) + + # Calculate Kinetic Energy + ke = 0.5 * mass * (velocity ** 2) + + # Plotly visualization + fig = go.Figure(data=[go.Bar( + x=['Kinetic Energy'], + y=[ke], + text=[f'{ke:.2f} J'], + textposition='auto' + )]) + fig.update_layout( + title='Kinetic Energy Calculation', + yaxis_title='Energy (Joules)' + ) + st.plotly_chart(fig) + + st.write(f"**Calculation:** KE = 1/2 * {mass} * {velocity}² = {ke:.2f} Joules") + + def _potential_energy_visualization(self): + """Visualization for Potential Energy""" + st.subheader("Gravitational Potential Energy Simulation") + + # Inputs for potential energy calculation + mass = st.slider("Mass (kg)", 1.0, 100.0, 10.0, 0.1) + height = st.slider("Height (m)", 0.0, 50.0, 10.0, 0.1) + g = 9.8 # acceleration due to gravity + + # Calculate Potential Energy + pe = mass * g * height + + # Create a simple visualization + fig = plt.figure(figsize=(8, 6)) + plt.title("Gravitational Potential Energy") + plt.bar(['Potential Energy'], [pe], color='green') + plt.ylabel('Energy (Joules)') + plt.text(0, pe, f'{pe:.2f} J', ha='center', va='bottom') + st.pyplot(fig) + + st.write(f"**Calculation:** PE = {mass} * {g} * {height} = {pe:.2f} Joules") + + def _thermal_energy_visualization(self): + """Visualization for Thermal Energy""" + st.subheader("Thermal Energy Temperature Simulation") + + # Inputs for thermal energy + mass = st.slider("Mass (kg)", 0.1, 10.0, 1.0, 0.1) + specific_heat = st.slider("Specific Heat Capacity (J/kg·K)", 100, 5000, 1000, 10) + delta_temp = st.slider("Temperature Change (°C)", -100, 100, 20, 1) + + # Calculate Thermal Energy + thermal_energy = mass * specific_heat * delta_temp + + # Plotly heat map visualization + temps = np.linspace(-100, 100, 100) + heat_map = [mass * specific_heat * t for t in temps] + + fig = go.Figure(data=[go.Scatter( + x=temps, + y=heat_map, + mode='lines', + name='Thermal Energy' + )]) + fig.update_layout( + title='Thermal Energy vs Temperature', + xaxis_title='Temperature Change (°C)', + yaxis_title='Thermal Energy (Joules)' + ) + st.plotly_chart(fig) + + st.write(f"**Calculation:** Q = {mass} * {specific_heat} * {delta_temp} = {thermal_energy:.2f} Joules") + + def _electrical_energy_visualization(self): + """Visualization for Electrical Energy""" + st.subheader("Electrical Energy Simulation") + + # Inputs for electrical energy + voltage = st.slider("Voltage (V)", 1, 240, 120, 1) + charge = st.slider("Charge (Coulombs)", 0.1, 10.0, 1.0, 0.1) + + # Calculate Electrical Energy + electrical_energy = voltage * charge + + # Plotly bar chart + fig = go.Figure(data=[go.Bar( + x=['Electrical Energy'], + y=[electrical_energy], + text=[f'{electrical_energy:.2f} J'], + textposition='auto' + )]) + fig.update_layout( + title='Electrical Energy Calculation', + yaxis_title='Energy (Joules)' + ) + st.plotly_chart(fig) + + st.write(f"**Calculation:** E = {voltage} * {charge} = {electrical_energy:.2f} Joules") + + def _chemical_energy_visualization(self): + """Visualization for Chemical Energy""" + st.subheader("Chemical Energy Bond Simulation") + + # Chemical bond energy types + bond_energies = { + "Hydrogen Bond": 20, + "Ionic Bond": 700, + "Covalent Bond": 350, + "Metallic Bond": 250 + } + + # Multiselect for bond types + selected_bonds = st.multiselect( + "Select Bond Types", + list(bond_energies.keys()), + default=["Covalent Bond"] + ) + + # Calculate total bond energy + total_bond_energy = sum(bond_energies[bond] for bond in selected_bonds) + + # Pie chart visualization + fig = go.Figure(data=[go.Pie( + labels=selected_bonds, + values=[bond_energies[bond] for bond in selected_bonds], + hole=.3 + )]) + fig.update_layout(title='Chemical Bond Energy Distribution') + st.plotly_chart(fig) + + st.write("**Bond Energies:**") + for bond in selected_bonds: + st.write(f"- {bond}: {bond_energies[bond]} kJ/mol") + st.write(f"**Total Bond Energy:** {total_bond_energy} kJ/mol") + +def main(): + # Initialize and run the Energy Visualization + energy_viz = EnergyVisualization() + energy_viz.display_energy_overview() + +if __name__ == "__main__": + main() + +# Requirements for this Streamlit app +# Run this in terminal: +# pip install streamlit numpy matplotlib plotly +# streamlit run energy_visualization.py \ No newline at end of file diff --git a/pages/farady-law.py b/pages/farady-law.py new file mode 100644 index 0000000000000000000000000000000000000000..a869243659702c0c7e40899cf22a69d72f247487 --- /dev/null +++ b/pages/farady-law.py @@ -0,0 +1,116 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D + +# Set up the page +st.set_page_config(page_title="Faraday's Law Visualization", layout="wide") +st.title("Visualization of Faraday's Law of Electromagnetic Induction") + +# Theory section +st.markdown(""" +## Faraday's Law +Faraday's Law states that the induced electromotive force (EMF) in a closed circuit is equal to: +""") +st.latex(r'''\varepsilon = -N \frac{d\Phi_B}{dt}''') +st.markdown(""" +Where: +- ε = Induced EMF (Volts) +- N = Number of turns in coil +- Φ_B = Magnetic flux (Weber) +- t = Time (seconds) + +Magnetic flux is given by: +""") +st.latex(r'''\Phi_B = B \cdot A \cdot \cos(\theta)''') + +# Sidebar controls +st.sidebar.header("Simulation Parameters") +B0 = st.sidebar.slider("Peak Magnetic Field (T)", 0.1, 10.0, 2.0) +N = st.sidebar.slider("Number of Turns", 1, 100, 50) +radius = st.sidebar.slider("Coil Radius (m)", 0.1, 2.0, 0.5) +theta_deg = st.sidebar.slider("Angle between B and Normal (degrees)", 0, 180, 45) +freq = st.sidebar.slider("Oscillation Frequency (Hz)", 0.1, 10.0, 1.0) + +# Convert angle to radians +theta = np.deg2rad(theta_deg) + +# Time array +t = np.linspace(0, 2, 1000) # 2 seconds simulation + +# Calculations +B = B0 * np.sin(2 * np.pi * freq * t) # Time-varying magnetic field +A = np.pi * radius**2 # Coil area +flux = N * B * A * np.cos(theta) # Magnetic flux +emf = -np.gradient(flux, t) # Induced EMF + +# Create figure for plots +fig = plt.figure(figsize=(12, 10)) + +# Magnetic field plot +ax1 = plt.subplot(311) +ax1.plot(t, B, color='tab:red') +ax1.set_ylabel('Magnetic Field (T)') +ax1.set_title('Time-varying Magnetic Field') +ax1.grid(True) + +# Magnetic flux plot +ax2 = plt.subplot(312) +ax2.plot(t, flux, color='tab:green') +ax2.set_ylabel('Magnetic Flux (Wb)') +ax2.set_title('Magnetic Flux Through Coil') +ax2.grid(True) + +# Induced EMF plot +ax3 = plt.subplot(313) +ax3.plot(t, emf, color='tab:blue') +ax3.set_ylabel('Induced EMF (V)') +ax3.set_xlabel('Time (s)') +ax3.set_title('Induced Electromotive Force') +ax3.grid(True) + +plt.tight_layout() +st.pyplot(fig) + +# 3D Visualization +st.markdown("### 3D Visualization of Coil Orientation") + +fig3d = plt.figure(figsize=(8, 8)) +ax3d = fig3d.add_subplot(111, projection='3d') + +# Generate coil coordinates +theta_coil = np.linspace(0, 2*np.pi, 100) +x = radius * np.cos(theta_coil) +y = radius * np.sin(theta_coil) * np.cos(theta) +z = radius * np.sin(theta_coil) * np.sin(theta) + +# Plot coil +ax3d.plot(x, y, z, color='blue', linewidth=2, label='Coil') + +# Plot magnetic field direction +ax3d.quiver(0, 0, -1, 0, 0, 2, color='red', + linewidth=3, arrow_length_ratio=0.1, label='Magnetic Field (B)') + +# Set plot limits and labels +ax3d.set_xlim([-radius*1.5, radius*1.5]) +ax3d.set_ylim([-radius*1.5, radius*1.5]) +ax3d.set_zlim([-radius*1.5, radius*1.5]) +ax3d.set_xlabel('X-axis') +ax3d.set_ylabel('Y-axis') +ax3d.set_zlabel('Z-axis') +ax3d.set_title('Coil Orientation Relative to Magnetic Field') +ax3d.legend() + +st.pyplot(fig3d) + +# Explanation +st.markdown(""" +## Explanation +1. **Magnetic Field (Red Plot):** Shows the sinusoidal variation of the external magnetic field +2. **Magnetic Flux (Green Plot):** Demonstrates the flux through the coil depending on angle θ +3. **Induced EMF (Blue Plot):** Shows the EMF generated by the changing magnetic flux + +The 3D visualization shows: +- Blue coil in its orientation relative to the magnetic field (red arrow) +- The angle θ between the coil's normal vector and magnetic field direction +""") \ No newline at end of file diff --git a/pages/field-concepts.py b/pages/field-concepts.py new file mode 100644 index 0000000000000000000000000000000000000000..693a16570b08b80975fefeda6e93fdc411d5c5a7 --- /dev/null +++ b/pages/field-concepts.py @@ -0,0 +1,131 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Introduction and instructions +st.markdown(""" +# Electric Field Visualization + +This app helps you visualize the electric field created by point charges in a 2D space. + +### How to Use +- In the sidebar, enter the charges in the format `x,y,Q` (one per line), where: + - `x` and `y` are the coordinates of the charge. + - `Q` is the charge value (positive or negative). +- Example: `0,0,1` places a positive charge of 1 at the origin, and `3,0,-1` places a negative charge at (3,0). +- Select the visualization type: + - **Quiver Plot**: Shows arrows representing the direction and strength of the electric field. + - **Field Lines**: Displays lines that follow the electric field's direction. + +Try starting with `0,0,1` and `3,0,-1` to see a dipole field! +""") + +# Sidebar inputs +charges_input = st.sidebar.text_area( + "Enter charges (x,y,Q) one per line", + "0,0,1\n3,0,-1" +) +vis_type = st.sidebar.radio("Visualization Type", ["Quiver Plot", "Field Lines"]) + +# Parse the input charges +charges = [] +for line in charges_input.split('\n'): + if line.strip(): + try: + x, y, Q = map(float, line.split(',')) + charges.append((x, y, Q)) + except ValueError: + st.sidebar.error(f"Invalid input: {line}. Use format x,y,Q.") + st.stop() + +# Check if there are any charges +if not charges: + st.warning("No charges entered. Please add charges in the sidebar.") + st.stop() + +# Function to compute the electric field at a point (x, y) +def electric_field(x, y, charges): + Ex, Ey = 0, 0 + for cx, cy, Q in charges: + dx = x - cx + dy = y - cy + r2 = dx**2 + dy**2 + # Avoid division by zero (when point is exactly at a charge) + if r2 < 1e-10: + continue + r3 = r2**1.5 + Ex += Q * dx / r3 + Ey += Q * dy / r3 + return Ex, Ey + +# Set up the plot +fig, ax = plt.subplots(figsize=(8, 8)) + +# Define the grid for quiver plot +x = np.arange(-10, 11, 1) +y = np.arange(-10, 11, 1) +X, Y = np.meshgrid(x, y) + +if vis_type == "Quiver Plot": + # Compute electric field across the grid + Ex = np.zeros_like(X, dtype=float) + Ey = np.zeros_like(Y, dtype=float) + for i in range(X.shape[0]): + for j in range(X.shape[1]): + Ex[i, j], Ey[i, j] = electric_field(X[i, j], Y[i, j], charges) + + # Calculate field magnitude for coloring + M = np.sqrt(Ex**2 + Ey**2) + + # Plot quiver with magnitude-based coloring + quiv = ax.quiver(X, Y, Ex, Ey, M, cmap='viridis', scale=50) + fig.colorbar(quiv, ax=ax, label='Field Magnitude') + +else: # Field Lines + # Generate starting points around each charge + starting_points = [] + for cx, cy, Q in charges: + # 8 points around each charge at a small radius + for angle in np.linspace(0, 2*np.pi, 8, endpoint=False): + x0 = cx + 0.1 * np.cos(angle) + y0 = cy + 0.1 * np.sin(angle) + starting_points.append((x0, y0)) + + # Trace field lines from each starting point + for x0, y0 in starting_points: + path = [(x0, y0)] + for _ in range(100): # Maximum steps + Ex, Ey = electric_field(x0, y0, charges) + mag = np.sqrt(Ex**2 + Ey**2) + if mag < 1e-5: # Stop if field is too weak + break + # Normalize step size + step_x = 0.1 * Ex / mag + step_y = 0.1 * Ey / mag + x0 += step_x + y0 += step_y + # Stop if outside the plot boundaries + if x0 < -10 or x0 > 10 or y0 < -10 or y0 > 10: + break + path.append((x0, y0)) + + # Plot the field line + px, py = zip(*path) + ax.plot(px, py, 'k-', linewidth=1) + +# Plot the charges +for cx, cy, Q in charges: + color = 'red' if Q > 0 else 'blue' + ax.plot(cx, cy, 'o', color=color, markersize=10, label=f'Q={Q}') + +# Customize the plot +ax.set_xlim(-10, 10) +ax.set_ylim(-10, 10) +ax.set_aspect('equal') +ax.set_xlabel('x') +ax.set_ylabel('y') +ax.set_title(f'Electric Field - {vis_type}') +ax.grid(True) + +# Display the plot in Streamlit +st.pyplot(fig) \ No newline at end of file diff --git a/pages/first-law-of-thermodynamics.py b/pages/first-law-of-thermodynamics.py new file mode 100644 index 0000000000000000000000000000000000000000..da0e4294fb746674c1881f02712976db1b1a496e --- /dev/null +++ b/pages/first-law-of-thermodynamics.py @@ -0,0 +1,52 @@ +import streamlit as st +import matplotlib.pyplot as plt + +def main(): + st.title("First Law of Thermodynamics Simulation") + st.write(""" + The First Law of Thermodynamics is essentially an expression of energy conservation. + It states that the change in internal energy (ΔU) of a system equals the heat (Q) added + to the system minus the work (W) done by the system: + + **ΔU = Q - W** + + Use the sliders below to adjust the heat added and the work done. + """) + + # User input for heat (Q) and work (W) + Q = st.slider("Heat added (Q) [Joules]", min_value=-200, max_value=200, value=50, step=1) + W = st.slider("Work done (W) [Joules]", min_value=-200, max_value=200, value=30, step=1) + + # Calculate the change in internal energy + delta_U = Q - W + st.write(f"**Calculated change in internal energy (ΔU):** {delta_U} Joules") + + # Create a bar chart to visualize the energy contributions + labels = ['Heat (Q)', 'Work (W)', 'ΔU'] + values = [Q, W, delta_U] + colors = ['#69b3a2', '#404080', '#e377c2'] + + fig, ax = plt.subplots() + bars = ax.bar(labels, values, color=colors) + ax.axhline(0, color='black', linewidth=0.8) + ax.set_ylabel("Energy (Joules)") + ax.set_title("Energy Balance: Q, W, and ΔU") + for bar in bars: + yval = bar.get_height() + # Position the text slightly above the bar if positive, or below if negative. + offset = 3 if yval >= 0 else -15 + ax.text(bar.get_x() + bar.get_width()/2.0, yval + offset, f'{yval}', + ha='center', color='black', fontsize=10) + st.pyplot(fig) + + st.write(""" + **Explanation:** + - **Q (Heat added):** The energy put into the system via heating. + - **W (Work done):** The energy used by the system to perform work (like expanding against a piston). + - **ΔU:** The resulting change in the system's internal energy. + + Notice that increasing Q or decreasing W (or both) will result in a higher ΔU, showing that more energy remains stored within the system. + """) + +if __name__ == '__main__': + main() diff --git a/pages/fluid-dynamics.py b/pages/fluid-dynamics.py new file mode 100644 index 0000000000000000000000000000000000000000..ca9d678936f66137f9b06ecc493658054fe09091 --- /dev/null +++ b/pages/fluid-dynamics.py @@ -0,0 +1,46 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +# Sidebar parameters for simulation control +num_particles = st.sidebar.number_input("Number of particles", min_value=10, max_value=500, value=100) +dt = st.sidebar.slider("Time step (dt)", min_value=0.01, max_value=1.0, value=0.1) +num_steps = st.sidebar.slider("Number of simulation steps", min_value=10, max_value=500, value=200) +refresh_rate = st.sidebar.slider("Refresh rate (seconds)", min_value=0.01, max_value=0.5, value=0.1) + +# Initialize particle positions randomly within a region +positions = np.random.uniform(-5, 5, (num_particles, 2)) + +def velocity_field(x, y): + """ + Defines a vortex velocity field centered at the origin. + u = -y, v = x gives a counter-clockwise rotation. + """ + u = -y + v = x + return u, v + +st.title("Fluid Dynamics Simulation: Particle Advection in a Vortex") + +if st.button("Start Simulation"): + placeholder = st.empty() # container to update the plot + for step in range(num_steps): + # Compute the velocity for all particles at their current positions + u, v = velocity_field(positions[:, 0], positions[:, 1]) + # Update particle positions using Euler integration + positions[:, 0] += u * dt + positions[:, 1] += v * dt + + # Create a plot of the current particle positions + fig, ax = plt.subplots(figsize=(6, 6)) + ax.scatter(positions[:, 0], positions[:, 1], color='blue', s=10) + ax.set_xlim(-10, 10) + ax.set_ylim(-10, 10) + ax.set_title(f"Step {step + 1}") + ax.set_xlabel("X") + ax.set_ylabel("Y") + + # Update the plot in the Streamlit app + placeholder.pyplot(fig) + time.sleep(refresh_rate) diff --git a/pages/fluid-statics.py b/pages/fluid-statics.py new file mode 100644 index 0000000000000000000000000000000000000000..e7e91520dd55546c1e05588b6574127aae0cd5d7 --- /dev/null +++ b/pages/fluid-statics.py @@ -0,0 +1,45 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +st.title("Fluid Statics Simulation") +st.markdown(""" +This simulation visualizes how pressure varies in a fluid column at rest. +According to fluid statics, the pressure increases with depth according to: +\[ P = P_0 + \rho g h \] +where: +- \(P_0\) is the surface pressure, +- \(\rho\) is the fluid density, +- \(g\) is the gravitational acceleration, +- \(h\) is the depth. +""") + +# User input for simulation parameters +density = st.slider("Fluid Density (kg/m³)", min_value=500, max_value=2000, value=1000, step=50) +gravity = st.slider("Gravitational Acceleration (m/s²)", min_value=1.0, max_value=20.0, value=9.81, step=0.1) +column_height = st.slider("Fluid Column Height (m)", min_value=1.0, max_value=100.0, value=10.0, step=1.0) +P0 = st.slider("Surface Pressure (Pa)", min_value=0, max_value=200000, value=101325, step=1000) + +# Calculate pressures at various depths +depths = np.linspace(0, column_height, 200) +pressures = P0 + density * gravity * depths + +# Plot Pressure vs. Depth +fig, ax = plt.subplots(figsize=(6, 4)) +ax.plot(pressures, depths, color="blue") +ax.set_xlabel("Pressure (Pa)") +ax.set_ylabel("Depth (m)") +ax.set_title("Pressure Variation with Depth") +ax.invert_yaxis() # so that the plot displays depth increasing downward +st.pyplot(fig) + +# Visualize the fluid column using a color gradient +fig2, ax2 = plt.subplots(figsize=(2, 6)) +# Create a vertical gradient image +gradient = np.linspace(0, 1, 256) +gradient = np.vstack((gradient, gradient)) +ax2.imshow(gradient, extent=[0, 1, 0, column_height], aspect='auto', cmap='Blues') +ax2.set_title("Fluid Column (Pressure Gradient)") +ax2.set_ylabel("Depth (m)") +ax2.set_xticks([]) +st.pyplot(fig2) diff --git a/pages/force.py b/pages/force.py new file mode 100644 index 0000000000000000000000000000000000000000..92a8f6451446f0cb5975c1485df5fe341b983a45 --- /dev/null +++ b/pages/force.py @@ -0,0 +1,131 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches + +def calculate_resultant_force(forces): + """ + Calculate the resultant force vector from multiple forces. + + Args: + forces (list): List of tuples (magnitude, angle in degrees) + + Returns: + tuple: Resultant force magnitude and angle + """ + # Convert forces to x and y components + fx = sum(force[0] * np.cos(np.radians(force[1])) for force in forces) + fy = sum(force[0] * np.sin(np.radians(force[1])) for force in forces) + + # Calculate resultant magnitude and angle + resultant_magnitude = np.sqrt(fx**2 + fy**2) + resultant_angle = np.degrees(np.arctan2(fy, fx)) + + return resultant_magnitude, resultant_angle + +def draw_force_diagram(forces): + """ + Create a force diagram using matplotlib. + + Args: + forces (list): List of tuples (magnitude, angle in degrees) + + Returns: + matplotlib figure + """ + fig, ax = plt.subplots(figsize=(8, 8)) + ax.set_xlim(-10, 10) + ax.set_ylim(-10, 10) + ax.grid(True, linestyle='--', alpha=0.7) + ax.set_title('Force Diagram') + ax.set_xlabel('X-axis') + ax.set_ylabel('Y-axis') + + # Draw origin point + ax.plot(0, 0, 'ro', markersize=10) + + # Colors for different forces + colors = ['blue', 'green', 'red', 'purple', 'orange'] + + # Draw individual forces + for i, (magnitude, angle) in enumerate(forces): + color = colors[i % len(colors)] + # Calculate x and y components + fx = magnitude * np.cos(np.radians(angle)) + fy = magnitude * np.sin(np.radians(angle)) + + # Draw force vector + ax.arrow(0, 0, fx, fy, head_width=0.3, head_length=0.5, + fc=color, ec=color, alpha=0.7, + label=f'Force {i+1}: {magnitude} N at {angle}°') + + # Calculate and draw resultant force + resultant_magnitude, resultant_angle = calculate_resultant_force(forces) + ax.arrow(0, 0, + resultant_magnitude * np.cos(np.radians(resultant_angle)), + resultant_magnitude * np.sin(np.radians(resultant_angle)), + head_width=0.5, head_length=0.8, + fc='black', ec='black', alpha=1, + label=f'Resultant: {resultant_magnitude:.2f} N at {resultant_angle:.2f}°') + + ax.legend() + return fig + +def main(): + st.title('Force Visualization Simulator') + + st.sidebar.header('Force Input') + + # Initialize session state for forces + if 'forces' not in st.session_state: + st.session_state.forces = [] + + # Force input + magnitude = st.sidebar.number_input('Force Magnitude (N)', min_value=0.0, step=0.1, value=5.0) + angle = st.sidebar.number_input('Force Angle (degrees)', min_value=0.0, max_value=360.0, step=1.0, value=0.0) + + # Add force button + if st.sidebar.button('Add Force'): + st.session_state.forces.append((magnitude, angle)) + + # Remove force button + if st.sidebar.button('Remove Last Force') and st.session_state.forces: + st.session_state.forces.pop() + + # Clear all forces + if st.sidebar.button('Clear All Forces'): + st.session_state.forces = [] + + # Display current forces + st.sidebar.subheader('Current Forces:') + for i, (mag, ang) in enumerate(st.session_state.forces, 1): + st.sidebar.text(f'Force {i}: {mag} N at {ang}°') + + # Visualization + if st.session_state.forces: + fig = draw_force_diagram(st.session_state.forces) + st.pyplot(fig) + + # Resultant force calculation + resultant_magnitude, resultant_angle = calculate_resultant_force(st.session_state.forces) + st.subheader('Resultant Force') + st.write(f'Magnitude: {resultant_magnitude:.2f} N') + st.write(f'Angle: {resultant_angle:.2f}°') + else: + st.info('Add forces to visualize the force diagram') + + # Physics explanation + st.sidebar.markdown(""" + ### How to Use + 1. Enter force magnitude and angle + 2. Click 'Add Force' to include in diagram + 3. Visualize vector forces and resultant + + ### Force Visualization Concepts + - **Magnitude**: Strength of the force (Newtons) + - **Angle**: Direction of the force (0-360 degrees) + - **Resultant Force**: Combined effect of all forces + """) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pages/free-fall.py b/pages/free-fall.py new file mode 100644 index 0000000000000000000000000000000000000000..b88c94eabd4bee7893be777644dad53ba6bd49c6 --- /dev/null +++ b/pages/free-fall.py @@ -0,0 +1,140 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +def calculate_free_fall(initial_height, gravity=9.8): + """ + Calculate free fall motion parameters + + Args: + initial_height (float): Starting height in meters + gravity (float): Acceleration due to gravity (default 9.8 m/s²) + + Returns: + tuple: Time of fall, position array, velocity array + """ + # Calculate time to hit ground + time_to_ground = np.sqrt(2 * initial_height / gravity) + + # Create time array + time = np.linspace(0, time_to_ground, 100) + + # Calculate position and velocity + position = initial_height - 0.5 * gravity * time**2 + velocity = -gravity * time + + return time, position, velocity + +def plot_free_fall(time, position, velocity): + """ + Create plots for free fall motion + + Args: + time (array): Time array + position (array): Position array + velocity (array): Velocity array + + Returns: + matplotlib figure + """ + # Create figure and axis + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 10), + gridspec_kw={'height_ratios': [2, 1]}) + + # Position plot + ax1.plot(time, position, 'b-') + ax1.scatter(time[-1], position[-1], color='red', s=100) + ax1.set_title('Free Fall - Position vs Time') + ax1.set_xlabel('Time (s)') + ax1.set_ylabel('Height (m)') + ax1.grid(True, linestyle='--', alpha=0.7) + + # Velocity plot + ax2.plot(time, velocity, 'g-') + ax2.scatter(time[-1], velocity[-1], color='red', s=100) + ax2.set_title('Free Fall - Velocity vs Time') + ax2.set_xlabel('Time (s)') + ax2.set_ylabel('Velocity (m/s)') + ax2.grid(True, linestyle='--', alpha=0.7) + + plt.tight_layout() + return fig + +def main(): + """ + Streamlit app for Free Fall Simulation + """ + st.title('Free Fall Motion Simulator') + + # Sidebar for inputs + st.sidebar.header('Simulation Parameters') + + # Initial height input + initial_height = st.sidebar.slider( + 'Initial Height (m)', + min_value=1.0, + max_value=100.0, + value=10.0, + step=0.5 + ) + + # Gravity selection (optional) + gravity_options = [ + ('Earth (9.8 m/s²)', 9.8), + ('Moon (1.6 m/s²)', 1.6), + ('Mars (3.7 m/s²)', 3.7), + ] + gravity = st.sidebar.selectbox( + 'Planet/Location', + gravity_options, + format_func=lambda x: x[0] + )[1] + + # Calculate free fall motion + time, position, velocity = calculate_free_fall(initial_height, gravity) + + # Create columns for information display + col1, col2 = st.columns(2) + + with col1: + st.metric('Initial Height', f'{initial_height} m') + st.metric('Time to Ground', f'{time[-1]:.2f} s') + + with col2: + st.metric('Gravity', f'{gravity} m/s²') + st.metric('Final Velocity', f'{velocity[-1]:.2f} m/s') + + # Create and display plots + st.header('Motion Visualization') + fig = plot_free_fall(time, position, velocity) + + # Display matplotlib figure + st.pyplot(fig) + + # Detailed explanation + st.markdown(""" + ### Physics Behind Free Fall + + In free fall motion: + - The object falls under the influence of gravity + - Acceleration is constant (9.8 m/s² on Earth) + - Air resistance is neglected + - Position follows quadratic equation: + $h(t) = h_0 - \\frac{1}{2}gt^2$ + - Velocity follows linear equation: + $v(t) = -gt$ + + ### How to Interpret the Graphs + - Top Graph: Shows object's height decreasing over time + - Bottom Graph: Shows velocity increasing in magnitude + (becoming more negative) as the object falls + - Red dot indicates final position/velocity + """) + +if __name__ == '__main__': + main() + +# Installation requirements +# Run in terminal: +# pip install streamlit numpy matplotlib +# streamlit run free_fall_simulation.py \ No newline at end of file diff --git a/pages/friction.py b/pages/friction.py new file mode 100644 index 0000000000000000000000000000000000000000..1321e07425a5ea0338ece10cb7cb748808000568 --- /dev/null +++ b/pages/friction.py @@ -0,0 +1,115 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sns + +class FrictionSimulation: + def __init__(self): + # Constants for simulation + self.g = 9.8 # Acceleration due to gravity (m/s^2) + self.mass = 10.0 # Mass of the object (kg) + self.angle = 0.0 # Angle of the inclined plane (degrees) + self.coefficient_static = 0.5 # Static friction coefficient + self.coefficient_kinetic = 0.3 # Kinetic friction coefficient + + def calculate_forces(self): + """ + Calculate forces acting on an object on an inclined plane + """ + # Convert angle to radians + theta = np.deg2rad(self.angle) + + # Normal force + normal_force = self.mass * self.g * np.cos(theta) + + # Weight components + weight_parallel = self.mass * self.g * np.sin(theta) + weight_perpendicular = self.mass * self.g * np.cos(theta) + + # Maximum static friction + max_static_friction = self.coefficient_static * normal_force + + # Kinetic friction + kinetic_friction = self.coefficient_kinetic * normal_force + + return { + 'Normal Force': normal_force, + 'Weight Parallel': weight_parallel, + 'Weight Perpendicular': weight_perpendicular, + 'Max Static Friction': max_static_friction, + 'Kinetic Friction': kinetic_friction + } + + def plot_force_diagram(self, forces): + """ + Create a force diagram visualization + """ + plt.figure(figsize=(10, 6)) + + # Create a bar plot of forces + force_names = list(forces.keys()) + force_values = list(forces.values()) + + plt.bar(force_names, force_values) + plt.title('Forces Acting on Object') + plt.xlabel('Force Types') + plt.ylabel('Force Magnitude (N)') + plt.xticks(rotation=45, ha='right') + plt.tight_layout() + + return plt + +def main(): + st.title('Interactive Friction Simulation') + + # Sidebar for inputs + st.sidebar.header('Simulation Parameters') + + # Create simulation instance + sim = FrictionSimulation() + + # Mass input + sim.mass = st.sidebar.slider('Object Mass (kg)', 1.0, 50.0, 10.0, 0.5) + + # Angle input + sim.angle = st.sidebar.slider('Incline Angle (degrees)', 0, 90, 30, 1) + + # Friction coefficients + sim.coefficient_static = st.sidebar.slider('Static Friction Coefficient', 0.0, 1.0, 0.5, 0.01) + sim.coefficient_kinetic = st.sidebar.slider('Kinetic Friction Coefficient', 0.0, 1.0, 0.3, 0.01) + + # Calculate forces + forces = sim.calculate_forces() + + # Display force calculations + st.header('Force Calculations') + force_df = pd.DataFrame.from_dict(forces, orient='index', columns=['Force (N)']) + st.dataframe(force_df) + + # Visualization of forces + st.header('Force Diagram') + fig = sim.plot_force_diagram(forces) + st.pyplot(fig) + + # Explanation section + st.header('Friction Explanation') + st.markdown(""" + ### Understanding Friction on an Inclined Plane + + - **Normal Force**: The force perpendicular to the surface, supporting the object's weight + - **Weight Parallel**: Component of weight pushing the object down the slope + - **Max Static Friction**: Maximum friction force that prevents object from sliding + - **Kinetic Friction**: Friction force when the object is moving + + #### When will the object start sliding? + - If the parallel component of weight exceeds maximum static friction + - The critical angle can be calculated using the static friction coefficient + """) + + # Critical angle calculation + critical_angle = np.arctan(sim.coefficient_static) * 180 / np.pi + st.info(f"Critical Angle: {critical_angle:.2f} degrees") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pages/general-relativity.py b/pages/general-relativity.py new file mode 100644 index 0000000000000000000000000000000000000000..6aec2a39c146d586500968d76ae08d09e4d13c47 --- /dev/null +++ b/pages/general-relativity.py @@ -0,0 +1,56 @@ +import streamlit as st +import numpy as np +import plotly.graph_objects as go + +def generate_surface(mass, grid_size=100, range_val=10): + """ + Generates a grid and calculates a "curvature" value for each point, + using a simplified gravitational potential analogy: + + Z = - mass / sqrt(x^2 + y^2 + epsilon) + + The epsilon is added to avoid singularity at (0,0). + """ + x = np.linspace(-range_val, range_val, grid_size) + y = np.linspace(-range_val, range_val, grid_size) + X, Y = np.meshgrid(x, y) + epsilon = 0.1 # small constant to prevent division by zero + Z = -mass / np.sqrt(X**2 + Y**2 + epsilon) + return X, Y, Z + +def main(): + st.title("General Relativity Visualization") + st.write(""" + This simulation visualizes a simplified model of space-time curvature due to a massive object. + Although general relativity is much more complex, the following "rubber-sheet" analogy + helps illustrate how mass can curve space. + """) + + # Sidebar controls for interactivity + mass = st.sidebar.slider("Mass", min_value=1.0, max_value=10.0, value=5.0, step=0.5, + help="Increase mass to deepen the gravitational well.") + grid_size = st.sidebar.slider("Grid Resolution", min_value=50, max_value=200, value=100, step=10, + help="Adjust the grid resolution of the visualization.") + range_val = st.sidebar.slider("Range", min_value=5, max_value=20, value=10, step=1, + help="Set the spatial range for the grid (in arbitrary units).") + + # Generate the grid and curvature data + X, Y, Z = generate_surface(mass, grid_size, range_val) + + # Create a 3D surface plot using Plotly + fig = go.Figure(data=[go.Surface(x=X, y=Y, z=Z, colorscale="Viridis")]) + fig.update_layout( + title="Space-time Curvature", + scene=dict( + xaxis_title="X", + yaxis_title="Y", + zaxis_title="Curvature (analogy)", + aspectratio=dict(x=1, y=1, z=0.5) + ), + autosize=True + ) + + st.plotly_chart(fig, use_container_width=True) + +if __name__ == "__main__": + main() diff --git a/pages/gravity.py b/pages/gravity.py new file mode 100644 index 0000000000000000000000000000000000000000..707d54e887002c01b793e71ef543f2f6cf2bda55 --- /dev/null +++ b/pages/gravity.py @@ -0,0 +1,161 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation +import matplotlib.animation as animation + +class GravitySimulation: + def __init__(self, num_bodies=3, width=10, height=10): + """ + Initialize the gravity simulation + + Parameters: + ----------- + num_bodies : int, optional + Number of bodies in the simulation (default is 3) + width : float, optional + Width of the simulation space (default is 10) + height : float, optional + Height of the simulation space (default is 10) + """ + self.num_bodies = num_bodies + self.width = width + self.height = height + + # Gravitational constant (scaled for visualization) + self.G = 1.0 + + # Initialize positions, velocities, and masses + self.positions = np.random.rand(num_bodies, 2) * np.array([width, height]) + self.velocities = np.random.randn(num_bodies, 2) * 0.1 + self.masses = np.random.uniform(0.5, 2, num_bodies) + + def compute_accelerations(self): + """ + Compute gravitational accelerations between bodies + + Returns: + -------- + numpy.ndarray + Accelerations for each body + """ + accelerations = np.zeros_like(self.positions) + + for i in range(self.num_bodies): + for j in range(self.num_bodies): + if i != j: + # Vector from body i to body j + r_vector = self.positions[j] - self.positions[i] + + # Distance between bodies + r_magnitude = np.linalg.norm(r_vector) + + # Avoid division by zero + if r_magnitude > 0: + # Gravitational acceleration + acceleration_magnitude = self.G * self.masses[j] / (r_magnitude ** 2) + + # Direction of acceleration + acceleration = acceleration_magnitude * (r_vector / r_magnitude) + + accelerations[i] += acceleration + + return accelerations + + def update(self, dt=0.01): + """ + Update positions and velocities using Euler integration + + Parameters: + ----------- + dt : float, optional + Time step for simulation (default is 0.01) + """ + # Compute accelerations + accelerations = self.compute_accelerations() + + # Update velocities + self.velocities += accelerations * dt + + # Update positions + self.positions += self.velocities * dt + + # Simple boundary conditions (bouncing off walls) + for i in range(self.num_bodies): + for dim in range(2): + if (self.positions[i, dim] < 0) or (self.positions[i, dim] > [self.width, self.height][dim]): + self.velocities[i, dim] *= -0.9 # Damped bounce + self.positions[i, dim] = np.clip(self.positions[i, dim], 0, [self.width, self.height][dim]) + +def main(): + st.title("Gravitational N-Body Simulation") + + # Sidebar for simulation parameters + st.sidebar.header("Simulation Parameters") + + # Number of bodies slider + num_bodies = st.sidebar.slider("Number of Bodies", min_value=2, max_value=10, value=3) + + # Simulation space dimensions + width = st.sidebar.number_input("Simulation Width", min_value=5.0, max_value=20.0, value=10.0) + height = st.sidebar.number_input("Simulation Height", min_value=5.0, max_value=20.0, value=10.0) + + # Time step slider + dt = st.sidebar.slider("Time Step", min_value=0.001, max_value=0.1, value=0.01, step=0.001) + + # Create simulation + sim = GravitySimulation(num_bodies=num_bodies, width=width, height=height) + + # Matplotlib figure for animation + fig, ax = plt.subplots(figsize=(8, 6)) + ax.set_xlim(0, width) + ax.set_ylim(0, height) + ax.set_title("Gravitational Interaction") + ax.set_xlabel("X Position") + ax.set_ylabel("Y Position") + + # Scatter plot for bodies + scatter = ax.scatter(sim.positions[:, 0], sim.positions[:, 1], + c=sim.masses, cmap='viridis', + s=sim.masses*100, alpha=0.7) + + # Animation update function + def update_plot(frame): + sim.update(dt) + scatter.set_offsets(sim.positions) + return scatter, + + # Create animation + anim = FuncAnimation(fig, update_plot, frames=200, interval=50, blit=True) + + # Convert animation to HTML for Streamlit + plt.close(fig) + + # Display animation + st.pyplot(fig) + + # Optional: Provide download of animation + st.sidebar.header("Animation Options") + if st.sidebar.button("Save Animation"): + # Save animation as gif + anim.save('gravity_simulation.gif', writer='pillow') + with open('gravity_simulation.gif', 'rb') as f: + st.sidebar.download_button( + label="Download Animation", + data=f.read(), + file_name="gravity_simulation.gif", + mime="image/gif" + ) + + # Information section + st.markdown("## About the Simulation") + st.markdown(""" + This simulation demonstrates gravitational interactions between multiple bodies: + - Bodies attract each other based on their masses + - Gravitational force is inversely proportional to the square of the distance + - Bodies bounce off the simulation boundaries + - Larger/darker points represent bodies with more mass + """) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pages/heat.py b/pages/heat.py new file mode 100644 index 0000000000000000000000000000000000000000..387de2d70ef4dfbdd8ab7e87d6a7dffe0b5821e8 --- /dev/null +++ b/pages/heat.py @@ -0,0 +1,47 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("Heat Diffusion Simulation") + +# Simulation parameters +nx, ny = 50, 50 # Grid size +alpha = st.slider("Thermal Diffusivity (α)", min_value=0.1, max_value=1.0, value=0.2, step=0.1) +dt = st.slider("Time Step (dt)", min_value=0.001, max_value=0.1, value=0.01, step=0.005) +steps = st.number_input("Number of Simulation Steps", min_value=10, max_value=1000, value=100, step=10) + +# Initialize temperature field: all zeros with a hot spot in the center +T = np.zeros((nx, ny)) +T[nx//2, ny//2] = 100.0 + +# Placeholder for plotting +plot_placeholder = st.empty() + +def update_temperature(T, alpha, dt): + """ + Update temperature using a simple finite difference scheme for the 2D heat equation. + The update formula for interior grid points is: + 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]) + """ + T_new = T.copy() + T_new[1:-1, 1:-1] = T[1:-1, 1:-1] + alpha * dt * ( + T[2:, 1:-1] + T[:-2, 1:-1] + T[1:-1, 2:] + T[1:-1, :-2] - 4 * T[1:-1, 1:-1] + ) + return T_new + +# Run the simulation loop +for i in range(int(steps)): + T = update_temperature(T, alpha, dt) + + # Create the plot + fig, ax = plt.subplots() + heatmap = ax.imshow(T, cmap='hot', interpolation='nearest') + ax.set_title(f"Step {i+1}") + ax.axis('off') + + # Display the plot in the Streamlit placeholder + plot_placeholder.pyplot(fig) + + # Pause to simulate time evolution (adjust the sleep time as needed) + time.sleep(0.1) diff --git a/pages/ideal-gas-law.py b/pages/ideal-gas-law.py new file mode 100644 index 0000000000000000000000000000000000000000..8e2276c1bf7022cfa62ecaee948be2ef019ab9e5 --- /dev/null +++ b/pages/ideal-gas-law.py @@ -0,0 +1,40 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Set the title and description for the simulation +st.title("Ideal Gas Law Visualization") +st.write(""" +This simulation visualizes the relationship between pressure, volume, and temperature of an ideal gas using the equation: +\[ P = \frac{nRT}{V} \] +Adjust the parameters in the sidebar to see how the pressure changes. +""") + +# Constants +R = 8.314 # Ideal gas constant in J/(mol*K) + +# Sidebar for user inputs +st.sidebar.header("Gas Parameters") +n = st.sidebar.slider("Number of moles (n)", min_value=0.1, max_value=5.0, value=1.0, step=0.1) +T = st.sidebar.slider("Temperature (T in Kelvin)", min_value=100, max_value=1000, value=300, step=50) +V = st.sidebar.slider("Volume (V in cubic meters)", min_value=0.1, max_value=10.0, value=1.0, step=0.1) + +# Calculate pressure using the ideal gas law +P = n * R * T / V +st.write(f"### Computed Pressure:") +st.write(f"The pressure of the gas is **{P:.2f} Pascals**.") + +# Create a range of volumes to plot the Pressure vs Volume curve +volumes = np.linspace(0.1, 10, 200) +pressures = n * R * T / volumes + +# Plotting the curve +fig, ax = plt.subplots() +ax.plot(volumes, pressures, color='b', label=f"T = {T} K, n = {n} mol") +ax.set_xlabel("Volume (m³)") +ax.set_ylabel("Pressure (Pascals)") +ax.set_title("Pressure vs Volume") +ax.legend() +ax.grid(True) + +st.pyplot(fig) diff --git a/pages/impulse.py b/pages/impulse.py new file mode 100644 index 0000000000000000000000000000000000000000..f2cfc01b10e1e13d67f6acd51c5c941f7e76efc4 --- /dev/null +++ b/pages/impulse.py @@ -0,0 +1,122 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +def calculate_impulse(force, time): + """Calculate impulse given force and time.""" + return force * time + +def calculate_velocity_change(mass, impulse): + """Calculate change in velocity using impulse-momentum theorem.""" + return impulse / mass + +def plot_force_time_graph(force, time): + """Create a force-time graph.""" + plt.figure(figsize=(10, 6)) + plt.plot([0, time, time], [force, force, 0], 'b-') + plt.fill_between([0, time], [force, force], alpha=0.3) + plt.title('Force-Time Graph') + plt.xlabel('Time (s)') + plt.ylabel('Force (N)') + plt.grid(True) + return plt + +def main(): + st.title('Impulse Visualization') + + # Sidebar for input parameters + st.sidebar.header('Simulation Parameters') + + # Mass input + mass = st.sidebar.number_input( + 'Mass of Object (kg)', + min_value=0.1, + max_value=100.0, + value=1.0, + step=0.1 + ) + + # Force input with different input methods + input_type = st.sidebar.radio( + 'Force Input Method', + ['Constant Force', 'Peak Force and Duration'] + ) + + if input_type == 'Constant Force': + # Constant force input + force = st.sidebar.number_input( + 'Constant Force (N)', + min_value=-1000.0, + max_value=1000.0, + value=10.0, + step=1.0 + ) + time = st.sidebar.number_input( + 'Duration (s)', + min_value=0.01, + max_value=10.0, + value=0.5, + step=0.1 + ) + else: + # Peak force and duration input + force = st.sidebar.number_input( + 'Peak Force (N)', + min_value=-1000.0, + max_value=1000.0, + value=50.0, + step=1.0 + ) + time = st.sidebar.number_input( + 'Force Duration (s)', + min_value=0.01, + max_value=10.0, + value=0.5, + step=0.1 + ) + + # Calculate impulse and velocity change + impulse = calculate_impulse(force, time) + velocity_change = calculate_velocity_change(mass, impulse) + + # Display results + st.header('Impulse Calculation Results') + col1, col2, col3 = st.columns(3) + + with col1: + st.metric('Force', f'{force} N') + with col2: + st.metric('Duration', f'{time} s') + with col3: + st.metric('Mass', f'{mass} kg') + + st.markdown('---') + + col1, col2 = st.columns(2) + + with col1: + st.metric('Impulse', f'{impulse:.2f} N·s') + with col2: + st.metric('Velocity Change', f'{velocity_change:.2f} m/s') + + # Visualize Force-Time Graph + st.header('Force-Time Graph') + fig = plot_force_time_graph(force, time) + st.pyplot(fig) + + # Explanation section + st.header('Understanding Impulse') + st.markdown(""" + ### What is Impulse? + - **Impulse** is defined as the product of force and time: Impulse = Force × Time + - It represents the total effect of a force acting over a period of time + - Impulse is equal to the change in momentum of an object + + ### Key Concepts + - The area under a Force-Time graph represents the impulse + - Larger impulse means a greater change in velocity + - Impulse can be increased by either increasing force or increasing time of application + """) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pages/inertia.py b/pages/inertia.py new file mode 100644 index 0000000000000000000000000000000000000000..7bb6b3be7cbe7e1a4139ab6100522b618bb84020 --- /dev/null +++ b/pages/inertia.py @@ -0,0 +1,111 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation + +def inertia_simulation(): + st.title('Inertia Visualization Simulation') + + # Sidebar controls + st.sidebar.header('Simulation Parameters') + + # Object mass + mass = st.sidebar.slider('Object Mass (kg)', min_value=1, max_value=25, value=10) + + # Initial velocity + initial_velocity = st.sidebar.slider('Initial Velocity (m/s)', min_value=0, max_value=20, value=10) + + # Friction coefficient + friction = st.sidebar.slider('Friction Coefficient', min_value=0.0, max_value=1.0, value=0.2, step=0.1) + + # Explanation + st.markdown(""" + ## Understanding Inertia + + Inertia is an object's resistance to change in its state of motion. + This simulation shows how: + - Heavier objects maintain their motion longer + - Friction gradually reduces an object's speed + - Objects tend to continue moving unless stopped + """) + + # Simulation calculations + def calculate_motion(mass, initial_velocity, friction, time): + # Gravitational acceleration + g = 9.8 + + # Deceleration due to friction + deceleration = friction * g *mass + + # Calculate position and velocity + if time <= initial_velocity / deceleration: + # Object is still moving forward + position = initial_velocity * time - 0.5 * deceleration * time**2 + velocity = initial_velocity - deceleration * time + else: + # Object has come to a stop + # Find the time when object stops + stop_time = initial_velocity / deceleration + + # Calculate final position + final_position = initial_velocity * stop_time - 0.5 * deceleration * stop_time**2 + + # Position after stopping point remains constant + position = final_position + velocity = 0 + + return position, velocity + + # Prepare plot + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # Time array + time = np.linspace(0, 5, 100) + + # Calculate motion + positions = [] + velocities = [] + for t in time: + pos, vel = calculate_motion(mass, initial_velocity, friction, t) + positions.append(pos) + velocities.append(vel) + + # Position plot + ax1.plot(time, positions, label='Position') + ax1.set_title(f'Position over Time (Mass: {mass} kg)') + ax1.set_xlabel('Time (s)') + ax1.set_ylabel('Distance (m)') + ax1.legend() + ax1.grid(True) + + # Velocity plot + ax2.plot(time, velocities, label='Velocity', color='red') + ax2.set_title(f'Velocity over Time (Initial Velocity: {initial_velocity} m/s)') + ax2.set_xlabel('Time (s)') + ax2.set_ylabel('Velocity (m/s)') + ax2.legend() + ax2.grid(True) + + # Adjust layout and display + plt.tight_layout() + st.pyplot(fig) + + # Detailed explanation + st.markdown(f""" + ### Simulation Details + - **Mass of Object:** {mass} kg + - **Initial Velocity:** {initial_velocity} m/s + - **Friction Coefficient:** {friction} + + #### Observation + The graphs show how: + 1. Position changes over time (distance traveled) + 2. Velocity decreases due to friction + """) + + + +# Streamlit page configuration +if __name__ == '__main__': + # Run the simulation + inertia_simulation() \ No newline at end of file diff --git a/pages/interference.py b/pages/interference.py new file mode 100644 index 0000000000000000000000000000000000000000..35410823d8922fdc042548bd267f6d9a329e69fc --- /dev/null +++ b/pages/interference.py @@ -0,0 +1,68 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +def interference_pattern(lambda_, d, grid_size=200, x_range=5): + """ + Compute the interference pattern for two point sources. + + Parameters: + - lambda_: Wavelength of the waves (float). + - d: Separation distance between the two sources (float). + - grid_size: Number of points in the grid (int, default=200). + - x_range: Range of x and y coordinates (float, default=5). + + Returns: + - I: 2D array of interference intensity. + - x: 1D array of x coordinates. + - y: 1D array of y coordinates. + - S1: Position of the first source (list). + - S2: Position of the second source (list). + """ + # Create a 2D grid + x = np.linspace(-x_range, x_range, grid_size) + y = np.linspace(-x_range, x_range, grid_size) + X, Y = np.meshgrid(x, y) + + # Define source positions + S1 = [-d/2, 0] # First source at (-d/2, 0) + S2 = [d/2, 0] # Second source at (d/2, 0) + + # Calculate distances from each source to every point on the grid + r1 = np.sqrt((X - S1[0])**2 + (Y - S1[1])**2) + r2 = np.sqrt((X - S2[0])**2 + (Y - S2[1])**2) + + # Compute path difference + delta = r2 - r1 + + # Calculate intensity (proportional to cos^2(π * Δ / λ)) + I = np.cos(np.pi * delta / lambda_)**2 + + return I, x, y, S1, S2 + +# Streamlit app setup +st.title("Interference Pattern Simulation") + +# Explanatory text +st.write(""" +This simulation visualizes the interference pattern created by two point sources emitting waves. +- **Bright regions**: Constructive interference, where waves reinforce each other. +- **Dark regions**: Destructive interference, where waves cancel out. +Use the sliders below to adjust the **wavelength (λ)** and **source separation (d)** to see how the pattern changes. +""") + +# Interactive sliders +lambda_ = st.slider("Wavelength λ", min_value=0.1, max_value=2.0, value=1.0, step=0.1) +d = st.slider("Source Separation d", min_value=0.5, max_value=5.0, value=2.0, step=0.1) + +# Compute the interference pattern +I, x, y, S1, S2 = interference_pattern(lambda_, d) + +# Create and display the plot +fig, ax = plt.subplots() +ax.imshow(I, cmap='hot', extent=[x.min(), x.max(), y.min(), y.max()], origin='lower') +ax.plot([S1[0], S2[0]], [S1[1], S2[1]], 'wo', markersize=5) # Mark sources with white dots +ax.set_xlabel("x") +ax.set_ylabel("y") +ax.set_title("Interference Pattern") +st.pyplot(fig) \ No newline at end of file diff --git a/pages/kinetic-theory-of-gas.py b/pages/kinetic-theory-of-gas.py new file mode 100644 index 0000000000000000000000000000000000000000..4c50ae85653a5c6094a69b09ac3f91a213c1ab84 --- /dev/null +++ b/pages/kinetic-theory-of-gas.py @@ -0,0 +1,91 @@ +import streamlit as st +import numpy as np +import plotly.graph_objects as go +import time + +# Streamlit app title and description +st.title("Kinetic Theory of Gases Simulation") +st.write("This simulation visualizes gas molecules as particles moving in a 2D container. Watch how their motion relates to temperature and pressure!") + +# Simulation parameters +L = 1.0 # Container side length +N = 50 # Number of particles +sigma = 0.05 # Standard deviation of velocities (related to temperature) +dt = 0.01 # Time step for simulation + +# Initialize particle positions and velocities +x = np.random.uniform(0, L, N) +y = np.random.uniform(0, L, N) +vx = np.random.normal(0, sigma, N) +vy = np.random.normal(0, sigma, N) + +# Initialize momentum transfer for pressure calculation +total_momentum_transfer = 0 + +# Create placeholders for plot and metrics +plot_placeholder = st.empty() +metrics_placeholder = st.empty() + +# Simulation loop +step = 0 +while True: + # Update particle positions + x_new = x + vx * dt + y_new = y + vy * dt + + # Handle collisions with walls and accumulate momentum transfer + for i in range(N): + if x_new[i] < 0: + x_new[i] = -x_new[i] # Reflect position + vx[i] = -vx[i] # Reverse velocity + total_momentum_transfer += 2 * abs(vx[i]) + elif x_new[i] > L: + x_new[i] = 2 * L - x_new[i] # Reflect position + vx[i] = -vx[i] # Reverse velocity + total_momentum_transfer += 2 * abs(vx[i]) + if y_new[i] < 0: + y_new[i] = -y_new[i] # Reflect position + vy[i] = -vy[i] # Reverse velocity + total_momentum_transfer += 2 * abs(vy[i]) + elif y_new[i] > L: + y_new[i] = 2 * L - y_new[i] # Reflect position + vy[i] = -vy[i] # Reverse velocity + total_momentum_transfer += 2 * abs(vy[i]) + + # Update positions + x = x_new + y = y_new + + # Increment step counter + step += 1 + + # Calculate and display temperature and pressure every 100 steps + if step % 100 == 0: + # Temperature: T = (1/2) * in 2D (with k=1, m=1) + v_squared = vx**2 + vy**2 + T = (1 / (2 * N)) * np.sum(v_squared) + + # Pressure: P = momentum_transfer / (perimeter * time) + time_elapsed = 100 * dt + P = total_momentum_transfer / (4 * L * time_elapsed) + + # Update metrics display + metrics_placeholder.write(f"Temperature: {T:.4f} | Pressure: {P:.4f}") + + # Reset momentum transfer + total_momentum_transfer = 0 + + # Create and update the Plotly scatter plot + fig = go.Figure() + fig.add_trace(go.Scatter(x=x, y=y, mode='markers', marker=dict(size=5))) + fig.update_layout( + xaxis_range=[0, L], + yaxis_range=[0, L], + width=500, + height=500, + title="Gas Molecules in Motion" + ) + plot_placeholder.plotly_chart(fig) + + # Control animation speed + time.sleep(0.05) \ No newline at end of file diff --git a/pages/lenz-law.py b/pages/lenz-law.py new file mode 100644 index 0000000000000000000000000000000000000000..baf26fb579dffca9851d5b402bb13b6b4fb734af --- /dev/null +++ b/pages/lenz-law.py @@ -0,0 +1,112 @@ +import streamlit as st +import matplotlib.pyplot as plt +import numpy as np + +st.title("Lenz's Law Visual Simulation") + +# --- Simulation Controls --- +st.header("Simulation Controls") +magnet_position = st.slider("Magnet Position", -5.0, 5.0, 0.0, step=0.1, + help="Control the horizontal position of the magnet.") +magnet_velocity = st.slider("Magnet Velocity (for change visualization)", -1.0, 1.0, 0.0, step=0.1, + 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.") +show_field_lines = st.checkbox("Show Magnetic Field Lines", value=True) +show_induced_current = st.checkbox("Show Induced Current", value=True) +show_forces = st.checkbox("Show Forces", value=True) + +# --- Plotting Area --- +plot_placeholder = st.empty() # Placeholder for the plot + +def draw_magnet(ax, x_magnet, magnet_width=1.0, magnet_height=0.5): + """Draws the magnet on the plot.""" + y_center = 0 + ax.add_patch(plt.Rectangle((x_magnet - magnet_width / 2, y_center - magnet_height / 2), magnet_width, magnet_height, + facecolor='red', edgecolor='black')) + ax.text(x_magnet - magnet_width / 4, y_center, 'N', color='white', ha='center', va='center', fontweight='bold') + ax.text(x_magnet + magnet_width / 4, y_center, 'S', color='white', ha='center', va='center', fontweight='bold') + +def draw_coil(ax, coil_x_center=0, coil_width=1.5, coil_height=1.0): + """Draws the coil on the plot.""" + y_center = 0 + ax.add_patch(plt.Rectangle((coil_x_center - coil_width / 2, y_center - coil_height / 2), coil_width, coil_height, + facecolor='lightgray', edgecolor='black')) + ax.text(coil_x_center, y_center, 'Coil', color='black', ha='center', va='center') + +def draw_magnetic_field_magnet(ax, x_magnet, field_strength=0.5, num_lines=15): + """Draws simplified magnetic field lines from the magnet.""" + y_center = 0 + start_x = x_magnet - 0.5 # Approximate North pole position for field lines + end_x = x_magnet + 0.5 # Approximate South pole position + + for i in range(num_lines): + y_offset = (i - num_lines // 2) * 0.2 * field_strength + if y_offset != 0: # Avoid drawing lines exactly on top of each other which can cause issues + 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 + 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 + +def draw_induced_current(ax, coil_x_center, magnet_velocity): + """Draws arrows indicating induced current direction based on magnet velocity.""" + y_center = 0 + current_direction = 0 # 0: No current, 1: Clockwise, -1: Counter-clockwise + + if magnet_velocity > 0.1: # Magnet moving right (towards coil) + current_direction = 1 # Clockwise to oppose increasing flux into the page (assuming field is into the page when N pole approaches) + elif magnet_velocity < -0.1: # Magnet moving left (away from coil) + current_direction = -1 # Counter-clockwise to oppose decreasing flux into the page + + if current_direction == 1: # Clockwise + 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') + 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') + ax.text(coil_x_center, y_center + 0.6, "Induced Current (Clockwise)", color='green', ha='center', va='center') + elif current_direction == -1: # Counter-clockwise + 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') + 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') + ax.text(coil_x_center, y_center + 0.6, "Induced Current (Counter-Clockwise)", color='green', ha='center', va='center') + else: + ax.text(coil_x_center, y_center + 0.6, "No Induced Current", color='gray', ha='center', va='center') + + +def draw_force_arrow(ax, x_magnet, magnet_velocity): + """Draws force arrows indicating repulsion or attraction based on Lenz's Law.""" + force_direction = 0 # 0: No force, 1: Repulsion (away from coil), -1: Attraction (towards coil) + + if magnet_velocity > 0.1: # Moving towards coil - Repulsion + force_direction = 1 + elif magnet_velocity < -0.1: # Moving away from coil - Attraction + force_direction = -1 + + if force_direction == 1: # Repulsion - Force on Magnet to the Left + ax.arrow(x_magnet, 1.0, -0.5, 0, head_width=0.1, head_length=0.2, fc='purple', ec='purple') + ax.text(x_magnet - 0.25, 1.2, "Repulsive Force", color='purple', ha='center', va='center') + elif force_direction == -1: # Attraction - Force on Magnet to the Right + ax.arrow(x_magnet, 1.0, 0.5, 0, head_width=0.1, head_length=0.2, fc='purple', ec='purple') + ax.text(x_magnet + 0.25, 1.2, "Attractive Force", color='purple', ha='center', va='center') + else: + ax.text(x_magnet, 1.2, "No Force", color='gray', ha='center', va='center') + + +# --- Main Plot Update Function --- +def update_plot(magnet_position, magnet_velocity, show_field_lines, show_induced_current, show_forces): + """Updates the plot based on slider values and checkboxes.""" + fig, ax = plt.subplots() + ax.set_xlim([-6, 6]) + ax.set_ylim([-2, 2]) + ax.set_aspect('equal') # Ensure circle is drawn as circle + ax.axis('off') # Hide axes + + draw_coil(ax) + draw_magnet(ax, magnet_position) + + if show_field_lines: + draw_magnetic_field_magnet(ax, magnet_position) + + if show_induced_current: + draw_induced_current(ax, 0, magnet_velocity) # Coil is at x=0 + + if show_forces: + draw_force_arrow(ax, magnet_position, magnet_velocity) + + plot_placeholder.pyplot(fig) # Update the plot in Streamlit + +# --- Run the simulation and update plot on slider change --- +update_plot(magnet_position, magnet_velocity, show_field_lines, show_induced_current, show_forces) \ No newline at end of file diff --git a/pages/light-wave.py b/pages/light-wave.py new file mode 100644 index 0000000000000000000000000000000000000000..b7661878563ade9a0c3280ab0e14138e589a1d2f --- /dev/null +++ b/pages/light-wave.py @@ -0,0 +1,70 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +# Title and description +st.title("Light Wave Simulation") +st.write(""" +This simulation visualizes light as a transverse wave. +Adjust the wavelength and frequency to see how the wave changes. +Check the 'Animate' box to see the wave propagate over time. +Note: Units are arbitrary, and the wave speed is slowed down for visualization purposes. +""") + +# Interactive sliders for wave parameters +wavelength = st.slider("Wavelength", min_value=0.1, max_value=10.0, value=1.0, step=0.1) +frequency = st.slider("Frequency", min_value=0.1, max_value=10.0, value=1.0, step=0.1) + +# Checkbox to toggle animation +animate = st.checkbox("Animate") + +# Define wave parameters +A = 1 # Amplitude (fixed at 1 for simplicity) +k = 2 * np.pi / wavelength # Wave number (k = 2π/λ) +omega = 2 * np.pi * frequency # Angular frequency (ω = 2πf) + +# Spatial coordinates +x = np.linspace(0, 10, 1000) # Position array from 0 to 10 with 1000 points + +# Animation or static plot based on user input +if animate: + # Create a placeholder for dynamic updates + placeholder = st.empty() + + # Run animation for 100 frames + num_frames = 100 + for frame in range(num_frames): + # Calculate time for this frame + t = frame * 0.1 + + # Compute wave amplitude at this time + y = A * np.sin(k * x - omega * t) + + # Create and configure the plot + fig, ax = plt.subplots() + ax.plot(x, y) + ax.set_ylim(-1.5, 1.5) # Fixed y-axis limits to show wave clearly + ax.set_xlabel("Position") + ax.set_ylabel("Amplitude") + ax.set_title(f"Light Wave at t = {t:.1f}") + + # Update the placeholder with the new plot + placeholder.pyplot(fig) + + # Close the figure to prevent memory buildup + plt.close(fig) + + # Control animation speed + time.sleep(0.1) +else: + # Display a static plot at t=0 when animation is off + t = 0 + y = A * np.sin(k * x - omega * t) + fig, ax = plt.subplots() + ax.plot(x, y) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel("Position") + ax.set_ylabel("Amplitude") + ax.set_title("Light Wave at t = 0") + st.pyplot(fig) \ No newline at end of file diff --git a/pages/magnetic-field.py b/pages/magnetic-field.py new file mode 100644 index 0000000000000000000000000000000000000000..155444b4943d7267c49910f5a557dcf57766f41b --- /dev/null +++ b/pages/magnetic-field.py @@ -0,0 +1,258 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Circle + +def magnetic_field_of_wire(x, y, z, current, wire_pos): + """Calculate magnetic field due to infinite straight wire at position wire_pos""" + r_vec = np.array([x - wire_pos[0], y - wire_pos[1], 0]) + r = np.sqrt(r_vec[0]**2 + r_vec[1]**2) + + # Prevent division by zero + if r < 1e-10: + return np.array([0, 0, 0]) + + # Direction: cross product of z unit vector with r_vec + direction = np.array([-r_vec[1], r_vec[0], 0]) / r + + # Magnitude: μ0 * I / (2π * r) + mu0 = 4 * np.pi * 1e-7 # magnetic permeability + magnitude = mu0 * current / (2 * np.pi * r) + + return magnitude * direction + +def magnetic_field_of_loop(x, y, z, current, loop_center, radius): + """Calculate magnetic field due to a circular current loop in the xy-plane""" + # Distance from the point to loop center + r_vec = np.array([x - loop_center[0], y - loop_center[1], z - loop_center[2]]) + r = np.sqrt(r_vec[0]**2 + r_vec[1]**2 + r_vec[2]**2) + + # Prevent division by zero + if r < 1e-10: + return np.array([0, 0, 0]) + + # On-axis field calculation (simplified) + mu0 = 4 * np.pi * 1e-7 # magnetic permeability + + if abs(r_vec[0]) < 1e-10 and abs(r_vec[1]) < 1e-10: + # On the z-axis + magnitude = mu0 * current * radius**2 / (2 * (radius**2 + r_vec[2]**2)**(3/2)) + return np.array([0, 0, magnitude if r_vec[2] >= 0 else -magnitude]) + + # For off-axis points, return a more approximate field (full calculation is complex) + # This is a simplification that gives reasonable visualization + z_component = mu0 * current * radius**2 / (2 * (radius**2 + r**2)**(3/2)) + r_component = mu0 * current * radius**2 * r_vec[2] / (4 * r * (radius**2 + r**2)**(3/2)) + + return np.array([ + r_component * r_vec[0]/r, + r_component * r_vec[1]/r, + z_component + ]) + +def magnetic_field_of_bar_magnet(x, y, z, magnet_center, magnet_length, magnet_strength): + """Calculate magnetic field due to a simple bar magnet (dipole approximation)""" + r_vec = np.array([x - magnet_center[0], y - magnet_center[1], z - magnet_center[2]]) + r = np.sqrt(r_vec[0]**2 + r_vec[1]**2 + r_vec[2]**2) + + # Prevent division by zero + if r < 1e-10: + return np.array([0, 0, 0]) + + # Dipole moment in the z-direction + m_vec = np.array([0, 0, magnet_strength * magnet_length]) + + # Dipole field formula + mu0 = 4 * np.pi * 1e-7 # magnetic permeability + constant = mu0 / (4 * np.pi * r**5) + dot_product = np.dot(m_vec, r_vec) + + field = constant * (3 * r_vec * dot_product - r**2 * m_vec) + + return field + +def plot_magnetic_field(field_type, parameters): + """Generate the magnetic field plot""" + fig, ax = plt.subplots(figsize=(10, 8)) + + # Create grid of points + n = 20 + x = np.linspace(-5, 5, n) + y = np.linspace(-5, 5, n) + X, Y = np.meshgrid(x, y) + Z = np.zeros_like(X) # Z=0 plane + + # Calculate field at each point + U = np.zeros_like(X) + V = np.zeros_like(Y) + W = np.zeros_like(Z) + + for i in range(n): + for j in range(n): + if field_type == "Wire": + field = magnetic_field_of_wire( + X[i, j], Y[i, j], Z[i, j], + parameters["current"], + parameters["position"] + ) + elif field_type == "Loop": + field = magnetic_field_of_loop( + X[i, j], Y[i, j], Z[i, j], + parameters["current"], + parameters["center"], + parameters["radius"] + ) + elif field_type == "Bar Magnet": + field = magnetic_field_of_bar_magnet( + X[i, j], Y[i, j], Z[i, j], + parameters["center"], + parameters["length"], + parameters["strength"] + ) + + # Normalize the field for better visualization + field_magnitude = np.sqrt(field[0]**2 + field[1]**2 + field[2]**2) + if field_magnitude > 0: + normalized_field = field / field_magnitude + else: + normalized_field = field + + U[i, j] = normalized_field[0] + V[i, j] = normalized_field[1] + W[i, j] = normalized_field[2] + + # Plot the magnetic field + ax.streamplot(X, Y, U, V, density=2, color='b', linewidth=1, arrowsize=1) + + # Draw the source of the field + if field_type == "Wire": + ax.plot(parameters["position"][0], parameters["position"][1], 'ro', markersize=10) + ax.annotate('Current into page' if parameters["current"] > 0 else 'Current out of page', + xy=(parameters["position"][0], parameters["position"][1]), + xytext=(parameters["position"][0] + 0.5, parameters["position"][1] + 0.5)) + elif field_type == "Loop": + circle = Circle(parameters["center"][:2], parameters["radius"], fill=False, color='r') + ax.add_patch(circle) + ax.annotate('Current loop', + xy=(parameters["center"][0], parameters["center"][1]), + xytext=(parameters["center"][0] + 0.5, parameters["center"][1] + 0.5)) + elif field_type == "Bar Magnet": + half_length = parameters["length"] / 2 + ax.plot([parameters["center"][0] - half_length, parameters["center"][0] + half_length], + [parameters["center"][1], parameters["center"][1]], 'r-', linewidth=4) + ax.text(parameters["center"][0] - half_length - 0.3, parameters["center"][1], 'S') + ax.text(parameters["center"][0] + half_length + 0.1, parameters["center"][1], 'N') + + ax.set_xlim(-5, 5) + ax.set_ylim(-5, 5) + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_title(f'Magnetic Field of {field_type}') + ax.grid(True) + ax.set_aspect('equal') + + return fig + +def main(): + st.title("Magnetic Field Visualization") + st.write(""" + This app visualizes the magnetic field created by different sources. + Choose a magnetic field source below and adjust the parameters to see how the field changes. + """) + + # Sidebar for parameters + st.sidebar.header("Field Source Parameters") + + # Select field type + field_type = st.sidebar.selectbox( + "Select a magnetic field source:", + ["Wire", "Loop", "Bar Magnet"] + ) + + # Parameters based on field type + if field_type == "Wire": + current = st.sidebar.slider("Current (A)", -10.0, 10.0, 5.0) + wire_x = st.sidebar.slider("Wire X Position", -3.0, 3.0, 0.0) + wire_y = st.sidebar.slider("Wire Y Position", -3.0, 3.0, 0.0) + + parameters = { + "current": current, + "position": [wire_x, wire_y] + } + + st.write(""" + ### Infinite Straight Wire + + The magnetic field around a long straight wire forms concentric circles around the wire. + - Direction: determined by the right-hand rule (thumb points in the current direction) + - Magnitude: proportional to the current and inversely proportional to the distance from the wire + + Mathematical formula: B = (μ₀ × I) / (2π × r) + """) + + elif field_type == "Loop": + current = st.sidebar.slider("Current (A)", -10.0, 10.0, 5.0) + loop_x = st.sidebar.slider("Loop Center X", -3.0, 3.0, 0.0) + loop_y = st.sidebar.slider("Loop Center Y", -3.0, 3.0, 0.0) + loop_z = st.sidebar.slider("Loop Center Z", -3.0, 3.0, 0.0) + radius = st.sidebar.slider("Loop Radius", 0.5, 3.0, 1.0) + + parameters = { + "current": current, + "center": [loop_x, loop_y, loop_z], + "radius": radius + } + + st.write(""" + ### Current Loop + + A current loop creates a magnetic field similar to a bar magnet. + - Near the center of the loop, the field lines are nearly parallel + - Far from the loop, the field approximates that of a dipole + + This is the basic principle behind electromagnets and solenoids. + """) + + elif field_type == "Bar Magnet": + magnet_x = st.sidebar.slider("Magnet Center X", -3.0, 3.0, 0.0) + magnet_y = st.sidebar.slider("Magnet Center Y", -3.0, 3.0, 0.0) + magnet_z = st.sidebar.slider("Magnet Center Z", -3.0, 3.0, 0.0) + length = st.sidebar.slider("Magnet Length", 0.5, 3.0, 2.0) + strength = st.sidebar.slider("Magnet Strength", 1.0, 10.0, 5.0) + + parameters = { + "center": [magnet_x, magnet_y, magnet_z], + "length": length, + "strength": strength + } + + st.write(""" + ### Bar Magnet + + A bar magnet produces a dipole magnetic field. + - Field lines emerge from the north pole and enter the south pole + - The field strength decreases with the cube of the distance from the magnet + + This simulation uses a simplified dipole approximation. + """) + + # Generate and display plot + fig = plot_magnetic_field(field_type, parameters) + st.pyplot(fig) + + # Additional explanations + st.write(""" + ### About Magnetic Fields + + Magnetic fields are vector fields that describe the magnetic influence on moving electric charges, electric currents, and magnetic materials. + + Key concepts: + - Magnetic field lines are continuous loops + - The density of field lines indicates the strength of the field + - The direction of the field is determined by the right-hand rule + + The visualization shows a 2D slice (Z=0 plane) of the magnetic field, with arrows indicating the field direction. + """) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pages/mass-energy-equivalence.py b/pages/mass-energy-equivalence.py new file mode 100644 index 0000000000000000000000000000000000000000..06b69f190c67c3d8401fb5c650472df4b09825b0 --- /dev/null +++ b/pages/mass-energy-equivalence.py @@ -0,0 +1,42 @@ +import streamlit as st +import numpy as np +import pandas as pd + +# Define the speed of light constant (in m/s) +c = 3e8 # 300,000,000 m/s + +# Set up the Streamlit app title and description. +st.title("Mass–Energy Equivalence Visualization") +st.write(""" +Einstein's famous equation \(E = mc^2\) tells us that mass and energy are interchangeable. +Use the slider below to adjust the mass and see the equivalent energy. +""") + +# Create a slider for selecting the mass (in kg) +mass = st.slider("Select mass (kg)", min_value=0.001, max_value=100.0, value=1.0, step=0.001) + +# Calculate energy using E = m * c^2 +energy = mass * c**2 + +# Display the calculated energy +st.write(f"**For a mass of {mass:.3f} kg, the equivalent energy is {energy:.3e} Joules.**") + +# Additional explanation for perspective +st.write(""" +For context, 1 kg of mass is equivalent to about 9 × 10^16 Joules of energy – +an amount that can power entire cities for days. +""") + +# Plotting: Show a graph of Energy vs Mass for a range from 0 up to the selected mass. +st.subheader("Energy as a Function of Mass") +mass_values = np.linspace(0, mass, 100) +energy_values = mass_values * c**2 + +# Create a DataFrame for plotting +df = pd.DataFrame({ + "Mass (kg)": mass_values, + "Energy (Joules)": energy_values +}) + +# Display the line chart +st.line_chart(df.set_index("Mass (kg)")) diff --git a/pages/mass.py b/pages/mass.py new file mode 100644 index 0000000000000000000000000000000000000000..a6725e3e20f538c64d65300ba1963d23a0272fea --- /dev/null +++ b/pages/mass.py @@ -0,0 +1,122 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import plotly.graph_objs as go +import plotly.express as px +import pandas as pd + +def introduction_to_mass(): + """ + Provides an introductory explanation of mass + """ + st.header("Understanding Mass 🧊") + st.markdown(""" + Mass is a fundamental property of matter that represents the amount of material in an object. + It is a measure of an object's resistance to acceleration when a force is applied. + + Key Characteristics of Mass: + - Measured in kilograms (kg) + - Remains constant regardless of location (unlike weight) + - Determines an object's inertia + """) + +def mass_vs_weight_simulation(): + """ + Interactive simulation showing difference between mass and weight + """ + st.header("Mass vs Weight Comparison 🌍") + + # Gravity values for different celestial bodies + gravity_dict = { + "Earth": 9.8, + "Moon": 1.62, + "Mars": 3.72, + "Jupiter": 24.79 + } + + # User input for mass + mass = st.slider("Select Object Mass (kg)", min_value=1, max_value=100, value=50) + + # Create DataFrame for visualization + data = [] + for planet, gravity in gravity_dict.items(): + weight = mass * gravity + data.append({"Celestial Body": planet, "Weight (N)": weight}) + + df = pd.DataFrame(data) + + # Plotly bar chart + fig = px.bar(df, x="Celestial Body", y="Weight (N)", + title=f"Weight of a {mass} kg Object on Different Planets", + labels={"Weight (N)": "Weight (Newtons)"}) + st.plotly_chart(fig) + + # Explanation + st.markdown(f""" + 🔍 For a {mass} kg object: + - Mass remains constant at {mass} kg everywhere + - Weight changes based on local gravitational acceleration + """) + +def density_visualization(): + """ + Visualization of mass density + """ + st.header("Mass Density Exploration 📊") + + # Predefined materials and their densities + materials = { + "Air": 0.001225, + "Water": 1000, + "Wood": 700, + "Aluminum": 2700, + "Iron": 7874, + "Lead": 11340 + } + + # User selects volume + volume = st.slider("Select Volume (m³)", min_value=0.1, max_value=10.0, value=1.0, step=0.1) + + # Calculate masses + masses = {material: density * volume for material, density in materials.items()} + + # Create DataFrame + df = pd.DataFrame.from_dict(masses, orient='index', columns=['Mass (kg)']) + df.index.name = 'Material' + df = df.reset_index() + + # Plotly bar chart + fig = px.bar(df, x='Material', y='Mass (kg)', + title=f"Mass of Different Materials (Volume: {volume} m³)", + labels={'Mass (kg)': 'Mass (kg)'}) + st.plotly_chart(fig) + + st.markdown(""" + 💡 Density determines how much mass is contained in a given volume: + - Low density: Less mass per volume (e.g., Air) + - High density: More mass per volume (e.g., Lead) + """) + +def main(): + """ + Main Streamlit app + """ + st.title("Mass Visualization Simulator 🔬") + + # Sidebar for navigation + page = st.sidebar.selectbox("Choose a Visualization", [ + "Introduction to Mass", + "Mass vs Weight", + "Density Exploration" + ]) + + # Page routing + if page == "Introduction to Mass": + introduction_to_mass() + elif page == "Mass vs Weight": + mass_vs_weight_simulation() + elif page == "Density Exploration": + density_visualization() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pages/maxwells-equations.py b/pages/maxwells-equations.py new file mode 100644 index 0000000000000000000000000000000000000000..a46e74179df003deceb8826ce01a4af42f75af6a --- /dev/null +++ b/pages/maxwells-equations.py @@ -0,0 +1,145 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("Visualizing Maxwell's Equations") +st.write("An illustrative visualization of fundamental concepts related to Maxwell's Equations.") + +equation_choice = st.selectbox("Choose a Maxwell's Equation Concept to Visualize:", + ["Electric Field of a Point Charge (Gauss's Law for Electricity)", + "Magnetic Field of a Current-Carrying Wire (Ampere's Law)", + "Electromagnetic Plane Wave (Faraday's & Ampere-Maxwell - Simplified)"]) + +# --- Visualization Functions --- + +def electric_field_point_charge(charge_pos_x, charge_pos_y, charge_magnitude, grid_size): + """Visualizes the electric field of a point charge.""" + x = np.linspace(-grid_size, grid_size, 30) + y = np.linspace(-grid_size, grid_size, 30) + X, Y = np.meshgrid(x, y) + + Ex = np.zeros_like(X) + Ey = np.zeros_like(Y) + + for i in range(X.shape[0]): + for j in range(X.shape[1]): + r_vec = np.array([X[i, j] - charge_pos_x, Y[i, j] - charge_pos_y]) + r_mag = np.linalg.norm(r_vec) + if r_mag > 0.1: # Avoid singularity at charge location + E_direction = r_vec / r_mag + E_magnitude = charge_magnitude / (r_mag**2) #Simplified formula, ignoring constants for visualization + Ex[i, j] = E_magnitude * E_direction[0] + Ey[i, j] = E_magnitude * E_direction[1] + + return X, Y, Ex, Ey + +def magnetic_field_wire(wire_pos_x, wire_pos_y, current_magnitude, grid_size): + """Visualizes the magnetic field around a long straight wire.""" + x = np.linspace(-grid_size, grid_size, 30) + y = np.linspace(-grid_size, grid_size, 30) + X, Y = np.meshgrid(x, y) + + Bx = np.zeros_like(X) + By = np.zeros_like(Y) + Bz = np.zeros_like(X) # Magnetic field in the Z direction (out of plane) + + for i in range(X.shape[0]): + for j in range(X.shape[1]): + r_vec = np.array([X[i, j] - wire_pos_x, Y[i, j] - wire_pos_y]) + r_mag = np.linalg.norm(r_vec) + if r_mag > 0.1: # Avoid singularity at wire location + B_direction = np.array([-r_vec[1], r_vec[0]]) / r_mag # Tangential direction (right-hand rule) + B_magnitude = current_magnitude / r_mag # Simplified, ignoring constants + Bx[i, j] = B_magnitude * B_direction[0] + By[i, j] = B_magnitude * B_direction[1] + Bz[i,j] = 0 # for 2D visualization, assume B is in xy plane + + return X, Y, Bx, By, Bz + +def electromagnetic_plane_wave(time_step, grid_size, wave_direction): + """Illustrative plane EM wave propagation (simplified).""" + x = np.linspace(-grid_size, grid_size, 30) + y = np.linspace(-grid_size, grid_size, 30) + X, Y = np.meshgrid(x, y) + t = time_step + + # Simplified Plane Wave along x-axis for illustration + if wave_direction == "x": + Ex = np.sin(X - t) + Ey = np.zeros_like(X) + Ez = np.zeros_like(X) + Bx = np.zeros_like(X) + By = np.zeros_like(X) + Bz = np.sin(X - t) # B field perpendicular to E and propagation direction (along z) + elif wave_direction == "y": + Ex = np.zeros_like(X) + Ey = np.sin(Y - t) + Ez = np.zeros_like(X) + Bx = np.sin(Y - t) # B perpendicular to E and prop. direction (along x) + By = np.zeros_like(X) + Bz = np.zeros_like(X) + + + return X, Y, Ex, Ey, Ez, Bx, By, Bz + + +# --- Streamlit UI based on equation choice --- + +st.subheader("Visualization Parameters") + +if equation_choice == "Electric Field of a Point Charge (Gauss's Law for Electricity)": + charge_pos_x = st.slider("Charge Position X", -5.0, 5.0, 0.0) + charge_pos_y = st.slider("Charge Position Y", -5.0, 5.0, 0.0) + charge_magnitude = st.slider("Charge Magnitude", -5.0, 5.0, 1.0) + grid_size = st.slider("Grid Size", 5, 15, 10) + + X, Y, Ex, Ey = electric_field_point_charge(charge_pos_x, charge_pos_y, charge_magnitude, grid_size) + + fig, ax = plt.subplots() + q = ax.quiver(X, Y, Ex, Ey, color='r', label='Electric Field (E)') + ax.plot(charge_pos_x, charge_pos_y, 'ro', markersize=10, label='Point Charge') + ax.set_title("Electric Field of a Point Charge") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.axis('equal') + ax.legend() + st.pyplot(fig) + +elif equation_choice == "Magnetic Field of a Current-Carrying Wire (Ampere's Law)": + wire_pos_x = st.slider("Wire Position X", -5.0, 5.0, 0.0) + wire_pos_y = st.slider("Wire Position Y", -5.0, 5.0, 0.0) + current_magnitude = st.slider("Current Magnitude (out of plane)", -5.0, 5.0, 1.0) + grid_size = st.slider("Grid Size", 5, 15, 10) + + X, Y, Bx, By, Bz = magnetic_field_wire(wire_pos_x, wire_pos_y, current_magnitude, grid_size) + + fig, ax = plt.subplots() + q = ax.quiver(X, Y, Bx, By, color='b', label='Magnetic Field (B)') + ax.plot(wire_pos_x, wire_pos_y, 'ko', markersize=10, label='Wire (Current out of plane)') + ax.set_title("Magnetic Field of a Current-Carrying Wire") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.axis('equal') + ax.legend() + st.pyplot(fig) + +elif equation_choice == "Electromagnetic Plane Wave (Faraday's & Ampere-Maxwell - Simplified)": + wave_direction = st.selectbox("Wave Propagation Direction", ["x", "y"]) + grid_size = st.slider("Grid Size", 5, 15, 10) + + placeholder = st.empty() # For animation + + for t in range(0, 100): # Animation loop + X, Y, Ex, Ey, Ez, Bx, By, Bz = electromagnetic_plane_wave(t * 0.1, grid_size, wave_direction) # Time step + fig, ax = plt.subplots() + 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 + 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 + ax.set_title("Electromagnetic Plane Wave Propagation") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.axis('equal') + ax.legend() + placeholder.pyplot(fig) # Update plot in placeholder + time.sleep(0.1) # Control animation speed + plt.close(fig) # Clear figure for next frame \ No newline at end of file diff --git a/pages/mechanical-advantage.py b/pages/mechanical-advantage.py new file mode 100644 index 0000000000000000000000000000000000000000..ec93e668fea0b7b1ab5133593aee1853030c8306 --- /dev/null +++ b/pages/mechanical-advantage.py @@ -0,0 +1,63 @@ +import streamlit as st +import matplotlib.pyplot as plt + +st.title("Mechanical Advantage Simulation: Lever") + +st.write( + """ + This simulation visualizes the concept of mechanical advantage for a simple lever. + Adjust the parameters below to see how the input force is amplified. + """ +) + +# Slider inputs for the simulation parameters +effort_arm = st.slider( + "Effort Arm Length (distance from fulcrum to applied force, in meters)", + min_value=0.1, max_value=10.0, value=5.0, step=0.1 +) +load_arm = st.slider( + "Load Arm Length (distance from fulcrum to load, in meters)", + min_value=0.1, max_value=10.0, value=2.0, step=0.1 +) +input_force = st.slider( + "Input Force (in Newtons)", + min_value=0.0, max_value=100.0, value=10.0, step=0.5 +) + +# Calculate mechanical advantage and output force +mechanical_advantage = effort_arm / load_arm +output_force = input_force * mechanical_advantage + +st.markdown(f"### Mechanical Advantage: {mechanical_advantage:.2f}") +st.markdown(f"### Output Force: {output_force:.2f} N") + +# Plotting a simple lever diagram +fig, ax = plt.subplots(figsize=(8, 3)) +ax.set_xlim(-1, effort_arm + load_arm + 1) +ax.set_ylim(-3, 3) +ax.axhline(0, color="black", linewidth=2) + +# Define positions for the fulcrum, effort, and load +fulcrum_pos = effort_arm +load_pos = fulcrum_pos + load_arm + +# Plot the fulcrum +ax.plot(fulcrum_pos, 0, marker="v", markersize=15, color="red") +ax.annotate("Fulcrum", xy=(fulcrum_pos, 0), xytext=(fulcrum_pos, -0.5), + ha="center", color="red") + +# Draw the lever as a horizontal line +ax.plot([0, load_pos], [0, 0], color="gray", linewidth=3) + +# Plot effort force arrow +ax.arrow(0, 0, fulcrum_pos * 0.8, 1.0, head_width=0.3, head_length=0.3, fc="green", ec="green") +ax.text(0, 1.2, "Effort", color="green", ha="center") + +# Plot load force arrow (direction reversed to indicate opposition) +ax.arrow(load_pos, 0, -load_arm * 0.8, -1.0, head_width=0.3, head_length=0.3, fc="blue", ec="blue") +ax.text(load_pos, -1.2, "Load", color="blue", ha="center") + +# Remove axes for clarity +ax.axis("off") + +st.pyplot(fig) diff --git a/pages/moment-of-inertia.py b/pages/moment-of-inertia.py new file mode 100644 index 0000000000000000000000000000000000000000..5c2dd963502e1a2c5ccbfb3e4a557b259a09ed63 --- /dev/null +++ b/pages/moment-of-inertia.py @@ -0,0 +1,78 @@ +import streamlit as st +import matplotlib.pyplot as plt +import numpy as np + +# Title and Introduction +st.title("Moment of Inertia Simulation") +st.markdown("### What is Moment of Inertia?") +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.") + +# Sidebar for Number of Masses +st.sidebar.title("Settings") +num_masses = st.sidebar.slider("Number of masses", 1, 5, 1) + +# Input Sliders for Masses and Positions +masses = [] +positions = [] +for i in range(num_masses): + st.subheader(f"Mass {i+1}") + col1, col2 = st.columns(2) + with col1: + m = st.slider(f"Mass (kg)", 0.1, 10.0, 1.0, key=f"m{i}") + with col2: + r = st.slider(f"Position (m)", 0.0, 1.0, 0.5, key=f"r{i}") + masses.append(m) + positions.append(r) + +# Axis of Rotation Slider +axis_pos = st.slider("Axis of rotation position (m)", 0.0, 1.0, 0.0) + +# Calculations +total_mass = sum(masses) +x_cm = sum(m * r for m, r in zip(masses, positions)) / total_mass if total_mass > 0 else 0 +I = sum(m * (r - axis_pos)**2 for m, r in zip(masses, positions)) + +# Display Formula and Results +st.write("The Moment of Inertia is calculated as:") +st.latex(r"I = \sum_{i} m_i (r_i - a)^2") +st.write(f"where *a* is the axis position: {axis_pos} m") +st.write("**Calculation details:**") +for i, (m, r) in enumerate(zip(masses, positions)): + contribution = m * (r - axis_pos)**2 + 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²") +st.write(f"**Total Moment of Inertia: {I:.2f} kg m²**") + +# Plot Rod with Masses +fig, ax = plt.subplots() +ax.plot([0, 1], [0, 0], 'k-', linewidth=2, label="Rod") +for r in positions: + ax.plot(r, 0, 'ro', markersize=10) # Masses as red circles +ax.axvline(x=axis_pos, color='b', linestyle='--', label="Axis of Rotation") +ax.plot(x_cm, 0, 'g*', markersize=15, label="Center of Mass") +ax.set_xlim(-0.1, 1.1) +ax.set_ylim(-0.1, 0.1) +ax.set_xlabel("Position (m)") +ax.set_title("Rod with Masses") +ax.legend() +st.pyplot(fig) + +# Plot I vs. Axis Position +st.subheader("Moment of Inertia vs. Axis Position") +axis_positions = np.linspace(0, 1, 100) +I_values = [sum(m * (r - a)**2 for m, r in zip(masses, positions)) for a in axis_positions] +fig2, ax2 = plt.subplots() +ax2.plot(axis_positions, I_values, label="I vs. Axis Position") +ax2.axvline(x=x_cm, color='g', linestyle='--', label="Center of Mass") +ax2.set_xlabel("Axis Position (m)") +ax2.set_ylabel("Moment of Inertia (kg m²)") +ax2.legend() +st.pyplot(fig2) + +# Torque and Angular Acceleration +st.subheader("Apply Torque") +torque = st.number_input("Torque (N m)", 0.0, 100.0, 1.0) +if I > 0: + alpha = torque / I + st.write(f"Angular acceleration: {alpha:.2f} rad/s²") +else: + st.write("Moment of Inertia is zero, cannot calculate angular acceleration.") \ No newline at end of file diff --git a/pages/momentum.py b/pages/momentum.py new file mode 100644 index 0000000000000000000000000000000000000000..a6159bfbc541e4638fb37c30cdabec6309edf5d7 --- /dev/null +++ b/pages/momentum.py @@ -0,0 +1,151 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas + +class MomentumSimulation: + def __init__(self, mass1, velocity1, mass2, velocity2): + """ + Initialize the momentum simulation with two objects + """ + self.mass1 = mass1 + self.velocity1 = velocity1 + self.mass2 = mass2 + self.velocity2 = velocity2 + + def calculate_momentum(self): + """ + Calculate initial and final momentum + """ + initial_momentum = self.mass1 * self.velocity1 + self.mass2 * self.velocity2 + + # Simplified elastic collision calculation + v1_final = ((self.mass1 - self.mass2) * self.velocity1 + + 2 * self.mass2 * self.velocity2) / (self.mass1 + self.mass2) + + v2_final = ((self.mass2 - self.mass1) * self.velocity2 + + 2 * self.mass1 * self.velocity1) / (self.mass1 + self.mass2) + + final_momentum = self.mass1 * v1_final + self.mass2 * v2_final + + return initial_momentum, final_momentum, v1_final, v2_final + + def visualize_collision(self): + """ + Create an animation of the collision + """ + # Create figure and axis + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + fig.suptitle('Momentum Collision Simulation') + + # Initial momentum plot + initial_momentum, final_momentum, v1_final, v2_final = self.calculate_momentum() + + # Before collision plot + ax1.set_title('Before Collision') + ax1.set_xlim(-10, 10) + ax1.set_ylim(0, max(self.mass1, self.mass2) + 1) + rect1 = plt.Rectangle((-5, 1), 1, self.mass1, + fc='blue', label=f'Mass 1: {self.mass1} kg') + rect2 = plt.Rectangle((5 - 1, 1), 1, self.mass2, + fc='red', label=f'Mass 2: {self.mass2} kg') + + ax1.add_patch(rect1) + ax1.add_patch(rect2) + + # Velocity annotations + ax1.annotate(f'v1: {self.velocity1} m/s', xy=(-4.5, 0.5), + xytext=(-4.5, 0.5), color='blue') + ax1.annotate(f'v2: {self.velocity2} m/s', xy=(5.5, 0.5), + xytext=(5.5, 0.5), color='red') + + ax1.legend() + ax1.set_xlabel('Position (m)') + ax1.set_ylabel('Mass') + + # After collision plot + ax2.set_title('After Collision') + ax2.set_xlim(-10, 10) + ax2.set_ylim(0, max(self.mass1, self.mass2) + 1) + rect1_final = plt.Rectangle((-5, 1), 1, self.mass1, + fc='blue', label=f'Mass 1: {self.mass1} kg') + rect2_final = plt.Rectangle((5 - 1, 1), 1, self.mass2, + fc='red', label=f'Mass 2: {self.mass2} kg') + + ax2.add_patch(rect1_final) + ax2.add_patch(rect2_final) + + # Final velocity annotations + ax2.annotate(f'v1 final: {v1_final:.2f} m/s', xy=(-4.5, 0.5), + xytext=(-4.5, 0.5), color='blue') + ax2.annotate(f'v2 final: {v2_final:.2f} m/s', xy=(5.5, 0.5), + xytext=(5.5, 0.5), color='red') + + ax2.legend() + ax2.set_xlabel('Position (m)') + ax2.set_ylabel('Mass') + + return fig + +def main(): + st.title('Momentum Collision Simulator') + + # Sidebar for input + st.sidebar.header('Collision Parameters') + + # Mass inputs + mass1 = st.sidebar.number_input('Mass of Object 1 (kg)', + min_value=0.1, + max_value=100.0, + value=10.0) + mass2 = st.sidebar.number_input('Mass of Object 2 (kg)', + min_value=0.1, + max_value=100.0, + value=5.0) + + # Velocity inputs + velocity1 = st.sidebar.number_input('Initial Velocity of Object 1 (m/s)', + min_value=-50.0, + max_value=50.0, + value=2.0) + velocity2 = st.sidebar.number_input('Initial Velocity of Object 2 (m/s)', + min_value=-50.0, + max_value=50.0, + value=-1.0) + + # Create simulation + simulation = MomentumSimulation(mass1, velocity1, mass2, velocity2) + + # Calculate momentum + initial_momentum, final_momentum, v1_final, v2_final = simulation.calculate_momentum() + + # Display momentum calculations + st.subheader('Momentum Analysis') + col1, col2 = st.columns(2) + + with col1: + st.metric('Initial Momentum', f'{initial_momentum:.2f} kg·m/s') + + with col2: + st.metric('Final Momentum', f'{final_momentum:.2f} kg·m/s') + + # Visualization + st.subheader('Collision Visualization') + fig = simulation.visualize_collision() + st.pyplot(fig) + + # Additional explanation + st.markdown(""" + ### Understanding Momentum + + **Momentum** is the product of an object's mass and velocity (p = mv). + In this simulation: + - The total momentum before and after the collision remains constant (Conservation of Momentum) + - The objects exchange velocities based on their masses + - Positive velocity indicates movement to the right + - Negative velocity indicates movement to the left + """) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pages/motion.py b/pages/motion.py new file mode 100644 index 0000000000000000000000000000000000000000..4ca3d8674c0eebfe3b3dc8fce72c64e09951b80a --- /dev/null +++ b/pages/motion.py @@ -0,0 +1,256 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation +import math + +class MotionSimulation: + def __init__(self): + # Set up the main page + st.title("Physics Motion Visualization") + + # Sidebar for motion type selection + self.motion_type = st.sidebar.selectbox( + "Select Motion Type", + [ + "Uniform Motion", + "Uniformly Accelerated Motion", + "Projectile Motion", + "Simple Harmonic Motion", + "Circular Motion" + ] + ) + + # Call the appropriate visualization method + if hasattr(self, self.motion_type.replace(" ", "_").lower()): + getattr(self, self.motion_type.replace(" ", "_").lower())() + + def uniform_motion(self): + """Visualize uniform motion with constant velocity""" + st.header("Uniform Motion Simulation") + + # User inputs + velocity = st.slider("Velocity (m/s)", min_value=1, max_value=20, value=5) + time_range = st.slider("Time Range (s)", min_value=1, max_value=20, value=10) + + # Calculations + time = np.linspace(0, time_range, 100) + position = velocity * time + + # Plotting + fig, ax = plt.subplots() + ax.plot(time, position) + ax.set_title("Position vs Time") + ax.set_xlabel("Time (s)") + ax.set_ylabel("Position (m)") + ax.grid(True) + + st.pyplot(fig) + + # Explanation + st.markdown(""" + ### Uniform Motion Explanation + - In uniform motion, the velocity remains constant + - Position changes linearly with time + - The graph is a straight line with slope equal to velocity + """) + + def uniformly_accelerated_motion(self): + """Visualize motion with constant acceleration""" + st.header("Uniformly Accelerated Motion") + + # User inputs + initial_velocity = st.slider("Initial Velocity (m/s)", min_value=0, max_value=10, value=0) + acceleration = st.slider("Acceleration (m/s²)", min_value=-10, max_value=10, value=2) + time_range = st.slider("Time Range (s)", min_value=1, max_value=20, value=10) + + # Calculations + time = np.linspace(0, time_range, 100) + position = initial_velocity * time + 0.5 * acceleration * time**2 + velocity = initial_velocity + acceleration * time + + # Create two subplots + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6)) + + # Position plot + ax1.plot(time, position) + ax1.set_title("Position vs Time") + ax1.set_xlabel("Time (s)") + ax1.set_ylabel("Position (m)") + ax1.grid(True) + + # Velocity plot + ax2.plot(time, velocity) + ax2.set_title("Velocity vs Time") + ax2.set_xlabel("Time (s)") + ax2.set_ylabel("Velocity (m/s)") + ax2.grid(True) + + plt.tight_layout() + st.pyplot(fig) + + # Explanation + st.markdown(""" + ### Uniformly Accelerated Motion Explanation + - Velocity changes linearly with time + - Position changes quadratically with time + - Follows equations: + * v = v₀ + at + * x = x₀ + v₀t + ½at² + """) + + def projectile_motion(self): + """Simulate projectile motion""" + st.header("Projectile Motion Simulation") + + # User inputs + initial_velocity = st.slider("Initial Velocity (m/s)", min_value=1, max_value=50, value=20) + angle = st.slider("Launch Angle (degrees)", min_value=0, max_value=90, value=45) + + # Convert angle to radians + theta = math.radians(angle) + + # Gravity constant + g = 9.8 + + # Calculate flight parameters + time_of_flight = 2 * initial_velocity * math.sin(theta) / g + max_height = (initial_velocity * math.sin(theta))**2 / (2 * g) + range_distance = initial_velocity**2 * math.sin(2*theta) / g + + # Create time array + time = np.linspace(0, time_of_flight, 100) + + # Calculate x and y positions + x = initial_velocity * math.cos(theta) * time + y = initial_velocity * math.sin(theta) * time - 0.5 * g * time**2 + + # Plotting + fig, ax = plt.subplots(figsize=(10, 6)) + ax.plot(x, y) + ax.set_title("Projectile Motion Trajectory") + ax.set_xlabel("Horizontal Distance (m)") + ax.set_ylabel("Vertical Distance (m)") + ax.grid(True) + + st.pyplot(fig) + + # Display flight parameters + st.write(f"Time of Flight: {time_of_flight:.2f} s") + st.write(f"Maximum Height: {max_height:.2f} m") + st.write(f"Range: {range_distance:.2f} m") + + # Explanation + st.markdown(""" + ### Projectile Motion Explanation + - Combination of horizontal and vertical motion + - Horizontal velocity remains constant + - Vertical motion affected by gravity + - Trajectory is a parabola + """) + + def simple_harmonic_motion(self): + """Simulate simple harmonic motion""" + st.header("Simple Harmonic Motion") + + # User inputs + amplitude = st.slider("Amplitude (m)", min_value=0.1, max_value=5.0, value=1.0, step=0.1) + frequency = st.slider("Frequency (Hz)", min_value=0.1, max_value=5.0, value=1.0, step=0.1) + + # Calculations + time = np.linspace(0, 10, 300) + displacement = amplitude * np.sin(2 * np.pi * frequency * time) + velocity = amplitude * 2 * np.pi * frequency * np.cos(2 * np.pi * frequency * time) + acceleration = -amplitude * (2 * np.pi * frequency)**2 * np.sin(2 * np.pi * frequency * time) + + # Create three subplots + fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(8, 10)) + + # Displacement plot + ax1.plot(time, displacement) + ax1.set_title("Displacement vs Time") + ax1.set_xlabel("Time (s)") + ax1.set_ylabel("Displacement (m)") + ax1.grid(True) + + # Velocity plot + ax2.plot(time, velocity) + ax2.set_title("Velocity vs Time") + ax2.set_xlabel("Time (s)") + ax2.set_ylabel("Velocity (m/s)") + ax2.grid(True) + + # Acceleration plot + ax3.plot(time, acceleration) + ax3.set_title("Acceleration vs Time") + ax3.set_xlabel("Time (s)") + ax3.set_ylabel("Acceleration (m/s²)") + ax3.grid(True) + + plt.tight_layout() + st.pyplot(fig) + + # Explanation + st.markdown(""" + ### Simple Harmonic Motion Explanation + - Oscillatory motion around an equilibrium position + - Displacement follows a sine wave + - Velocity follows a cosine wave + - Acceleration follows a negative sine wave + """) + + def circular_motion(self): + """Simulate circular motion""" + st.header("Circular Motion Simulation") + + # User inputs + radius = st.slider("Radius (m)", min_value=0.1, max_value=5.0, value=1.0, step=0.1) + angular_velocity = st.slider("Angular Velocity (rad/s)", min_value=0.1, max_value=5.0, value=1.0, step=0.1) + + # Calculations + time = np.linspace(0, 10, 300) + x = radius * np.cos(angular_velocity * time) + y = radius * np.sin(angular_velocity * time) + + # Plotting + fig, ax = plt.subplots(figsize=(8, 8)) + ax.plot(x, y) + ax.set_title("Circular Motion Trajectory") + ax.set_xlabel("X Position (m)") + ax.set_ylabel("Y Position (m)") + ax.set_aspect('equal') + ax.grid(True) + + # Draw the circle + circle = plt.Circle((0, 0), radius, fill=False, color='r', linestyle='--') + ax.add_artist(circle) + + st.pyplot(fig) + + # Instantaneous quantities + st.write(f"Radius: {radius} m") + st.write(f"Angular Velocity: {angular_velocity} rad/s") + st.write(f"Linear Velocity: {radius * angular_velocity:.2f} m/s") + st.write(f"Centripetal Acceleration: {(radius * angular_velocity**2):.2f} m/s²") + + # Explanation + st.markdown(""" + ### Circular Motion Explanation + - Object moves in a circular path + - Constant speed but changing velocity direction + - Requires centripetal acceleration towards the center + - Angular velocity determines rotation speed + """) + +def main(): + MotionSimulation() + +if __name__ == "__main__": + main() + +# How to run: +# 1. Save this script as motion_simulation.py +# 2. Install required libraries: +# pip install streamlit numpy matplotlib +# 3. Run the simulation: +# streamlit run motion_simulation.py \ No newline at end of file diff --git a/pages/newtons-law.py b/pages/newtons-law.py new file mode 100644 index 0000000000000000000000000000000000000000..70cbaafd73c568df6d2d5000da0e06bc3fce31ab --- /dev/null +++ b/pages/newtons-law.py @@ -0,0 +1,145 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from matplotlib.animation import PillowWriter + +# First Law: Inertia Visualization +def first_law_simulation(): + st.header("Newton's First Law: Law of Inertia") + st.write("An object at rest stays at rest, and an object in motion stays in motion unless acted upon by an external force.") + + # Simulation parameters + friction_options = st.radio("Select Friction Condition:", + ["No Friction", "With Friction"]) + + # Create figure and axis + fig, ax = plt.subplots(figsize=(10, 4)) + + if friction_options == "No Friction": + st.write("In this simulation, the object continues moving at constant velocity with no external forces.") + # Scenario with no friction + x = np.linspace(0, 10, 100) + y = np.ones_like(x) * 5 # Horizontal line at constant height + + ax.plot(x, y, 'b-', linewidth=2) + ax.plot(0, 5, 'ro', markersize=10) # Object + ax.set_xlim(0, 10) + ax.set_ylim(0, 10) + ax.set_title("Object Moving at Constant Velocity") + ax.set_xlabel("Distance") + ax.set_ylabel("Position") + + else: + st.write("In this simulation, friction gradually slows down the object.") + # Scenario with friction + x = np.linspace(0, 10, 100) + y = np.linspace(5, 5, 100) + + # Declining velocity simulation + velocities = np.linspace(1, 0, 100) + + ax.plot(x, y, 'b-', linewidth=2) + point, = ax.plot(0, 5, 'ro', markersize=10) + + def update(frame): + point.set_xdata(x[frame]) + return point, + + ani = animation.FuncAnimation(fig, update, frames=100, interval=50, blit=True) + st.pyplot(fig) + st.write("Note how the object slows down due to friction.") + +# Second Law: Force = Mass * Acceleration +def second_law_simulation(): + st.header("Newton's Second Law: F = ma") + st.write("The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass.") + + # Sliders for mass and force + mass = st.slider("Mass (kg)", 1, 10, 5) + force = st.slider("Applied Force (N)", 1, 50, 25) + + # Calculate acceleration + acceleration = force / mass + + # Visualization + fig, ax = plt.subplots(figsize=(10, 6)) + + # Create bar chart + labels = ['Mass', 'Force', 'Acceleration'] + values = [mass, force, acceleration] + colors = ['blue', 'green', 'red'] + + ax.bar(labels, values, color=colors) + ax.set_title("Relationship between Mass, Force, and Acceleration") + ax.set_ylabel("Value") + + st.pyplot(fig) + + # Explanation + st.write(f"With a mass of {mass} kg and a force of {force} N:") + st.write(f"Acceleration = {force} N ÷ {mass} kg = {acceleration:.2f} m/s²") + +# Third Law: Action-Reaction +def third_law_simulation(): + st.header("Newton's Third Law: Action-Reaction") + st.write("For every action, there is an equal and opposite reaction.") + + # Interaction type selection + interaction = st.selectbox("Select Interaction:", + ["Rocket Propulsion", "Walking", "Collision"]) + + fig, ax = plt.subplots(figsize=(10, 6)) + + if interaction == "Rocket Propulsion": + st.write("Rocket launches by expelling gas in the opposite direction.") + ax.arrow(5, 5, 0, 2, head_width=0.3, head_length=0.3, fc='blue', ec='blue') + ax.arrow(5, 5, 0, -2, head_width=0.3, head_length=0.3, fc='red', ec='red') + ax.set_title("Rocket Propulsion: Action and Reaction Forces") + + elif interaction == "Walking": + st.write("Person walks by pushing ground backward.") + ax.arrow(4, 5, 1, 0, head_width=0.3, head_length=0.3, fc='blue', ec='blue') + ax.arrow(6, 5, -1, 0, head_width=0.3, head_length=0.3, fc='red', ec='red') + ax.set_title("Walking: Ground Pushes Back") + + else: # Collision + st.write("Collision between two objects with equal and opposite forces.") + ax.arrow(3, 5, 1, 0, head_width=0.3, head_length=0.3, fc='blue', ec='blue') + ax.arrow(7, 5, -1, 0, head_width=0.3, head_length=0.3, fc='red', ec='red') + ax.set_title("Collision: Equal and Opposite Forces") + + ax.set_xlim(0, 10) + ax.set_ylim(0, 10) + ax.set_xticks([]) + ax.set_yticks([]) + + st.pyplot(fig) + +# Main Streamlit App +def main(): + st.title("Newton's Laws of Motion Interactive Visualization") + + law_selection = st.sidebar.radio( + "Select a Law to Visualize", + ["First Law (Inertia)", + "Second Law (F = ma)", + "Third Law (Action-Reaction)"] + ) + + if law_selection == "First Law (Inertia)": + first_law_simulation() + elif law_selection == "Second Law (F = ma)": + second_law_simulation() + else: + third_law_simulation() + +if __name__ == "__main__": + main() + +# Instructions for running: +# 1. Save this script as newtons_laws_simulation.py +# 2. Install required libraries: +# pip install streamlit numpy matplotlib +# 3. Run the app: +# streamlit run newtons_laws_simulation.py \ No newline at end of file diff --git a/pages/non-linear-dynamics.py b/pages/non-linear-dynamics.py new file mode 100644 index 0000000000000000000000000000000000000000..7c1cceb269f0814c6f1af7e480048a5e74da185a --- /dev/null +++ b/pages/non-linear-dynamics.py @@ -0,0 +1,42 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from scipy.integrate import odeint + +st.title("Lorenz Attractor Simulation") +st.write("Visualize the chaotic behavior of a nonlinear dynamical system by adjusting the parameters below:") + +# Parameter sliders +sigma = st.slider("Sigma (σ)", min_value=0.1, max_value=20.0, value=10.0, step=0.1) +rho = st.slider("Rho (ρ)", min_value=0.1, max_value=50.0, value=28.0, step=0.1) +beta = st.slider("Beta (β)", min_value=0.1, max_value=10.0, value=8/3, step=0.1) + +st.write(f"Current Parameters: σ = {sigma}, ρ = {rho}, β = {beta}") + +# Define the Lorenz system of differential equations +def lorenz(state, t, sigma, beta, rho): + x, y, z = state + dxdt = sigma * (y - x) + dydt = x * (rho - z) - y + dzdt = x * y - beta * z + return [dxdt, dydt, dzdt] + +# Time span for the simulation +t = np.linspace(0, 40, 10000) +# Initial conditions for x, y, z +state0 = [1.0, 1.0, 1.0] + +# Integrate the Lorenz equations over time t +states = odeint(lorenz, state0, t, args=(sigma, beta, rho)) +x, y, z = states[:, 0], states[:, 1], states[:, 2] + +# Create a 3D plot of the Lorenz attractor +fig = plt.figure(figsize=(10, 7)) +ax = fig.add_subplot(111, projection='3d') +ax.plot(x, y, z, lw=0.5) +ax.set_title("Lorenz Attractor") +ax.set_xlabel("X Axis") +ax.set_ylabel("Y Axis") +ax.set_zlabel("Z Axis") + +st.pyplot(fig) diff --git a/pages/ohm-law.py b/pages/ohm-law.py new file mode 100644 index 0000000000000000000000000000000000000000..98d562f138d59241bd8177fd9be7cdd9843ed344 --- /dev/null +++ b/pages/ohm-law.py @@ -0,0 +1,247 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation +import matplotlib.patches as patches +from matplotlib.collections import PatchCollection + +# Set page configuration +st.set_page_config(page_title="Electric Current & Ohm's Law Simulation", + page_icon="⚡", + layout="wide") + +# Title and introduction +st.title("⚡ Electric Current & Ohm's Law Visualization") +st.markdown(""" +This interactive simulation demonstrates the fundamental principles of electric current and Ohm's Law. +Adjust the voltage and resistance values to see how they affect current flow in a circuit. + +**Ohm's Law**: $V = I \\times R$, where: +- $V$ is voltage (in volts, V) +- $I$ is current (in amperes, A) +- $R$ is resistance (in ohms, Ω) +""") + +# Create sidebar for controls +st.sidebar.header("Circuit Controls") + +# Input widgets for voltage and resistance +voltage = st.sidebar.slider("Voltage (V)", min_value=0.0, max_value=24.0, value=12.0, step=0.1) +resistance = st.sidebar.slider("Resistance (Ω)", min_value=0.1, max_value=100.0, value=10.0, step=0.1) + +# Calculate current using Ohm's Law +if resistance > 0: + current = voltage / resistance +else: + current = 0 # Prevent division by zero + +# Display the calculated values +st.sidebar.markdown("### Calculated Values") +st.sidebar.info(f"Current (I): {current:.2f} A") +st.sidebar.info(f"Power (P = V × I): {voltage * current:.2f} W") + +# Main display area with two columns +col1, col2 = st.columns([3, 2]) + +with col1: + st.subheader("Circuit Visualization") + + # Create figure for the circuit visualization + fig_circuit, ax_circuit = plt.subplots(figsize=(10, 6)) + + # Draw circuit components + # Battery + battery_rect = patches.Rectangle((0.1, 0.4), 0.1, 0.2, fill=True, color='red') + battery_line1 = plt.Line2D([0.1, 0.1], [0.4, 0.6], lw=2, color='black') + battery_line2 = plt.Line2D([0.2, 0.2], [0.35, 0.65], lw=2, color='black') + ax_circuit.add_patch(battery_rect) + ax_circuit.add_line(battery_line1) + ax_circuit.add_line(battery_line2) + ax_circuit.text(0.15, 0.3, f"{voltage}V", ha='center') + + # Resistor + resistor_x = 0.6 + resistor_y = 0.5 + resistor_width = 0.2 + resistor_height = 0.1 + + resistor = patches.Rectangle((resistor_x, resistor_y - resistor_height/2), + resistor_width, resistor_height, + fill=True, color='#FFA500') + ax_circuit.add_patch(resistor) + ax_circuit.text(resistor_x + resistor_width/2, resistor_y - 0.1, + f"{resistance}Ω", ha='center') + + # Wires + wire1 = plt.Line2D([0.2, resistor_x], [0.5, 0.5], lw=2, color='black') + wire2 = plt.Line2D([resistor_x + resistor_width, 0.9], [0.5, 0.5], lw=2, color='black') + wire3 = plt.Line2D([0.9, 0.9], [0.5, 0.2], lw=2, color='black') + wire4 = plt.Line2D([0.9, 0.1], [0.2, 0.2], lw=2, color='black') + wire5 = plt.Line2D([0.1, 0.1], [0.2, 0.4], lw=2, color='black') + + ax_circuit.add_line(wire1) + ax_circuit.add_line(wire2) + ax_circuit.add_line(wire3) + ax_circuit.add_line(wire4) + ax_circuit.add_line(wire5) + + # Electron animation setup + num_electrons = min(int(current * 15), 50) # Scale number of electrons with current + electrons_x = [] + electrons_y = [] + electron_positions = [] + + # Initialize electron positions along the circuit path + circuit_path = [ + # Top horizontal wire (right to left) + [(resistor_x + resistor_width + 0.01, 0.9), (0.5, 0.5)], + # Right vertical wire (top to bottom) + [(0.9, 0.9), (0.9, 0.2)], + # Bottom horizontal wire (right to left) + [(0.9, 0.1), (0.1, 0.2)], + # Left vertical wire (bottom to top) + [(0.1, 0.1), (0.1, 0.4)] + ] + + # Distribute electrons evenly across the circuit + for i in range(num_electrons): + segment = i % 4 + position = i / num_electrons + + start, end = circuit_path[segment] + x = start[0] + position * (end[0] - start[0]) + y = start[1] + position * (end[1] - start[1]) + + electrons_x.append(x) + electrons_y.append(y) + electron_positions.append((segment, position)) + + electrons = ax_circuit.scatter(electrons_x, electrons_y, s=50, + color='blue', alpha=0.7, zorder=3) + + # Set plot limits and remove axes + ax_circuit.set_xlim(0, 1) + ax_circuit.set_ylim(0, 1) + ax_circuit.axis('off') + + # Display the static circuit + st.pyplot(fig_circuit) + + # Animated version (using streamlit animation) + st.subheader("Animated Current Flow") + + # Create a placeholder for the animation + animation_placeholder = st.empty() + + # Animation function + def update_animation(frame): + electron_speed = current / 10 # Speed proportional to current + + new_x = [] + new_y = [] + + for i in range(num_electrons): + segment, pos = electron_positions[i] + + # Update position + pos += electron_speed * 0.01 + + # If position exceeds 1, move to next segment + if pos >= 1: + segment = (segment + 1) % 4 + pos = pos - 1 + + electron_positions[i] = (segment, pos) + + # Calculate x, y coordinates + start, end = circuit_path[segment] + x = start[0] + pos * (end[0] - start[0]) + y = start[1] + pos * (end[1] - start[1]) + + new_x.append(x) + new_y.append(y) + + electrons.set_offsets(np.column_stack([new_x, new_y])) + return electrons, + + # Set up the animation + ani = FuncAnimation(fig_circuit, update_animation, frames=50, + interval=50, blit=True) + + # Display the animation (as static frames for Streamlit) + for frame in range(30): + update_animation(frame) + animation_placeholder.pyplot(fig_circuit) + +with col2: + st.subheader("Ohm's Law Graph") + + # Create figure for Ohm's Law graph + fig_graph, ax_graph = plt.subplots(figsize=(8, 6)) + + # Plot I-V curve for current resistance + v_values = np.linspace(0, 24, 100) + i_values = v_values / resistance + + ax_graph.plot(v_values, i_values, 'b-', linewidth=2, label=f'R = {resistance}Ω') + + # Add current point + ax_graph.plot(voltage, current, 'ro', markersize=10) + ax_graph.text(voltage+0.5, current+0.1, f'({voltage}V, {current:.2f}A)', + fontsize=10, color='red') + + # Plot additional I-V curves for comparison + comparison_resistances = [5, 20, 50] + colors = ['g', 'm', 'c'] + + for i, r in enumerate(comparison_resistances): + if r != resistance: # Skip if it's the same as current resistance + i_values_comp = v_values / r + ax_graph.plot(v_values, i_values_comp, f'{colors[i]}--', + linewidth=1, label=f'R = {r}Ω') + + # Set labels and title + ax_graph.set_xlabel('Voltage (V)', fontsize=12) + ax_graph.set_ylabel('Current (A)', fontsize=12) + ax_graph.set_title('I-V Curve (Ohm\'s Law)', fontsize=14) + ax_graph.grid(True, linestyle='--', alpha=0.7) + ax_graph.legend(loc='upper left') + + # Set axis limits + ax_graph.set_xlim(0, 25) + y_max = max(24/5, current*1.5) # Adjust based on current value + ax_graph.set_ylim(0, y_max) + + # Display the graph + st.pyplot(fig_graph) + +# Add explanatory text at the bottom +st.markdown(""" +## How Ohm's Law Works + +1. **Voltage (V)** is the electrical pressure that pushes electrons through a circuit, measured in volts (V). +2. **Current (I)** is the flow rate of electrons, measured in amperes (A). +3. **Resistance (R)** is the opposition to current flow, measured in ohms (Ω). + +As you adjust the sliders: +- Increasing voltage while keeping resistance constant increases the current (more pressure = more flow). +- Increasing resistance while keeping voltage constant decreases the current (more resistance = less flow). +- The power (P = V × I) tells you how much energy is converted in the circuit per second. + +In the animation, the blue dots represent electrons flowing through the circuit. Their speed corresponds to the current intensity. +""") + +# Add a section about applications +st.markdown(""" +## Real-World Applications + +- **Home Electrical Systems**: Circuit breakers prevent excessive current that could cause fires. +- **Electronic Devices**: Resistors control current flow to sensitive components. +- **Power Transmission**: Engineers use Ohm's Law to design efficient power grids. +- **LED Lighting**: Current-limiting resistors protect LEDs from burning out. +- **Battery-Powered Devices**: Battery life depends on current draw, which follows Ohm's Law. +""") + +# Add footer +st.markdown("---") +st.markdown("Interactive Physics Simulation | Created with Streamlit and Matplotlib") \ No newline at end of file diff --git a/pages/oscillations.py b/pages/oscillations.py new file mode 100644 index 0000000000000000000000000000000000000000..a6076dff2fb16d77dd46ba0076f4bfc85bc1cbf8 --- /dev/null +++ b/pages/oscillations.py @@ -0,0 +1,50 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +def main(): + st.title("Oscillations Simulation") + st.write("Visualize a simple harmonic oscillator using interactive parameters.") + + # Define interactive controls for oscillator parameters. + amplitude = st.slider("Amplitude", 0.1, 10.0, 1.0, step=0.1) + frequency = st.slider("Frequency (Hz)", 0.1, 5.0, 1.0, step=0.1) + phase = st.slider("Phase (radians)", 0.0, 2*np.pi, 0.0, step=0.1) + duration = st.slider("Simulation Duration (seconds)", 1, 20, 10) + dt = 0.1 # Time step for the animation + + start = st.button("Start Simulation") + if start: + # Use an empty placeholder that will be updated for the animation. + placeholder = st.empty() + t_vals_full = np.linspace(0, duration, 500) # Time points for the full curve + + # Animation loop: update the plot for each time step. + for t in np.arange(0, duration, dt): + # Calculate current displacement using simple harmonic motion equation. + x_current = amplitude * np.cos(2 * np.pi * frequency * t + phase) + + # Compute full oscillation for plotting + x_vals_full = amplitude * np.cos(2 * np.pi * frequency * t_vals_full + phase) + + # Create the plot + fig, ax = plt.subplots() + ax.plot(t_vals_full, x_vals_full, label="Oscillation") + # Mark the current position with a red dot + ax.scatter([t], [x_current], color="red", s=100, zorder=5, label="Current Position") + + ax.set_xlabel("Time (s)") + ax.set_ylabel("Displacement") + ax.set_title("Simple Harmonic Motion") + ax.legend() + ax.set_xlim(0, duration) + # Adjust y-axis limits based on amplitude + ax.set_ylim(-amplitude - 1, amplitude + 1) + + # Update the placeholder with the new plot + placeholder.pyplot(fig) + time.sleep(dt) + +if __name__ == "__main__": + main() diff --git a/pages/phase-transitions.py b/pages/phase-transitions.py new file mode 100644 index 0000000000000000000000000000000000000000..4aa1ac79d558de67e50e2fd74508dc1adbeb72f9 --- /dev/null +++ b/pages/phase-transitions.py @@ -0,0 +1,63 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Define grid size and interaction energy +grid_size = 50 +ε = 1.0 # Energy scale for interactions + +# Initialize the grid with half the sites occupied (stored in session state) +if 'grid' not in st.session_state: + total_sites = grid_size * grid_size + num_occupied = total_sites // 2 + grid = np.zeros((grid_size, grid_size), dtype=int) + indices = np.random.choice(total_sites, num_occupied, replace=False) + grid.flat[indices] = 1 + st.session_state.grid = grid + +# Function to count occupied neighbors with periodic boundaries +def get_occupied_neighbors(grid, i, j): + return (grid[(i-1) % grid_size, j] + grid[(i+1) % grid_size, j] + + grid[i, (j-1) % grid_size] + grid[i, (j+1) % grid_size]) + +# Add a temperature slider +T = st.slider("Temperature", min_value=0.0, max_value=5.0, value=2.0, step=0.1) + +# Run Monte Carlo simulation with Kawasaki dynamics +num_steps = 1000 +for _ in range(num_steps): + # Find occupied and empty sites + occupied_indices = np.argwhere(st.session_state.grid == 1) + empty_indices = np.argwhere(st.session_state.grid == 0) + if len(occupied_indices) == 0 or len(empty_indices) == 0: + break + # Randomly select one occupied (A) and one empty (B) site + A = tuple(occupied_indices[np.random.randint(len(occupied_indices))]) + B = tuple(empty_indices[np.random.randint(len(empty_indices))]) + # Calculate energy change for swapping A and B + num_neighbors_A = get_occupied_neighbors(st.session_state.grid, *A) + num_neighbors_B = get_occupied_neighbors(st.session_state.grid, *B) + ΔE = ε * (num_neighbors_A - num_neighbors_B) + # Accept swap based on Metropolis criterion + if ΔE < 0 or np.random.rand() < np.exp(-ΔE / T): + st.session_state.grid[A] = 0 + st.session_state.grid[B] = 1 + +# Visualize the grid +fig, ax = plt.subplots() +ax.imshow(st.session_state.grid, cmap='Blues') +ax.set_title(f"Temperature = {T}") +ax.set_xticks([]) +ax.set_yticks([]) # Remove axis ticks for clarity +st.pyplot(fig) + +# Add explanatory text +st.write(""" +### What’s Happening? +This simulation uses the lattice gas model to show a liquid-gas phase transition: +- **Blue cells** represent the liquid phase (occupied sites). +- **White cells** represent the gas phase (empty sites). +- Below the critical temperature (\(T_c \approx 2.27\)), the system separates into high-density (liquid) and low-density (gas) regions. +- Above \(T_c\), it remains in a uniform mixed phase. +Adjust the temperature slider to see how the system evolves! +""") \ No newline at end of file diff --git a/pages/photoelectrc-effect.py b/pages/photoelectrc-effect.py new file mode 100644 index 0000000000000000000000000000000000000000..6e3d739cde63734ca9955da2cf597d63ed2cbfcd --- /dev/null +++ b/pages/photoelectrc-effect.py @@ -0,0 +1,86 @@ +import streamlit as st +import numpy as np +import plotly.graph_objects as go + +# Title and explanation +st.title("Photoelectric Effect Simulation") + +st.write(""" +### Explanation +The **photoelectric effect** occurs when light of frequency ν strikes a metal surface, causing electrons to be emitted if ν exceeds the threshold frequency ν₀ = φ / h, where: +- **φ** is the work function of the metal (energy required to eject an electron), +- **h** is Planck's constant. + +The **kinetic energy (KE)** of the emitted electrons is: +- KE = h(ν - ν₀) if ν > ν₀, +- KE = 0 otherwise. + +The **photoelectric current** (number of electrons emitted per second) is proportional to the light intensity I when ν > ν₀, and zero otherwise. + +For simplicity, this simulation sets h = 1 (arbitrary units), so ν₀ = φ. All quantities (frequency, work function, energy, intensity, current) are in arbitrary units to focus on the relationships. +""") + +# Sidebar with input sliders +st.sidebar.header("Adjust Parameters") +ν = st.sidebar.slider('Frequency ν', min_value=0.0, max_value=10.0, value=5.0, step=0.1) +φ = st.sidebar.slider('Work function φ', min_value=0.0, max_value=5.0, value=2.0, step=0.1) +I = st.sidebar.slider('Intensity I', min_value=0.0, max_value=10.0, value=5.0, step=0.1) + +# Constants and calculations +# h = 1.0 # Planck's constant in arbitrary units +# ν₀ = φ / h # Threshold frequency (since h=1, ν₀=φ) +# KE = max(ν - φ, 0) # Kinetic energy of emitted electrons +# current = I if ν > φ else 0 # Photoelectric current + +# Constants and calculations +h = 1.0 # Planck's constant in arbitrary units +nu0 = φ / h # Threshold frequency (since h=1, nu0=φ) +KE = max(ν - φ, 0) # Kinetic energy of emitted electrons +current = I if ν > φ else 0 # Photoelectric current + + +# Generate data for plotting +ν_range = np.linspace(0, 10, 100) +KE_range = np.maximum(ν_range - φ, 0) # KE for the range of frequencies +current_range = np.where(ν_range > φ, I, 0) # Current for the range of frequencies + +# Create interactive plot with Plotly +fig = go.Figure() + +# Add trace for Kinetic Energy +fig.add_trace(go.Scatter(x=ν_range, y=KE_range, name='Kinetic Energy', line=dict(color='blue'))) + +# Add trace for Current (using step-like behavior) +fig.add_trace(go.Scatter(x=ν_range, y=current_range, name='Current', yaxis='y2', line=dict(color='green', shape='hv'))) + +# Add vertical line at selected frequency +fig.add_vline(x=ν, line_dash='dash', line_color='red') + +# Add shaded region for ν < φ (no emission) +fig.add_shape(type='rect', x0=0, x1=φ, y0=0, y1=10, fillcolor='lightgray', opacity=0.5, layer='below') + +# Update layout with titles and axis ranges +fig.update_layout( + title='Photoelectric Effect', + xaxis_title='Frequency ν', + xaxis_range=[0, 10], + yaxis=dict(title='Kinetic Energy', range=[0, 10], titlefont=dict(color='blue'), tickfont=dict(color='blue')), + yaxis2=dict(title='Current', range=[0, 10], overlaying='y', side='right', titlefont=dict(color='green'), tickfont=dict(color='green')), + legend=dict(x=0.75, y=0.95), +) + +# Display the plot in Streamlit +st.plotly_chart(fig) + +# Display simulation results +st.subheader("Simulation Results") +st.write(f"Selected frequency ν = {ν}") +st.write(f"Work function φ = {φ}") +st.write(f"Intensity I = {I}") + +if ν > φ: + st.write("**Electrons are emitted**") + st.write(f"Kinetic energy of emitted electrons: {KE:.2f}") + st.write(f"Photoelectric current: {current:.2f}") +else: + st.write("**No electrons are emitted**") \ No newline at end of file diff --git a/pages/photon.py b/pages/photon.py new file mode 100644 index 0000000000000000000000000000000000000000..560745aa7715509bbcb19af1c2554f074908ff72 --- /dev/null +++ b/pages/photon.py @@ -0,0 +1,70 @@ +import streamlit as st +import matplotlib.pyplot as plt +import numpy as np +import time + +# Set up the Streamlit interface +st.title("Photon Emission Simulation") +st.write("This simulation shows photons being emitted from a central light source and traveling outward in straight lines.") + +# Create the figure and axis +fig = plt.figure(figsize=(8, 8)) +ax = fig.add_subplot(111) +ax.set_xlim(-10, 10) +ax.set_ylim(-10, 10) +ax.set_aspect('equal') # Ensure the plot is square +ax.set_xticks([]) # Remove x-axis ticks +ax.set_yticks([]) # Remove y-axis ticks + +# Plot the light source at the origin +ax.scatter([0], [0], color='red', s=100, label='Light Source') + +# Initialize lists to store photon positions and velocities +positions = [] +velocities = [] +v = 0.5 # Speed of photons (arbitrary units per frame) + +# Create a placeholder for the plot in Streamlit +placeholder = st.empty() + +# Run the animation for 100 frames +for i in range(100): + # Add a new photon at the source with a random direction + theta = np.random.uniform(0, 2 * np.pi) # Random angle in radians + vx = v * np.cos(theta) # x-component of velocity + vy = v * np.sin(theta) # y-component of velocity + positions.append([0.0, 0.0]) # Start at origin + velocities.append([vx, vy]) + + # Update the position of each photon + for j in range(len(positions)): + positions[j][0] += velocities[j][0] + positions[j][1] += velocities[j][1] + + # Remove photons that go out of bounds (beyond x=±10 or y=±10) + indices_to_keep = [ + j for j in range(len(positions)) + if abs(positions[j][0]) < 10 and abs(positions[j][1]) < 10 + ] + positions = [positions[j] for j in indices_to_keep] + velocities = [velocities[j] for j in indices_to_keep] + + # Clear the previous plot and redraw + ax.clear() + ax.set_xlim(-10, 10) + ax.set_ylim(-10, 10) + ax.set_aspect('equal') + ax.set_xticks([]) + ax.set_yticks([]) + ax.scatter([0], [0], color='red', s=100) # Redraw the source + + # Plot the photons if there are any + if positions: + pos_array = np.array(positions) + ax.scatter(pos_array[:, 0], pos_array[:, 1], color='blue', s=10) + + # Update the plot in the Streamlit placeholder + placeholder.pyplot(fig) + + # Pause briefly to control animation speed + time.sleep(0.1) \ No newline at end of file diff --git a/pages/position.py b/pages/position.py new file mode 100644 index 0000000000000000000000000000000000000000..77c17c7b726f4a78551f6060a3ee96042c1bfaeb --- /dev/null +++ b/pages/position.py @@ -0,0 +1,90 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +def constant_velocity_motion(initial_position, velocity, time): + """Calculate position for constant velocity motion.""" + return initial_position + velocity * time + +def accelerated_motion(initial_position, initial_velocity, acceleration, time): + """Calculate position for uniformly accelerated motion.""" + return initial_position + initial_velocity * time + 0.5 * acceleration * time**2 + +def plot_position_graph(times, positions): + """Create a position vs time graph.""" + plt.figure(figsize=(10, 6)) + plt.plot(times, positions, 'b-', linewidth=2) + plt.title('Position vs Time Graph', fontsize=15) + plt.xlabel('Time (s)', fontsize=12) + plt.ylabel('Position (m)', fontsize=12) + plt.grid(True, linestyle='--', alpha=0.7) + plt.tight_layout() + return plt + +def main(): + st.title('Physics Motion Visualization: Position') + + # Sidebar for motion type selection + motion_type = st.sidebar.selectbox( + 'Select Motion Type', + ['Constant Velocity', 'Uniformly Accelerated Motion'] + ) + + # Input parameters + st.sidebar.header('Motion Parameters') + + if motion_type == 'Constant Velocity': + initial_position = st.sidebar.number_input('Initial Position (m)', value=0.0, step=0.1) + velocity = st.sidebar.number_input('Velocity (m/s)', value=1.0, step=0.1) + + # Time range for visualization + time_start = st.sidebar.number_input('Start Time (s)', value=0.0, step=0.1) + time_end = st.sidebar.number_input('End Time (s)', value=10.0, step=0.1) + time_step = st.sidebar.number_input('Time Step (s)', value=0.5, step=0.1) + + # Calculate positions + times = np.arange(time_start, time_end + time_step, time_step) + positions = [constant_velocity_motion(initial_position, velocity, t) for t in times] + + else: # Uniformly Accelerated Motion + initial_position = st.sidebar.number_input('Initial Position (m)', value=0.0, step=0.1) + initial_velocity = st.sidebar.number_input('Initial Velocity (m/s)', value=0.0, step=0.1) + acceleration = st.sidebar.number_input('Acceleration (m/s²)', value=2.0, step=0.1) + + # Time range for visualization + time_start = st.sidebar.number_input('Start Time (s)', value=0.0, step=0.1) + time_end = st.sidebar.number_input('End Time (s)', value=10.0, step=0.1) + time_step = st.sidebar.number_input('Time Step (s)', value=0.5, step=0.1) + + # Calculate positions + times = np.arange(time_start, time_end + time_step, time_step) + positions = [accelerated_motion(initial_position, initial_velocity, acceleration, t) for t in times] + + # Display graph + st.header(f'{motion_type} Visualization') + + # Plot the graph + plt = plot_position_graph(times, positions) + st.pyplot(plt) + + # Display position table + st.subheader('Position Data') + position_data = { + 'Time (s)': times, + 'Position (m)': positions + } + st.dataframe(position_data) + + # Additional explanations + st.markdown(""" + ### Understanding the Graph + - **X-axis**: Represents time in seconds + - **Y-axis**: Represents position in meters + + ### Interpretation + - In constant velocity motion, the position changes linearly with time + - In uniformly accelerated motion, the position changes quadratically with time + """) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pages/power.py b/pages/power.py new file mode 100644 index 0000000000000000000000000000000000000000..3d5b2a3402f3ff7873c988297ba955f8e665ffa0 --- /dev/null +++ b/pages/power.py @@ -0,0 +1,131 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +def calculate_power_work_time(work, time): + """Calculate power given work and time.""" + return work / time if time != 0 else 0 + +def calculate_power_force_velocity(force, velocity): + """Calculate power given force and velocity.""" + return force * velocity + +def calculate_power_torque_angular_velocity(torque, angular_velocity): + """Calculate power given torque and angular velocity.""" + return torque * angular_velocity + +def visualize_power_work_time(): + """Visualization for Power = Work / Time""" + st.subheader("Power: Work / Time") + + work = st.slider("Work (Joules)", min_value=0.0, max_value=1000.0, value=50.0, step=10.0) + time = st.slider("Time (Seconds)", min_value=0.1, max_value=60.0, value=10.0, step=0.5) + + power = calculate_power_work_time(work, time) + + st.write(f"Power = {work} J / {time} s = {power:.2f} Watts") + + # Create visualization + fig, ax = plt.subplots(figsize=(8, 4)) + + # Bar graph showing work and time + ax.bar(['Work (J)', 'Time (s)'], [work, time], color=['blue', 'green']) + ax.set_ylabel('Value') + ax.set_title('Work and Time Relationship') + + # Annotate power + ax.text(0.5, max(work, time), f'Power = {power:.2f} W', + horizontalalignment='center', verticalalignment='bottom') + + st.pyplot(fig) + +def visualize_power_force_velocity(): + """Visualization for Power = Force * Velocity""" + st.subheader("Power: Force * Velocity") + + force = st.slider("Force (Newtons)", min_value=0.0, max_value=100.0, value=50.0, step=5.0) + velocity = st.slider("Velocity (m/s)", min_value=0.0, max_value=20.0, value=10.0, step=0.5) + + power = calculate_power_force_velocity(force, velocity) + + st.write(f"Power = {force} N * {velocity} m/s = {power:.2f} Watts") + + # Create visualization + fig, ax = plt.subplots(figsize=(8, 4)) + + # Scatter plot to show force and velocity + scatter = ax.scatter([force], [velocity], c=[power], cmap='viridis', + s=500, alpha=0.7) + ax.set_xlabel('Force (N)') + ax.set_ylabel('Velocity (m/s)') + ax.set_title('Force and Velocity Relationship') + + # Colorbar to show power + plt.colorbar(scatter, ax=ax, label='Power (W)') + + st.pyplot(fig) + +def visualize_power_torque_angular_velocity(): + """Visualization for Power = Torque * Angular Velocity""" + st.subheader("Power: Torque * Angular Velocity") + + torque = st.slider("Torque (N·m)", min_value=0.0, max_value=100.0, value=50.0, step=5.0) + angular_velocity = st.slider("Angular Velocity (rad/s)", + min_value=0.0, max_value=20.0, value=10.0, step=0.5) + + power = calculate_power_torque_angular_velocity(torque, angular_velocity) + + st.write(f"Power = {torque} N·m * {angular_velocity} rad/s = {power:.2f} Watts") + + # Create visualization + fig, ax = plt.subplots(figsize=(8, 4)) + + # Line plot to show power relationship + x = np.linspace(0, torque, 100) + y = x * angular_velocity + + ax.plot(x, y, label='Power Curve', color='red') + ax.scatter([torque], [power], color='blue', s=200, label='Current Point') + + ax.set_xlabel('Torque (N·m)') + ax.set_ylabel('Power (W)') + ax.set_title('Torque and Angular Velocity Power Relationship') + ax.legend() + + st.pyplot(fig) + +def main(): + st.title("Power Visualization in Physics") + + # Introduction to Power + st.write(""" + ### Understanding Power + Power is the rate of doing work or the amount of energy transferred per unit time. + It is measured in Watts (W), which is equivalent to Joules per second (J/s). + + There are multiple ways to calculate power: + 1. Power = Work / Time + 2. Power = Force * Velocity + 3. Power = Torque * Angular Velocity + """) + + # Visualization options + visualization_option = st.selectbox( + "Select Power Visualization", + [ + "Work and Time", + "Force and Velocity", + "Torque and Angular Velocity" + ] + ) + + # Select appropriate visualization based on user choice + if visualization_option == "Work and Time": + visualize_power_work_time() + elif visualization_option == "Force and Velocity": + visualize_power_force_velocity() + else: + visualize_power_torque_angular_velocity() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pages/pressure.py b/pages/pressure.py new file mode 100644 index 0000000000000000000000000000000000000000..b26d5051560e425771a65dca001d3e0cdfd77a40 --- /dev/null +++ b/pages/pressure.py @@ -0,0 +1,48 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Set up the Streamlit page +st.title("Pressure Visualization Simulation") +st.markdown(""" +This simulation visualizes how pressure increases with depth in a fluid using the formula: + +**P = P0 + ρ g h** + +- **P0**: Atmospheric pressure (Pa) +- **ρ**: Fluid density (kg/m³) +- **g**: Gravitational acceleration (m/s²) +- **h**: Depth (m) +""") + +# User inputs via interactive sliders +P0 = st.slider("Atmospheric Pressure (P0 in Pa)", min_value=50000, max_value=150000, value=101325, step=100) +density = st.slider("Fluid Density (ρ in kg/m³)", min_value=500, max_value=2000, value=1000, step=50) +g = st.slider("Gravitational Acceleration (g in m/s²)", min_value=1.0, max_value=20.0, value=9.81, step=0.1) +max_depth = st.slider("Maximum Depth (m)", min_value=1, max_value=100, value=10, step=1) +depth_marker = st.slider("Select Depth Marker (m)", min_value=0, max_value=max_depth, value=max_depth // 2, step=1) + +# Compute the pressure distribution along the depth +depth = np.linspace(0, max_depth, num=100) +pressure = P0 + density * g * depth + +# Create a plot of pressure vs. depth +fig, ax = plt.subplots() +ax.plot(depth, pressure, label="Pressure (Pa)") +ax.set_xlabel("Depth (m)") +ax.set_ylabel("Pressure (Pa)") +ax.set_title("Pressure vs. Depth") +ax.grid(True) + +# Mark the pressure at the selected depth marker +selected_pressure = P0 + density * g * depth_marker +ax.plot(depth_marker, selected_pressure, 'ro', markersize=8, + label=f"At {depth_marker} m: {selected_pressure:.2f} Pa") +ax.legend() + +# Display the plot in Streamlit +st.pyplot(fig) + +st.markdown(""" +The graph above shows that pressure increases linearly with depth in a fluid. Use the sliders above to adjust the parameters and explore how they affect the pressure distribution. +""") diff --git a/pages/quantization.py b/pages/quantization.py new file mode 100644 index 0000000000000000000000000000000000000000..785af0f2d092574061d21ef26cafef81fee8e53d --- /dev/null +++ b/pages/quantization.py @@ -0,0 +1,35 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Set page configuration +st.set_page_config(page_title="Quantization Visualization", layout="centered") + +st.title("Visualization of Quantization: Particle in a Box") + +# Sidebar controls for interactive parameters +st.sidebar.header("Simulation Parameters") +L = st.sidebar.slider("Length of the Box (L)", min_value=1.0, max_value=10.0, value=1.0, step=0.1) +n = st.sidebar.slider("Quantum Number (n)", min_value=1, max_value=10, value=1, step=1) + +# Define spatial domain inside the box +x = np.linspace(0, L, 1000) + +# Compute the wavefunction for a particle in an infinite potential well: +# ψ_n(x) = sqrt(2/L) * sin(nπx/L) +psi = np.sqrt(2 / L) * np.sin(n * np.pi * x / L) +# Probability density is |ψ(x)|² +prob_density = psi**2 + +# Display an energy estimate (in arbitrary units, since E ∝ n² for a particle in a box) +E_n = n**2 +st.write(f"Energy of level n = {n} is proportional to {E_n} (arbitrary units)") + +# Plot the probability density +fig, ax = plt.subplots(figsize=(8, 4)) +ax.plot(x, prob_density, label=f"|ψ(x)|² for n = {n}") +ax.set_xlabel("Position x") +ax.set_ylabel("Probability Density") +ax.set_title("Particle in a Box: Probability Density") +ax.legend() +st.pyplot(fig) diff --git a/pages/quantum-decoherence.py b/pages/quantum-decoherence.py new file mode 100644 index 0000000000000000000000000000000000000000..a64d7f1226f55fcbeb0ea8c472b1126d3453eb2f --- /dev/null +++ b/pages/quantum-decoherence.py @@ -0,0 +1,42 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Set up the title and description for the app +st.title("Quantum Decoherence Simulation: Double-Slit Interference") +st.write( + """ + This simulation demonstrates how quantum decoherence affects an interference pattern. + The two wave functions represent paths through two slits. Their interference term is + scaled by a decoherence factor (D). When D = 1 the interference is maximal (fully coherent), + and when D = 0, decoherence has washed out the interference. + """ +) + +# Sidebar controls for simulation parameters +sigma = st.sidebar.slider("Gaussian width (sigma)", 0.5, 2.0, 1.0, help="Controls the spread of each wave packet.") +d = st.sidebar.slider("Slit separation (d)", 1.0, 5.0, 2.0, help="Distance of the centers of the two slits from the origin.") +k = st.sidebar.slider("Wave number (k)", 1.0, 10.0, 5.0, help="Determines the oscillation frequency of the waves.") +decoherence_factor = st.sidebar.slider("Decoherence factor (D)", 0.0, 1.0, 1.0, help="1.0 = full coherence, 0.0 = complete decoherence.") + +# Generate an array of positions along which we calculate the intensity +x = np.linspace(-10, 10, 500) + +# Define the two Gaussian wave functions with a phase factor +psi1 = np.exp(-((x - d)**2) / (2 * sigma**2)) * np.exp(1j * k * x) +psi2 = np.exp(-((x + d)**2) / (2 * sigma**2)) * np.exp(1j * k * x) + +# Compute the intensity. The interference term is scaled by the decoherence factor. +intensity = np.abs(psi1)**2 + np.abs(psi2)**2 + 2 * decoherence_factor * np.real(psi1 * np.conj(psi2)) + +# Plot the resulting interference pattern +fig, ax = plt.subplots(figsize=(8, 4)) +ax.plot(x, intensity, label=f"Decoherence factor = {decoherence_factor:.2f}") +ax.set_xlabel("Position (x)") +ax.set_ylabel("Intensity") +ax.set_title("Interference Pattern with Quantum Decoherence") +ax.legend() +ax.grid(True) + +# Render the plot in the Streamlit app +st.pyplot(fig) diff --git a/pages/quantum-entanglement.py b/pages/quantum-entanglement.py new file mode 100644 index 0000000000000000000000000000000000000000..8a4050e0f66ef0cf948a4d2ba59cab746f7fab91 --- /dev/null +++ b/pages/quantum-entanglement.py @@ -0,0 +1,89 @@ +import streamlit as st +import numpy as np +import math +import matplotlib.pyplot as plt + +st.title("Quantum Entanglement Simulation") +st.write( + """ + This simulation visualizes quantum entanglement for a pair of spin‑½ particles in a singlet state. + Adjust the measurement angles for Particle A and Particle B to see how the correlation between outcomes changes. + + In a singlet state: + - When both particles are measured along the same axis (θ = 0°), the outcomes are perfectly anti‑correlated. + - When the measurement axes differ, the probability for Particle B to be the opposite of Particle A is given by: + + P(B = –A) = cos²(θ/2) + + and the probability for getting the same result is: + + P(B = A) = sin²(θ/2) + """ +) + +# Input: measurement angles and number of trials +angle_A = st.slider("Measurement angle for Particle A (degrees)", 0, 360, 0) +angle_B = st.slider("Measurement angle for Particle B (degrees)", 0, 360, 0) +num_trials = st.number_input("Number of measurement trials", min_value=100, max_value=10000, value=1000, step=100) + +def simulate_entanglement(angle_A, angle_B, num_trials): + # Compute the effective angle difference (in radians) as the smallest difference + diff_deg = abs(angle_A - angle_B) + if diff_deg > 180: + diff_deg = 360 - diff_deg + theta = math.radians(diff_deg) + + # For a singlet state: + # P(B = -A) = cos²(theta/2) (anti-correlated outcomes) + # P(B = A) = sin²(theta/2) (correlated outcomes) + p_anti = math.cos(theta/2)**2 + p_same = math.sin(theta/2)**2 + + outcomes = [] + for _ in range(num_trials): + # Randomly assign outcome for Particle A + A = np.random.choice([1, -1]) + # Decide outcome for Particle B based on the probabilities + if np.random.rand() < p_anti: + B = -A + else: + B = A + outcomes.append((A, B)) + outcomes = np.array(outcomes) + # The correlation is the average of the product A * B + correlation = np.mean(outcomes[:, 0] * outcomes[:, 1]) + return outcomes, correlation, theta, p_anti, p_same + +outcomes, sim_corr, theta, p_anti, p_same = simulate_entanglement(angle_A, angle_B, int(num_trials)) + +# Theoretical correlation for a singlet state is -cos(theta) +theo_corr = -math.cos(theta) + +st.write("### Simulation Results") +st.write(f"Effective angle difference: {math.degrees(theta):.2f}°") +st.write(f"Probability of anti-correlated outcomes (B = -A): {p_anti:.3f}") +st.write(f"Probability of correlated outcomes (B = A): {p_same:.3f}") +st.write(f"Simulated correlation (average of A×B): {sim_corr:.3f}") +st.write(f"Theoretical correlation (-cos(theta)): {theo_corr:.3f}") + +# Count outcome occurrences for visualization +unique, counts = np.unique(outcomes, axis=0, return_counts=True) +outcome_counts = {} +for outcome, count in zip(unique, counts): + outcome_counts[tuple(outcome)] = count + +st.write("#### Outcome Counts") +st.write(outcome_counts) + +# Bar chart to display the distribution of outcomes +labels = ['(1, 1)', '(1, -1)', '(-1, 1)', '(-1, -1)'] +values = [outcome_counts.get((1, 1), 0), + outcome_counts.get((1, -1), 0), + outcome_counts.get((-1, 1), 0), + outcome_counts.get((-1, -1), 0)] +fig, ax = plt.subplots() +ax.bar(labels, values, color='skyblue') +ax.set_xlabel("Measurement outcomes (A, B)") +ax.set_ylabel("Counts") +ax.set_title("Outcome Distribution") +st.pyplot(fig) diff --git a/pages/quantum-field.py b/pages/quantum-field.py new file mode 100644 index 0000000000000000000000000000000000000000..d4a122e172c719b5d71c9e12214d3e5fc66ed8ef --- /dev/null +++ b/pages/quantum-field.py @@ -0,0 +1,65 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("1D Klein–Gordon Field Simulation") +st.markdown( + """ +This simulation visualizes the dynamics of a free scalar field governed by the Klein–Gordon equation in one spatial dimension: + +$$ +\\frac{\\partial^2 \\phi}{\\partial t^2} = \\frac{\\partial^2 \\phi}{\\partial x^2} - m^2\\phi. +$$ + +This is a classical evolution of the field, which is a stepping stone toward understanding quantum field theory. + """ +) + +# Sidebar parameters for the simulation +L = st.sidebar.slider("Domain Length (L)", 10.0, 100.0, 50.0) +N = st.sidebar.slider("Number of Spatial Points", 100, 1000, 200, step=10) +dx = L / N +dt = st.sidebar.slider("Time Step (dt)", 0.001, 0.1, 0.01) +m = st.sidebar.slider("Mass (m)", 0.0, 10.0, 1.0) +sim_delay = st.sidebar.slider("Simulation Delay (sec)", 0.001, 0.1, 0.01) + +# Create spatial grid +x = np.linspace(-L/2, L/2, N) + +# Initial conditions: a Gaussian pulse centered at x=0 +phi = np.exp(-x**2) +phi_prev = phi.copy() # For leapfrog integration + +# Define the Laplacian with periodic boundary conditions using np.roll +def laplacian(phi, dx): + return (np.roll(phi, -1) - 2 * phi + np.roll(phi, 1)) / dx**2 + +# Set up the figure for plotting +fig, ax = plt.subplots() +line, = ax.plot(x, phi, lw=2) +ax.set_ylim(-1.5, 1.5) +ax.set_title("Field Configuration") +ax.set_xlabel("x") +ax.set_ylabel("$\\phi(x,t)$") +plot_placeholder = st.empty() + +# Run simulation when button is clicked +if st.button("Start Simulation"): + num_steps = 500 # Total time steps for the simulation + for i in range(num_steps): + # Leapfrog update: + # phi_next = 2*phi - phi_prev + dt^2*(phi_xx - m^2 * phi) + phi_next = 2 * phi - phi_prev + dt**2 * (laplacian(phi, dx) - m**2 * phi) + + # Update variables for next step + phi_prev = phi.copy() + phi = phi_next.copy() + + # Update the plot + line.set_ydata(phi) + ax.set_title(f"Time step: {i}") + plot_placeholder.pyplot(fig) + + # Delay to make the animation visible + time.sleep(sim_delay) diff --git a/pages/quantum-mechanics.py b/pages/quantum-mechanics.py new file mode 100644 index 0000000000000000000000000000000000000000..beac47e7436a5ddcae8767ac267c3dff94ad3e6d --- /dev/null +++ b/pages/quantum-mechanics.py @@ -0,0 +1,48 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Set page configuration +st.set_page_config(page_title="Quantum Wave Packet Simulation", layout="wide") + +st.title("Quantum Wave Packet Evolution Simulation") +st.write(""" +This simulation visualizes the time evolution of a free-particle Gaussian wave packet in one dimension. +We assume natural units where ℏ = 1 and m = 1. Adjust the parameters in the sidebar to see how the +probability density evolves with time. +""") + +# Sidebar for user inputs +st.sidebar.header("Simulation Parameters") +sigma = st.sidebar.slider("Initial width (σ)", min_value=0.1, max_value=5.0, value=1.0, step=0.1) +x0 = st.sidebar.slider("Initial position (x₀)", min_value=-10.0, max_value=10.0, value=0.0, step=0.1) +p0 = st.sidebar.slider("Initial momentum (p₀)", min_value=-5.0, max_value=5.0, value=1.0, step=0.1) +t = st.sidebar.slider("Time (t)", min_value=0.0, max_value=10.0, value=0.0, step=0.1) + +# Create a range of x values +x = np.linspace(-20, 20, 800) + +# Calculate the time-dependent width sigma_t +sigma_t = sigma * np.sqrt(1 + (t / sigma**2)**2) + +# Compute the probability density |ψ(x,t)|^2 for a free-particle Gaussian wave packet +# The center of the packet moves with velocity p0 and the packet spreads over time. +prob_density = (1 / (np.sqrt(np.pi) * sigma_t)) * np.exp(-((x - x0 - p0 * t) ** 2) / (sigma_t**2)) + +# Create the plot +fig, ax = plt.subplots(figsize=(10, 4)) +ax.plot(x, prob_density, color="blue", lw=2, label=r"$|\psi(x,t)|^2$") +ax.set_xlabel("Position (x)") +ax.set_ylabel("Probability Density") +ax.set_title("Gaussian Wave Packet Evolution") +ax.legend() +ax.grid(True) + +# Display the plot in Streamlit +st.pyplot(fig) + +# Additional explanation +st.write(""" +The simulation uses an analytic expression for a Gaussian wave packet. Notice that as time increases, +the packet not only translates (due to the initial momentum *p₀*) but also spreads, which is a hallmark of quantum evolution. +""") diff --git a/pages/reflection.py b/pages/reflection.py new file mode 100644 index 0000000000000000000000000000000000000000..3081a3b8b05ce111833c072355d1320cebc8a44b --- /dev/null +++ b/pages/reflection.py @@ -0,0 +1,65 @@ +import streamlit as st +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import Arc + +# Streamlit app title and description +st.title("Reflection Simulation") +st.write("Adjust the angle of incidence to see how the reflected ray changes according to the law of reflection.") + +# Slider for angle of incidence (in degrees) +θ = st.slider("Angle of incidence (degrees)", 0, 89, 45) + +# Calculate angles in degrees for incident and reflected rays +# Normal is at 90° (vertical), incident ray comes from the left side +φ_incident = 90 + θ # Angle of incident ray from positive x-axis +φ_reflected = 90 - θ # Angle of reflected ray from positive x-axis + +# Create Matplotlib figure +fig, ax = plt.subplots(figsize=(8, 6)) +ax.set_aspect('equal') # Ensure angles look correct +ax.set_xlim(-10, 10) +ax.set_ylim(-1, 10) # Mirror at y=0, rays above + +# Draw the mirror (horizontal line) +ax.plot([-10, 10], [0, 0], 'k-', linewidth=2, label='Mirror') + +# Draw the normal (vertical dashed line) +ax.plot([0, 0], [0, 5], 'k--', linewidth=1, label='Normal') + +# Length of rays for visualization +r = 10 + +# Calculate endpoint coordinates for incident and reflected rays +x_inc = r * np.cos(np.radians(φ_incident)) +y_inc = r * np.sin(np.radians(φ_incident)) +x_ref = r * np.cos(np.radians(φ_reflected)) +y_ref = r * np.sin(np.radians(φ_reflected)) + +# Draw incident ray (from endpoint to origin) +ax.arrow(x_inc, y_inc, -x_inc, -y_inc, head_width=0.5, head_length=0.5, fc='blue', ec='blue', label='Incident Ray') + +# Draw reflected ray (from origin to endpoint) +ax.arrow(0, 0, x_ref, y_ref, head_width=0.5, head_length=0.5, fc='red', ec='red', label='Reflected Ray') + +# Label the rays +ax.text(x_inc/2, y_inc/2, 'Incident Ray', color='blue', fontsize=10) +ax.text(x_ref/2, y_ref/2, 'Reflected Ray', color='red', fontsize=10) + +# Add angle arcs and labels +# Incident angle arc (from normal at 90° to incident ray) +arc_inc = Arc((0, 0), 4, 4, theta1=90, theta2=φ_incident, edgecolor='green', linewidth=1) +ax.add_patch(arc_inc) +ax.text(2, 2, f'θ = {θ}°', color='green', fontsize=10) + +# Reflected angle arc (from reflected ray to normal at 90°) +arc_ref = Arc((0, 0), 4, 4, theta1=φ_reflected, theta2=90, edgecolor='green', linewidth=1) +ax.add_patch(arc_ref) +ax.text(-3, 2, f'θ = {θ}°', color='green', fontsize=10) + +# Add labels for axes +ax.set_xlabel('X-axis') +ax.set_ylabel('Y-axis') + +# Display the plot in Streamlit +st.pyplot(fig) \ No newline at end of file diff --git a/pages/refraction.py b/pages/refraction.py new file mode 100644 index 0000000000000000000000000000000000000000..b06601ba9f786d236d73234b686e1beaeca5530c --- /dev/null +++ b/pages/refraction.py @@ -0,0 +1,82 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Title and description +st.title("Refraction Simulation") +st.markdown(""" +This simulation visualizes the refraction of light as it passes from one medium to another. +Adjust the indices of refraction (n1 and n2) and the angle of incidence to see how the light ray bends. +- **Red arrow**: Incident ray +- **Blue arrow**: Refracted ray (if refraction occurs) +- **Green arrow**: Reflected ray (if total internal reflection occurs) +""") + +# Input sliders +n1 = st.slider("Index of refraction of first medium (n1)", min_value=1.0, max_value=2.0, value=1.0, step=0.01) +n2 = st.slider("Index of refraction of second medium (n2)", min_value=1.0, max_value=2.0, value=1.5, step=0.01) +theta1_deg = st.slider("Angle of incidence (degrees)", min_value=0, max_value=90, value=30, step=1) + +# Convert angle to radians +theta1 = np.deg2rad(theta1_deg) + +# Check for total internal reflection and calculate theta2 +if n1 > n2 and np.sin(theta1) > n2 / n1: + total_internal_reflection = True + theta2_deg = None +else: + total_internal_reflection = False + sin_theta2 = (n1 / n2) * np.sin(theta1) + theta2 = np.arcsin(sin_theta2) + theta2_deg = np.rad2deg(theta2) + +# Create plot +fig, ax = plt.subplots(figsize=(8, 8)) +ax.set_xlim(-10, 10) +ax.set_ylim(-10, 10) +ax.set_aspect('equal') # Ensure angles are visually accurate + +# Draw interface between media +ax.axhline(0, color='gray', linewidth=2, label='Interface') + +# Fill media with colors +ax.fill_between([-10, 10], 0, 10, color='lightblue', alpha=0.3, label=f'Medium 1 (n1 = {n1:.2f})') +ax.fill_between([-10, 10], -10, 0, color='lightgreen', alpha=0.3, label=f'Medium 2 (n2 = {n2:.2f})') + +# Draw normal line +ax.axvline(0, color='gray', linestyle='--', label='Normal') + +# Incident ray (from top-left to origin) +start_x = -5 * np.sin(theta1) +start_y = 5 * np.cos(theta1) +dx_inc = -start_x # Direction to (0,0) +dy_inc = -start_y +ax.arrow(start_x, start_y, dx_inc, dy_inc, head_width=0.5, head_length=0.5, fc='r', ec='r', length_includes_head=True, label='Incident ray') + +# Refracted or reflected ray +if not total_internal_reflection: + # Refracted ray (from origin downward) + dx_ref = 5 * np.sin(theta2) + dy_ref = -5 * np.cos(theta2) + ax.arrow(0, 0, dx_ref, dy_ref, head_width=0.5, head_length=0.5, fc='b', ec='b', length_includes_head=True, label='Refracted ray') +else: + # Reflected ray (from origin upward) + dx_ref = 5 * np.sin(theta1) + dy_ref = 5 * np.cos(theta1) + ax.arrow(0, 0, dx_ref, dy_ref, head_width=0.5, head_length=0.5, fc='g', ec='g', length_includes_head=True, label='Reflected ray') + +# Add labels for media +ax.text(-9, 5, f'Medium 1\nn1 = {n1:.2f}', fontsize=10, verticalalignment='center') +ax.text(-9, -5, f'Medium 2\nn2 = {n2:.2f}', fontsize=10, verticalalignment='center') + +# Add legend +ax.legend(loc='upper right') + +# Display plot +st.pyplot(fig) + +# Display results +if total_internal_reflection: + st.write("Total Internal Reflection occurs. No refraction.") +else: + st.write(f"Angle of refraction: {theta2_deg:.2f} degrees") \ No newline at end of file diff --git a/pages/renormalization.py b/pages/renormalization.py new file mode 100644 index 0000000000000000000000000000000000000000..601f22e799db24991b9daa7caac2ce4cb6394c3f --- /dev/null +++ b/pages/renormalization.py @@ -0,0 +1,60 @@ +import numpy as np +import streamlit as st +import matplotlib.pyplot as plt + +def generate_ising_lattice(N, seed=None): + """Generate an N x N lattice with random spins ±1.""" + if seed is not None: + np.random.seed(seed) + lattice = np.random.choice([1, -1], size=(N, N)) + return lattice + +def block_spin_transform(lattice, block_size): + """ + Perform a block spin transformation: + Divide the lattice into blocks of size block_size x block_size. + Each block's spin is set to +1 if the sum of spins is non-negative, and -1 otherwise. + """ + N = lattice.shape[0] + new_size = N // block_size + new_lattice = np.zeros((new_size, new_size), dtype=int) + for i in range(new_size): + for j in range(new_size): + block = lattice[i*block_size:(i+1)*block_size, j*block_size:(j+1)*block_size] + if np.sum(block) >= 0: + new_lattice[i, j] = 1 + else: + new_lattice[i, j] = -1 + return new_lattice + +# Streamlit app layout +st.title("Renormalization Group Visualization via Block Spin Transformation") +st.write(""" +This simulation demonstrates a renormalization group (RG) transformation applied to an Ising model. +The RG step is implemented as a block spin transformation where blocks of spins are replaced by their majority. +Adjust the parameters in the sidebar to see how coarse-graining affects the lattice configuration. +""") + +# Sidebar controls for interactive simulation +N = st.sidebar.slider("Lattice Size", 64, 256, 128, step=16) +block_size = st.sidebar.slider("Block Size", 2, 16, 2, step=1) +iterations = st.sidebar.slider("Iterations", 1, 5, 1, step=1) +seed = st.sidebar.number_input("Random Seed (optional)", value=42) + +# Generate the initial lattice and perform iterative block transformations +lattice = generate_ising_lattice(N, seed) +lattice_current = lattice.copy() + +# Create subplots to display the original and renormalized lattices +fig, axs = plt.subplots(1, iterations+1, figsize=(4*(iterations+1), 4)) +axs[0].imshow(lattice, cmap="bwr", interpolation="nearest") +axs[0].set_title("Original Lattice") +axs[0].axis("off") + +for it in range(iterations): + lattice_current = block_spin_transform(lattice_current, block_size) + axs[it+1].imshow(lattice_current, cmap="bwr", interpolation="nearest") + axs[it+1].set_title(f"Iteration {it+1}") + axs[it+1].axis("off") + +st.pyplot(fig) diff --git a/pages/resonance.py b/pages/resonance.py new file mode 100644 index 0000000000000000000000000000000000000000..0b5d5b8e3a406872cbbdd59e9ff35a59bcef4b9e --- /dev/null +++ b/pages/resonance.py @@ -0,0 +1,64 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from scipy.integrate import odeint + +st.title("Resonance Visualization: Driven Damped Harmonic Oscillator") +st.write( + "This simulation demonstrates resonance in a driven, damped oscillator. " + "Use the sidebar to adjust the forcing amplitude, damping coefficient, and driving frequency. " + "Watch how the displacement over time changes, and explore the resonance curve showing the steady-state amplitude." +) + +# Sidebar parameters +F0 = st.sidebar.slider("Forcing Amplitude (F0)", min_value=0.0, max_value=10.0, value=1.0, step=0.1) +b = st.sidebar.slider("Damping Coefficient (b)", min_value=0.0, max_value=2.0, value=0.1, step=0.05) +omega_drive = st.sidebar.slider("Driving Frequency (ω)", min_value=0.0, max_value=3.0, value=1.0, step=0.1) + +# Physical parameters +m = 1.0 # mass +k = 1.0 # spring constant (thus natural frequency ω₀ = sqrt(k/m) = 1) + +# Time settings +t = np.linspace(0, 50, 1000) + +# Define the differential equation for the oscillator +def oscillator(state, t): + x, v = state + # dx/dt = v, and dv/dt comes from: m*x'' + b*x' + k*x = F0*cos(omega_drive*t) + dxdt = v + dvdt = (F0 * np.cos(omega_drive * t) - b * v - k * x) / m + return [dxdt, dvdt] + +# Initial conditions: starting from rest +state0 = [0.0, 0.0] +sol = odeint(oscillator, state0, t) + +# Plot the displacement vs time +fig, ax = plt.subplots(figsize=(10, 4)) +ax.plot(t, sol[:, 0], label="Displacement x(t)") +ax.set_xlabel("Time (s)") +ax.set_ylabel("Displacement (m)") +ax.set_title("Driven Damped Harmonic Oscillator Response") +ax.legend() +st.pyplot(fig) + +# Plot the resonance curve: steady-state amplitude vs driving frequency +st.header("Steady-State Amplitude vs Driving Frequency") +omega_range = np.linspace(0.1, 3.0, 300) +# The steady-state amplitude can be derived analytically: +# A = F0 / sqrt((k - m*ω²)² + (b*ω)²) +amplitudes = F0 / np.sqrt((k - m * omega_range**2)**2 + (b * omega_range)**2) + +fig2, ax2 = plt.subplots(figsize=(10, 4)) +ax2.plot(omega_range, amplitudes, 'r-', label="Steady-State Amplitude") +ax2.set_xlabel("Driving Frequency (ω)") +ax2.set_ylabel("Amplitude") +ax2.set_title("Resonance Curve") +ax2.legend() +st.pyplot(fig2) + +st.write( + "Notice that the amplitude peaks when the driving frequency approaches the system's natural frequency (ω ≈ 1). " + "This is the resonance phenomenon: the system responds most strongly when forced near its natural frequency." +) diff --git a/pages/rotational-motion.py b/pages/rotational-motion.py new file mode 100644 index 0000000000000000000000000000000000000000..6f5f7bb23b0bedd8004933af612d009e9e4175de --- /dev/null +++ b/pages/rotational-motion.py @@ -0,0 +1,63 @@ +import streamlit as st +import matplotlib.pyplot as plt +import numpy as np +import time + +# Set up the Streamlit app +st.title("Rotational Motion Simulation") + +# User inputs via sliders +omega0 = st.slider("Initial angular velocity (rad/s)", 0.0, 10.0, 1.0) +alpha = st.slider("Angular acceleration (rad/s²)", -5.0, 5.0, 0.5) +T = st.slider("Total time (s)", 0.0, 10.0, 5.0) + +# Create the figure and axis for plotting +fig, ax = plt.subplots() +ax.set_aspect('equal') # Ensure the disk appears circular +ax.set_xlim(-1.5, 1.5) # Set x-axis limits +ax.set_ylim(-1.5, 1.5) # Set y-axis limits +ax.set_xticks([]) # Remove x-axis ticks +ax.set_yticks([]) # Remove y-axis ticks +ax.set_title("Rotating Disk") + +# Plot the disk as a circle +theta_circle = np.linspace(0, 2 * np.pi, 100) +x_circle = np.cos(theta_circle) +y_circle = np.sin(theta_circle) +ax.plot(x_circle, y_circle, 'b-', label="Disk") + +# Plot the initial orientation line (radius) +line, = ax.plot([0, 1], [0, 0], 'r-', linewidth=2, label="Orientation") + +# Add a legend (optional, can be removed for simplicity) +ax.legend() + +# Button to start the simulation +if st.button("Run Simulation"): + # Create placeholders for the figure and text + placeholder_fig = st.empty() + placeholder_text = st.empty() + + # Simulation loop + for t in np.arange(0, T, 0.1): + # Calculate angular displacement and velocity + theta = omega0 * t + 0.5 * alpha * t**2 + omega = omega0 + alpha * t + + # Update the line position + x = np.cos(theta) + y = np.sin(theta) + line.set_data([0, x], [0, y]) + + # Update the figure in the placeholder + placeholder_fig.pyplot(fig) + + # Update the text with current values + placeholder_text.write( + f"Time: {t:.2f} s, Angle: {theta:.2f} rad, Angular velocity: {omega:.2f} rad/s" + ) + + # Pause briefly to create animation effect + time.sleep(0.1) +else: + st.write("Click the button to run the simulation.") \ No newline at end of file diff --git a/pages/scaling-laws.py b/pages/scaling-laws.py new file mode 100644 index 0000000000000000000000000000000000000000..2f62622cf91cb23117306fc8d036b05900f784bc --- /dev/null +++ b/pages/scaling-laws.py @@ -0,0 +1,75 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +st.title("Scaling Laws and Similarity Visualization") +st.markdown( + """ +This simulation demonstrates how geometric properties scale with size. +Select a shape, adjust the baseline dimension, and change the scale factor to see how the surface area and volume change. +""" +) + +# Choose the shape for the simulation. +shape = st.selectbox("Select a shape", ["Cube", "Sphere"]) + +# Select the scale factor and baseline dimension. +scale_factor = st.slider("Scale Factor", min_value=0.1, max_value=5.0, step=0.1, value=1.0) +baseline = st.slider("Baseline Dimension", min_value=0.1, max_value=10.0, step=0.1, value=1.0) + +# Compute properties based on selected shape. +if shape == "Cube": + # For a cube, side length L = baseline * scale_factor. + L = baseline * scale_factor + area = 6 * L**2 + volume = L**3 + + st.write(f"**Side Length:** {L:.2f}") + st.write(f"**Surface Area:** {area:.2f}") + st.write(f"**Volume:** {volume:.2f}") + + # Prepare data for scaling plot. + scale_vals = np.linspace(0.1, 5, 100) + side_vals = baseline * scale_vals + area_vals = 6 * side_vals**2 + volume_vals = side_vals**3 + +elif shape == "Sphere": + # For a sphere, radius R = baseline * scale_factor. + R = baseline * scale_factor + area = 4 * np.pi * R**2 + volume = (4/3) * np.pi * R**3 + + st.write(f"**Radius:** {R:.2f}") + st.write(f"**Surface Area:** {area:.2f}") + st.write(f"**Volume:** {volume:.2f}") + + # Prepare data for scaling plot. + scale_vals = np.linspace(0.1, 5, 100) + radius_vals = baseline * scale_vals + area_vals = 4 * np.pi * radius_vals**2 + volume_vals = (4/3) * np.pi * radius_vals**3 + +# Create log-log plots to visualize the power-law relationships. +fig, ax = plt.subplots(1, 2, figsize=(12, 4)) + +ax[0].loglog(scale_vals, area_vals, label="Area", lw=2) +ax[0].set_title("Area vs Scale Factor (log–log)") +ax[0].set_xlabel("Scale Factor") +ax[0].set_ylabel("Area") +ax[0].legend() + +ax[1].loglog(scale_vals, volume_vals, label="Volume", color="red", lw=2) +ax[1].set_title("Volume vs Scale Factor (log–log)") +ax[1].set_xlabel("Scale Factor") +ax[1].set_ylabel("Volume") +ax[1].legend() + +st.pyplot(fig) + +st.markdown( + """ +The plots above illustrate that the surface area scales as the square of the length (slope of 2 on a log–log plot) and the volume scales as the cube of the length (slope of 3). +Adjust the sliders to see how these scaling laws manifest for different baseline dimensions and scale factors. +""" +) diff --git a/pages/second-thermodynamic-law.py b/pages/second-thermodynamic-law.py new file mode 100644 index 0000000000000000000000000000000000000000..f08991e3624a57edb6348e10a7823276b34330b6 --- /dev/null +++ b/pages/second-thermodynamic-law.py @@ -0,0 +1,51 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("Second Law of Thermodynamics: Diffusion Simulation") +st.markdown(""" +This simulation illustrates the Second Law of Thermodynamics. +Initially, particles are confined to the left half of the container. +Over time, random motion causes them to spread out evenly. +""") + +# Sidebar controls to adjust simulation parameters +num_particles = st.sidebar.slider("Number of Particles", 100, 1000, 500, step=50) +num_steps = st.sidebar.slider("Number of Steps", 100, 2000, 500, step=50) +container_size = st.sidebar.slider("Container Size", 5, 20, 10) +diffusion_scale = st.sidebar.slider("Diffusion Scale", 0.05, 1.0, 0.1, step=0.05) +update_delay = st.sidebar.slider("Update Delay (seconds)", 0.001, 0.1, 0.01, step=0.005) + +# Button to start simulation +if st.button("Start Simulation"): + # Initialize particle positions: confined to the left half + positions = np.zeros((num_particles, 2)) + positions[:, 0] = np.random.uniform(0, container_size / 2, num_particles) + positions[:, 1] = np.random.uniform(0, container_size, num_particles) + + # Placeholder for the plot to update in each simulation step + plot_placeholder = st.empty() + + for step in range(num_steps): + # Update particle positions with a random walk (diffusion) + positions += np.random.normal(loc=0.0, scale=diffusion_scale, size=positions.shape) + + # Reflect particles off the container boundaries + positions[:, 0] = np.clip(positions[:, 0], 0, container_size) + positions[:, 1] = np.clip(positions[:, 1], 0, container_size) + + # Create a scatter plot for the current state + fig, ax = plt.subplots() + ax.scatter(positions[:, 0], positions[:, 1], s=10, color='blue') + ax.set_xlim(0, container_size) + ax.set_ylim(0, container_size) + ax.set_title(f"Step {step + 1}/{num_steps}") + ax.set_xlabel("X Position") + ax.set_ylabel("Y Position") + + # Update the plot in the Streamlit app + plot_placeholder.pyplot(fig) + + # Pause briefly to control update speed + time.sleep(update_delay) diff --git a/pages/simple-harmonic-motion.py b/pages/simple-harmonic-motion.py new file mode 100644 index 0000000000000000000000000000000000000000..448695d6ecc5519780a0b74c329a7cbfc5bffea2 --- /dev/null +++ b/pages/simple-harmonic-motion.py @@ -0,0 +1,42 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +# Set up the app title and sidebar parameters. +st.title("Simple Harmonic Motion Simulation") + +st.sidebar.header("SHM Parameters") +A = st.sidebar.slider("Amplitude (A)", 0.0, 10.0, 5.0, 0.1) +omega = st.sidebar.slider("Angular Frequency (ω)", 0.1, 10.0, 2.0, 0.1) +phase = st.sidebar.slider("Phase (φ)", 0.0, 2*np.pi, 0.0, 0.1) +duration = st.sidebar.slider("Duration (seconds)", 1, 60, 20) +dt = st.sidebar.slider("Time Step (seconds)", 0.01, 0.2, 0.05) + +# Button to start the simulation. +if st.button("Start Simulation"): + t = 0.0 + placeholder = st.empty() # Container for dynamically updating the plot. + + while t < duration: + # Calculate the displacement using the SHM formula. + x = A * np.cos(omega * t + phase) + + # Create a new figure for this time step. + fig, ax = plt.subplots(figsize=(8, 4)) + ax.set_xlim(-A-1, A+1) + ax.set_ylim(-1.5, 1.5) + ax.axhline(0, color="black", linestyle="--", linewidth=0.5) # Equilibrium line + + # Draw the oscillator as a red circle at (x, 0). + ax.plot(x, 0, "ro", markersize=15) + ax.set_title(f"Time: {t:.2f} s, x = {x:.2f}") + ax.set_xlabel("Displacement") + ax.get_yaxis().set_visible(False) # Hide the y-axis for clarity + + # Update the plot in the placeholder container. + placeholder.pyplot(fig) + + # Pause for a short duration to control the update rate. + time.sleep(dt) + t += dt diff --git a/pages/simple-machine.py b/pages/simple-machine.py new file mode 100644 index 0000000000000000000000000000000000000000..8560e752e04d4a2ba5b5ff6a3f3ae5ee2faac383 --- /dev/null +++ b/pages/simple-machine.py @@ -0,0 +1,236 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import patches + +def lever_simulation(): + st.header("Lever Simulation") + st.write("Adjust the parameters below to explore a lever system.") + + # Parameters for the lever with unique keys + lever_length = st.slider("Lever Length (m)", min_value=1.0, max_value=5.0, value=3.0, step=0.1, key="lever_length_slider") + pivot_position = st.slider("Pivot Position (m from left end)", min_value=0.0, max_value=lever_length, value=lever_length/2, step=0.1, key="pivot_slider") + + st.subheader("Left Side") + mass_left = st.number_input("Mass on Left Side (kg)", value=1.0, min_value=0.0, key="mass_left_input") + distance_left = st.slider("Distance from Pivot (m)", min_value=0.0, max_value=pivot_position, value=pivot_position/2, step=0.1, key="distance_left_slider") + + st.subheader("Right Side") + mass_right = st.number_input("Mass on Right Side (kg)", value=1.0, min_value=0.0, key="mass_right_input") + distance_right = st.slider("Distance from Pivot (m)", min_value=0.0, max_value=lever_length - pivot_position, value=(lever_length - pivot_position)/2, step=0.1, key="distance_right_slider") + + g = 9.81 # gravitational acceleration (m/s²) + torque_left = mass_left * g * distance_left + torque_right = mass_right * g * distance_right + + st.write("**Torque on Left Side:**", torque_left, "Nm") + st.write("**Torque on Right Side:**", torque_right, "Nm") + + # Create a plot for the lever + fig, ax = plt.subplots() + # Draw lever as a horizontal line + ax.plot([0, lever_length], [0, 0], color="black", linewidth=4) + # Mark the pivot point + ax.plot([pivot_position], [0], marker="o", markersize=10, color="red", label="Pivot") + + # Mark left mass position (to the left of pivot) + left_mass_position = pivot_position - distance_left + ax.plot([left_mass_position], [0], marker="o", markersize=10, color="blue", label="Left Mass") + ax.arrow(left_mass_position, 0, 0, -0.5, head_width=0.05, head_length=0.1, fc='blue', ec='blue') + + # Mark right mass position (to the right of pivot) + right_mass_position = pivot_position + distance_right + ax.plot([right_mass_position], [0], marker="o", markersize=10, color="green", label="Right Mass") + ax.arrow(right_mass_position, 0, 0, -0.5, head_width=0.05, head_length=0.1, fc='green', ec='green') + + ax.set_xlim(-0.5, lever_length + 0.5) + ax.set_ylim(-1, 1) + ax.set_aspect("equal") + ax.set_title("Lever Diagram") + ax.legend() + st.pyplot(fig) + +def inclined_plane_simulation(): + st.header("Inclined Plane Simulation") + st.write("Adjust the parameters below to explore an inclined plane.") + + angle_deg = st.slider("Angle of Incline (degrees)", min_value=0, max_value=60, value=30, step=1, key="angle_slider") + length = st.slider("Length of the Inclined Plane (m)", min_value=1.0, max_value=10.0, value=5.0, step=0.1, key="length_slider") + friction_coeff = st.slider("Coefficient of Friction", min_value=0.0, max_value=1.0, value=0.2, step=0.05, key="friction_slider") + mass_block = st.number_input("Mass of the Block (kg)", value=1.0, min_value=0.0, key="mass_block_input") + + angle_rad = np.deg2rad(angle_deg) + height = length * np.sin(angle_rad) + base = length * np.cos(angle_rad) + + g = 9.81 + weight = mass_block * g + force_down = weight * np.sin(angle_rad) + friction_force = friction_coeff * weight * np.cos(angle_rad) + net_force = force_down - friction_force + + st.write("**Block Weight:**", weight, "N") + st.write("**Component of Weight Down the Plane:**", force_down, "N") + st.write("**Friction Force:**", friction_force, "N") + st.write("**Net Force on Block:**", net_force, "N") + + # Plotting the inclined plane + fig, ax = plt.subplots() + x_vals = [0, base] + y_vals = [0, height] + ax.plot(x_vals, y_vals, color="black", linewidth=4) + + # Draw the block as a rotated rectangle placed halfway along the plane + block_pos = 0.5 * length + block_x = block_pos * np.cos(angle_rad) + block_y = block_pos * np.sin(angle_rad) + block_width = 0.5 + block_height = 0.3 + transform = plt.matplotlib.transforms.Affine2D().rotate_deg_around(block_x, block_y, angle_deg) + ax.transData + block = patches.Rectangle((block_x - block_width/2, block_y - block_height/2), block_width, block_height, transform=transform, color="blue") + ax.add_patch(block) + + ax.set_xlim(-1, base + 1) + ax.set_ylim(-1, height + 1) + ax.set_aspect("equal") + ax.set_title("Inclined Plane Diagram") + st.pyplot(fig) + +def pulley_simulation(): + st.header("Pulley Simulation") + st.write("This simulation visualizes a simple pulley system with two masses.") + + mass_left = st.number_input("Mass on Left Side (kg)", value=2.0, min_value=0.0, key="pulley_mass_left") + mass_right = st.number_input("Mass on Right Side (kg)", value=1.0, min_value=0.0, key="pulley_mass_right") + g = 9.81 + weight_left = mass_left * g + weight_right = mass_right * g + net_force = abs(weight_left - weight_right) + total_mass = mass_left + mass_right if (mass_left + mass_right) > 0 else 1 + acceleration = net_force / total_mass + + st.write("**Acceleration of the System:**", acceleration, "m/s²") + + # Plotting the pulley system + fig, ax = plt.subplots() + pulley_center = (0, 0) + pulley_radius = 0.5 + # Draw the pulley + pulley_circle = plt.Circle(pulley_center, pulley_radius, color='black', fill=False, linewidth=3) + ax.add_patch(pulley_circle) + + # Draw the ropes for both sides + ax.plot([pulley_center[0] - pulley_radius, pulley_center[0] - pulley_radius], + [pulley_center[1] - pulley_radius, pulley_center[1] - pulley_radius - 2], + color="black", linewidth=2) + ax.plot([pulley_center[0] + pulley_radius, pulley_center[0] + pulley_radius], + [pulley_center[1] - pulley_radius, pulley_center[1] - pulley_radius - 2], + color="black", linewidth=2) + + # Draw masses as rectangles + left_rect = patches.Rectangle((pulley_center[0] - pulley_radius - 0.3, pulley_center[1] - pulley_radius - 2 - 0.3), + 0.6, 0.6, color="blue") + right_rect = patches.Rectangle((pulley_center[0] + pulley_radius - 0.3, pulley_center[1] - pulley_radius - 2 - 0.3), + 0.6, 0.6, color="green") + ax.add_patch(left_rect) + ax.add_patch(right_rect) + + ax.set_xlim(-3, 3) + ax.set_ylim(-5, 2) + ax.set_aspect("equal") + ax.set_title("Pulley Diagram") + st.pyplot(fig) + +def wheel_axle_simulation(): + st.header("Wheel and Axle Simulation") + st.write("Adjust the parameters below to explore the wheel and axle mechanism.") + + wheel_radius = st.slider("Wheel Radius (m)", min_value=0.5, max_value=2.0, value=1.0, step=0.1, key="wheel_radius_slider") + axle_radius = st.slider("Axle Radius (m)", min_value=0.1, max_value=wheel_radius, value=0.3, step=0.05, key="axle_radius_slider") + + mech_adv = wheel_radius / axle_radius + st.write("**Mechanical Advantage (Wheel Radius / Axle Radius):**", mech_adv) + + fig, ax = plt.subplots() + # Draw the wheel + wheel = plt.Circle((0, 0), wheel_radius, color='black', fill=False, linewidth=3) + ax.add_patch(wheel) + # Draw the axle + axle = plt.Circle((0, 0), axle_radius, color='gray', fill=True) + ax.add_patch(axle) + + ax.set_xlim(-wheel_radius-0.5, wheel_radius+0.5) + ax.set_ylim(-wheel_radius-0.5, wheel_radius+0.5) + ax.set_aspect("equal") + ax.set_title("Wheel and Axle Diagram") + st.pyplot(fig) + +def wedge_simulation(): + st.header("Wedge Simulation") + st.write("Adjust the parameters below to visualize a wedge. A wedge converts a force applied at its blunt end into forces perpendicular to its inclined surfaces.") + + wedge_length = st.slider("Wedge Length (m)", min_value=0.5, max_value=3.0, value=2.0, step=0.1, key="wedge_length_slider") + wedge_height = st.slider("Wedge Height (m)", min_value=0.1, max_value=2.0, value=0.5, step=0.1, key="wedge_height_slider") + + fig, ax = plt.subplots() + # Draw the wedge as a filled triangle + triangle = np.array([[0, 0], [wedge_length, 0], [0, wedge_height]]) + ax.fill(triangle[:, 0], triangle[:, 1], color="brown", alpha=0.5) + + ax.set_xlim(-0.5, wedge_length + 0.5) + ax.set_ylim(-0.5, wedge_height + 0.5) + ax.set_aspect("equal") + ax.set_title("Wedge Diagram") + st.pyplot(fig) + +def screw_simulation(): + st.header("Screw Simulation") + st.write("Adjust the parameters below to visualize a screw, which is essentially an inclined plane wrapped around a cylinder.") + + screw_length = st.slider("Screw Length (m)", min_value=0.1, max_value=1.0, value=0.5, step=0.05, key="screw_length_slider") + screw_diameter = st.slider("Screw Diameter (m)", min_value=0.01, max_value=0.2, value=0.05, step=0.005, key="screw_diameter_slider") + pitch = st.slider("Pitch (distance between threads, m)", min_value=0.005, max_value=0.1, value=0.02, step=0.005, key="pitch_slider") + + screw_radius = screw_diameter / 2 + mech_adv = (2 * np.pi * screw_radius) / pitch + st.write("**Mechanical Advantage:**", mech_adv) + + fig, ax = plt.subplots() + # Draw the screw as a rectangle with thread lines + rect = patches.Rectangle((0, 0), screw_length, screw_diameter, color="gray", alpha=0.3) + ax.add_patch(rect) + num_threads = int(screw_length / pitch) + for i in range(num_threads): + x_start = i * pitch + ax.plot([x_start, x_start + pitch/2], [0, screw_diameter], color="black") + ax.set_xlim(0, screw_length + 0.1) + ax.set_ylim(0, screw_diameter + 0.1) + ax.set_aspect("equal") + ax.set_title("Screw Diagram (Side View)") + st.pyplot(fig) + +def main(): + st.title("Simple Machines Simulation") + st.write("Explore and visualize various simple machines along with some key physics concepts behind them.") + + machine = st.sidebar.selectbox( + "Select a Simple Machine", + ["Lever", "Inclined Plane", "Pulley", "Wheel and Axle", "Wedge", "Screw"], + key="machine_select" + ) + + if machine == "Lever": + lever_simulation() + elif machine == "Inclined Plane": + inclined_plane_simulation() + elif machine == "Pulley": + pulley_simulation() + elif machine == "Wheel and Axle": + wheel_axle_simulation() + elif machine == "Wedge": + wedge_simulation() + elif machine == "Screw": + screw_simulation() + +if __name__ == '__main__': + main() diff --git a/pages/sound-wave.py b/pages/sound-wave.py new file mode 100644 index 0000000000000000000000000000000000000000..e59e6bcc151008eb1420d97e589a0ded4e0d91c9 --- /dev/null +++ b/pages/sound-wave.py @@ -0,0 +1,41 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("Sound Wave Simulation") + +# Create sliders for adjusting the simulation parameters. +amplitude = st.slider("Amplitude", min_value=0.1, max_value=2.0, value=1.0, step=0.1) +frequency = st.slider("Frequency (Hz)", min_value=1, max_value=20, value=5) +phase_speed = st.slider("Speed", min_value=0.1, max_value=5.0, value=1.0, step=0.1) + +# Button to start the simulation +simulate = st.button("Simulate Sound Wave") + +# Placeholder for the animated plot +plot_placeholder = st.empty() + +if simulate: + # Create a spatial domain for the simulation. + x = np.linspace(0, 2 * np.pi, 400) + # Run the simulation for a fixed number of frames. + for i in range(200): + t = i * 0.05 # time increments + # Compute the wave function: y(x, t) = A * sin(2π f (x - vt)) + y = amplitude * np.sin(2 * np.pi * frequency * (x - phase_speed * t)) + + # Create the plot + fig, ax = plt.subplots() + ax.plot(x, y) + ax.set_ylim(-2, 2) + ax.set_title("Sound Wave") + ax.set_xlabel("Distance") + ax.set_ylabel("Amplitude") + + # Update the placeholder with the new plot + plot_placeholder.pyplot(fig) + plt.close(fig) + + # Delay to control the animation speed + time.sleep(0.05) diff --git a/pages/special-relativity.py b/pages/special-relativity.py new file mode 100644 index 0000000000000000000000000000000000000000..e9b81a36b2f06ff94b2d2ed0cabf661248129ab8 --- /dev/null +++ b/pages/special-relativity.py @@ -0,0 +1,52 @@ +import streamlit as st +import math +import matplotlib.pyplot as plt + +st.title("Special Relativity Visualization: Length Contraction") + +st.markdown( + """ + In special relativity, an object moving with velocity \( v \) will have its length contracted as observed in the lab frame. + The contracted length \( L \) is given by: + + \[ + L = L_0 \sqrt{1 - \beta^2} \quad \text{with} \quad \beta = \frac{v}{c} + \] + + (Here, we set \( c = 1 \) for simplicity.) + """ +) + +# Slider to select the velocity (as a fraction of c) +v = st.slider("Select velocity (v) as a fraction of the speed of light", 0.0, 0.99, 0.0, 0.01) + +# Define the rest length (in arbitrary units) +L0 = 10 # rest length +beta = v # since c = 1 +gamma = 1 / math.sqrt(1 - beta**2) # Lorentz factor +L_contracted = L0 * math.sqrt(1 - beta**2) + +st.write(f"At \( v = {v:.2f}c \):") +st.write(f"- Lorentz factor \( \\gamma \\) = {gamma:.2f}") +st.write(f"- Rest Length: {L0} units") +st.write(f"- Contracted Length: {L_contracted:.2f} units") + +# Create a plot to visualize the contraction +fig, ax = plt.subplots(figsize=(8, 2)) + +# Draw the rest length bar (in the rest frame) in blue +ax.broken_barh([(0, L0)], (10, 9), facecolors='blue', label="Rest Frame (Proper Length)") + +# Draw the moving rod (length contracted) in red +ax.broken_barh([(0, L_contracted)], (0, 9), facecolors='red', label="Moving Frame (Contracted)") + +# Formatting the plot +ax.set_ylim(0, 20) +ax.set_xlim(0, L0 + 1) +ax.set_xlabel("Length (arbitrary units)") +ax.set_yticks([5, 15]) +ax.set_yticklabels(["Moving Frame", "Rest Frame"]) +ax.set_title("Length Contraction Visualization") +ax.legend(loc='upper right') + +st.pyplot(fig) diff --git a/pages/statics.py b/pages/statics.py new file mode 100644 index 0000000000000000000000000000000000000000..53cbb2cd27036423f52a947da32bc368213ab722 --- /dev/null +++ b/pages/statics.py @@ -0,0 +1,158 @@ +import streamlit as st +import pandas as pd +import numpy as np +import plotly.graph_objects as go + +# Set up the Streamlit app +st.title("Statics Beam Simulation") +st.write("Visualize a simply supported beam with point loads. Enter the beam length and add loads below.") + +# User input for beam length +L = st.number_input("Beam Length (m)", min_value=0.1, value=10.0, step=0.1, help="Length of the beam in meters") + +# Initialize an example data frame for loads +example_loads = pd.DataFrame({ + 'Position (m)': [2.0, 5.0], + 'Magnitude (N)': [100.0, 200.0] +}) + +# User input for loads using data editor +st.write("Add or edit point loads (downward forces are positive):") +loads = st.data_editor( + example_loads, + column_config={ + 'Position (m)': st.column_config.NumberColumn( + min_value=0.0, max_value=L, step=0.1, help="Position along the beam (0 to L)" + ), + 'Magnitude (N)': st.column_config.NumberColumn( + min_value=0.0, step=1.0, help="Force magnitude in Newtons (downward)" + ) + }, + num_rows="dynamic", + use_container_width=True +) + +# Process the loads: group by position to sum magnitudes at the same position +loads_grouped = loads.groupby('Position (m)')['Magnitude (N)'].sum().reset_index() + +# Calculate reaction forces +sum_loads = loads_grouped['Magnitude (N)'].sum() +sum_moment = (loads_grouped['Magnitude (N)'] * loads_grouped['Position (m)']).sum() +R_B = sum_moment / L # Reaction at B (right support) +R_A = sum_loads - R_B # Reaction at A (left support) + +# Display reaction forces +st.subheader("Reaction Forces") +st.write(f"Reaction at A (x=0): **{R_A:.2f} N** (upward)") +st.write(f"Reaction at B (x={L}): **{R_B:.2f} N** (upward)") + +# --- Beam Diagram --- +st.subheader("Beam Diagram") +fig_beam = go.Figure() + +# Draw the beam as a horizontal line +fig_beam.add_shape(type="line", x0=0, y0=0, x1=L, y1=0, line=dict(color="black", width=3)) + +# Determine scale for arrows based on maximum force +max_force = max([abs(R_A), abs(R_B)] + [abs(load) for load in loads_grouped['Magnitude (N)']] if not loads_grouped.empty else [1]) +scale = max_force / 5 # Scale so largest arrow is 5 units + +# Add reaction arrows (upward) +fig_beam.add_annotation( + x=0, y=0, ax=0, ay=R_A / scale, + xref="x", yref="y", axref="x", ayref="y", + text=f"{R_A:.2f} N", showarrow=True, arrowhead=2, arrowsize=1, arrowwidth=2, arrowcolor="blue", + standoff=5 +) +fig_beam.add_annotation( + x=L, y=0, ax=L, ay=R_B / scale, + xref="x", yref="y", axref="x", ayref="y", + text=f"{R_B:.2f} N", showarrow=True, arrowhead=2, arrowsize=1, arrowwidth=2, arrowcolor="blue", + standoff=5 +) + +# Add load arrows (downward) +for index, load in loads_grouped.iterrows(): + x_load = load['Position (m)'] + magnitude = load['Magnitude (N)'] + fig_beam.add_annotation( + x=x_load, y=0, ax=x_load, ay=-magnitude / scale, + xref="x", yref="y", axref="x", ayref="y", + text=f"{magnitude:.2f} N", showarrow=True, arrowhead=2, arrowsize=1, arrowwidth=2, arrowcolor="red", + standoff=5 + ) + +# Update layout for beam diagram +fig_beam.update_layout( + xaxis_title="Position (m)", + yaxis_title="Force (arbitrary units)", + showlegend=False, + height=400, + yaxis_range=[-max_force/scale * 1.5, max_force/scale * 1.5], + xaxis_range=[-0.1*L, L*1.1] +) +st.plotly_chart(fig_beam, use_container_width=True) + +# --- Shear Force and Bending Moment Calculations --- +load_positions = sorted(loads_grouped['Position (m)'].unique()) +critical_points = [0] + load_positions + [L] + +# Compute shear force (V) at each critical point +V_list = [] +cumulative_load = 0 +for k in range(len(critical_points)): + if k > 0: + x_prev = critical_points[k-1] + load_at_x_prev = loads_grouped[loads_grouped['Position (m)'] == x_prev]['Magnitude (N)'].sum() + cumulative_load += load_at_x_prev + V_k = R_A - cumulative_load + V_list.append(V_k) + +# Compute bending moment (M) at each critical point +M_list = [0] # M(0) = 0 +for k in range(1, len(critical_points)): + x_prev = critical_points[k-1] + x_curr = critical_points[k] + V_k = V_list[k-1] # Shear force in the segment + M_curr = M_list[-1] + V_k * (x_curr - x_prev) + M_list.append(M_curr) + +# --- Shear Force Diagram --- +st.subheader("Shear Force Diagram") +fig_V = go.Figure() +fig_V.add_trace(go.Scatter( + x=critical_points, + y=V_list, + mode='lines', + line_shape='hv', # Horizontal-vertical steps for shear force + name='Shear Force', + line=dict(color='green') +)) +fig_V.update_layout( + xaxis_title="Position (m)", + yaxis_title="Shear Force (N)", + height=400, + xaxis_range=[-0.1*L, L*1.1] +) +st.plotly_chart(fig_V, use_container_width=True) + +# --- Bending Moment Diagram --- +st.subheader("Bending Moment Diagram") +fig_M = go.Figure() +fig_M.add_trace(go.Scatter( + x=critical_points, + y=M_list, + mode='lines', + name='Bending Moment', + line=dict(color='purple') +)) +fig_M.update_layout( + xaxis_title="Position (m)", + yaxis_title="Bending Moment (N·m)", + height=400, + xaxis_range=[-0.1*L, L*1.1] +) +st.plotly_chart(fig_M, use_container_width=True) + +# Footer +st.write("Note: This simulation assumes a simply supported beam with supports at x=0 and x=L. Loads are point forces applied vertically downward.") \ No newline at end of file diff --git a/pages/statistical-mechanics.py b/pages/statistical-mechanics.py new file mode 100644 index 0000000000000000000000000000000000000000..e0f34c25a7317ed740f21dfa0ce3acd2c9f5f94c --- /dev/null +++ b/pages/statistical-mechanics.py @@ -0,0 +1,106 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +# Streamlit app title +st.title("1D Ideal Gas Simulation for Statistical Mechanics") + +# Sidebar for parameters +st.sidebar.header("Simulation Parameters") +N = st.sidebar.slider("Number of particles", 10, 500, 100, step=10) +L = st.sidebar.slider("Box length", 1.0, 10.0, 5.0, step=0.5) +v0 = st.sidebar.slider("Initial velocity scale", 0.1, 5.0, 1.0, step=0.1) + +# Constants +m = 1.0 # Particle mass (arbitrary units) +k = 1.0 # Boltzmann constant (simplified units) +dt = 0.01 # Time step + +# Initialize simulation state +if 'positions' not in st.session_state: + st.session_state.positions = np.random.uniform(0, L, N) + st.session_state.velocities = np.random.normal(0, v0, N) + st.session_state.total_momentum_transfer = 0.0 + st.session_state.total_time = 0.0 + +# Start button +if st.button("Start Simulation"): + positions = st.session_state.positions + velocities = st.session_state.velocities + total_momentum_transfer = st.session_state.total_momentum_transfer + total_time = st.session_state.total_time + + # Placeholder for dynamic updates + placeholder = st.empty() + + # Simulation loop + for step in range(500): # Run for 500 steps + # Update positions + positions += velocities * dt + + # Handle collisions with walls + for i in range(N): + if positions[i] < 0: + positions[i] = -positions[i] # Reflect off left wall + velocities[i] = -velocities[i] + total_momentum_transfer += 2 * m * abs(velocities[i]) + elif positions[i] > L: + positions[i] = 2 * L - positions[i] # Reflect off right wall + velocities[i] = -velocities[i] + total_momentum_transfer += 2 * m * abs(velocities[i]) + + total_time += dt + + # Calculate temperature from average kinetic energy + # In 1D: <(1/2) m v^2> = (1/2) k T + average_ke = (0.5 * m * np.sum(velocities**2)) / N + T = (2 * average_ke) / k + + # Calculate force as momentum transfer per unit time + F = total_momentum_transfer / total_time if total_time > 0 else 0 + F_theory = (N * k * T) / L # Theoretical force from 1D ideal gas law + + # Create plots + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) + + # Position histogram + ax1.hist(positions, bins=20, range=(0, L), color='blue', alpha=0.7) + ax1.set_title("Particle Positions") + ax1.set_xlabel("Position") + ax1.set_ylabel("Count") + + # Velocity histogram + ax2.hist(velocities, bins=20, color='red', alpha=0.7) + ax2.set_title("Particle Velocities") + ax2.set_xlabel("Velocity") + ax2.set_ylabel("Count") + + plt.tight_layout() + + # Update placeholder with plot and metrics + with placeholder.container(): + st.pyplot(fig) + st.write(f"**Temperature (T):** {T:.2f} units") + st.write(f"**Simulated Force (F):** {F:.2f} units") + st.write(f"**Theoretical Force (F_theory):** {F_theory:.2f} units") + + # Small delay for visualization + time.sleep(0.05) + + # Update session state + st.session_state.positions = positions + st.session_state.velocities = velocities + st.session_state.total_momentum_transfer = total_momentum_transfer + st.session_state.total_time = total_time + +# Instructions +st.write(""" +### How to Use +1. Adjust the parameters in the sidebar: + - **Number of particles (N)**: Total particles in the simulation. + - **Box length (L)**: Length of the 1D box. + - **Initial velocity scale (v0)**: Controls the spread of initial velocities. +2. Click "Start Simulation" to run the simulation for 500 steps. +3. Observe the histograms and calculated values updating in real-time. +""") \ No newline at end of file diff --git a/pages/temperature.py b/pages/temperature.py new file mode 100644 index 0000000000000000000000000000000000000000..9425b7a4558b09602596f4a83f3bb5b01b3e3a2d --- /dev/null +++ b/pages/temperature.py @@ -0,0 +1,67 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("Temperature Distribution Simulation") + +st.markdown(""" +This simulation visualizes the evolution of temperature along a 1D rod using the heat equation: + +\[ +\frac{\partial u}{\partial t} = \alpha \frac{\partial^2 u}{\partial x^2} +\] + +We discretize space and time and update the temperature profile using the explicit finite difference method. +""") + +# Simulation parameters from user inputs +length = st.sidebar.slider("Rod Length", min_value=10, max_value=100, value=50) +N = st.sidebar.slider("Number of Spatial Points", min_value=50, max_value=500, value=100) +alpha = st.sidebar.slider("Thermal Diffusivity (α)", min_value=0.01, max_value=1.0, value=0.1, step=0.01) +time_steps = st.sidebar.slider("Number of Time Steps", min_value=100, max_value=1000, value=500) +dt = st.sidebar.slider("Time Step Size (dt)", min_value=0.001, max_value=0.1, value=0.01, step=0.001) + +dx = length / (N - 1) + +# Stability check for the explicit finite difference method (CFL condition) +if dt > dx**2 / (2 * alpha): + st.warning("The chosen time step may be too large for stability. Consider reducing dt.") + +# Define the spatial domain +x = np.linspace(0, length, N) + +# Initial condition: base temperature with a Gaussian peak in the center +base_temp = 20 # base temperature in °C +u = np.ones(N) * base_temp +u += 80 * np.exp(-((x - length/2)**2) / (2 * (length/10)**2)) + +# Create a placeholder for the plot +plot_placeholder = st.empty() + +st.markdown("### Simulation Running...") + +# Simulation loop +for t in range(time_steps): + # Create a new temperature array + u_new = u.copy() + + # Update interior points using the finite difference method + for i in range(1, N - 1): + u_new[i] = u[i] + dt * alpha * (u[i + 1] - 2 * u[i] + u[i - 1]) / dx**2 + u = u_new + + # Plot the temperature distribution + fig, ax = plt.subplots() + ax.plot(x, u, color='r', lw=2, label="Temperature") + ax.set_xlabel("Position along the rod") + ax.set_ylabel("Temperature (°C)") + ax.set_title(f"Time step {t+1}/{time_steps}") + ax.legend() + ax.set_ylim(0, base_temp + 90) + + # Update the plot in the Streamlit app + plot_placeholder.pyplot(fig) + + # A short delay for animation effect + time.sleep(0.05) diff --git a/pages/third-thermodynamics-law.py b/pages/third-thermodynamics-law.py new file mode 100644 index 0000000000000000000000000000000000000000..b7446c8a0be435491d2e2e5ae8f9a477513ab8d9 --- /dev/null +++ b/pages/third-thermodynamics-law.py @@ -0,0 +1,48 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +st.title("Visualization of the Third Law of Thermodynamics") +st.write(""" +This simulation demonstrates the third law of thermodynamics using a simple model of a crystalline lattice. +As temperature approaches absolute zero, the atoms vibrate less, leading to a perfectly ordered state (minimal entropy). +""") + +# Temperature slider (in Kelvin) +T = st.slider("Select Temperature (K)", 0.0, 300.0, 150.0, step=1.0) + +# Define lattice dimensions (a simple 10x10 grid) +num_points = 10 +x_coords, y_coords = np.meshgrid(np.linspace(0, 10, num_points), np.linspace(0, 10, num_points)) +x_coords = x_coords.flatten() +y_coords = y_coords.flatten() + +# Determine noise amplitude based on temperature. +# At T=300K the maximum displacement is set to 0.5 units, and at T=0 there is no displacement. +noise_amp = (T / 300) * 0.5 + +# Add random displacement to simulate thermal vibrations +x_noise = np.random.normal(0, noise_amp, size=x_coords.shape) +y_noise = np.random.normal(0, noise_amp, size=y_coords.shape) + +# Compute the current positions of the atoms +x_positions = x_coords + x_noise +y_positions = y_coords + y_noise + +# Plot the lattice with the displaced atomic positions +fig, ax = plt.subplots(figsize=(6, 6)) +ax.scatter(x_positions, y_positions, color="blue") +ax.set_title(f"Atomic positions at T = {T:.1f} K") +ax.set_xlabel("X position") +ax.set_ylabel("Y position") +ax.set_xlim(-1, 11) +ax.set_ylim(-1, 11) +ax.set_aspect("equal") + +st.pyplot(fig) + +st.write(""" +As you lower the temperature using the slider, notice that the atoms’ positions become less scattered. +At T = 0 K, the atoms remain fixed in place (no thermal agitation), illustrating the third law: a perfect crystal +has zero entropy at absolute zero. +""") diff --git a/pages/time.py b/pages/time.py new file mode 100644 index 0000000000000000000000000000000000000000..bc2769daf43702b231b4c1bb48ec1e0509e590f3 --- /dev/null +++ b/pages/time.py @@ -0,0 +1,164 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +from scipy.special import gamma +import time as pytime + +def time_dilation_visualization(): + st.header("Time Dilation Visualization") + st.markdown(""" + Explore how time changes at different velocities according to Einstein's Special Relativity. + + Time dilation is the phenomenon where time passes differently for objects moving at different speeds. + """) + + # Velocity slider + velocity = st.slider( + "Velocity as a fraction of speed of light (c)", + min_value=0.0, + max_value=0.99, + value=0.5, + step=0.01 + ) + + # Speed of light (m/s) + c = 299792458 + + # Calculate time dilation factor + time_dilation_factor = 1 / np.sqrt(1 - (velocity**2)) + + # Visualize time dilation + col1, col2 = st.columns(2) + + with col1: + st.write("### Stationary Observer") + st.write(f"Time passes normally") + st.write(f"Time scale: 1.0") + + with col2: + st.write("### Moving Object") + st.write(f"Velocity: {velocity * 100:.2f}% of c") + st.write(f"Time Dilation Factor: {time_dilation_factor:.4f}") + st.write(f"Time passes slower by a factor of {time_dilation_factor:.4f}") + + # Visualization graph + plt.figure(figsize=(10, 6)) + velocities = np.linspace(0, 0.99, 100) + dilation_factors = 1 / np.sqrt(1 - (velocities**2)) + + plt.plot(velocities * 100, dilation_factors, 'b-') + plt.title('Time Dilation vs Velocity') + plt.xlabel('Velocity (% of speed of light)') + plt.ylabel('Time Dilation Factor') + plt.grid(True) + st.pyplot(plt) + +def entropy_and_time_visualization(): + st.header("Entropy and Time's Arrow") + st.markdown(""" + Explore how entropy relates to the perception of time's direction. + + The Second Law of Thermodynamics suggests that entropy (disorder) always increases, + giving time a distinct direction or "arrow". + """) + + # Entropy simulation + st.subheader("Entropy Simulation") + + # Random walk to simulate entropy increase + steps = st.slider("Number of Steps", min_value=10, max_value=500, value=100) + + # Run simulation + def entropy_walk(steps): + position = np.zeros(steps) + for i in range(1, steps): + # Random step left or right + position[i] = position[i-1] + np.random.choice([-1, 1]) + return position + + # Multiple walks + plt.figure(figsize=(12, 6)) + for _ in range(5): + walk = entropy_walk(steps) + plt.plot(range(steps), walk) + + plt.title('Random Walks Demonstrating Entropy Increase') + plt.xlabel('Time Steps') + plt.ylabel('Position') + st.pyplot(plt) + + # Entropy explanation + st.write(""" + ### Understanding the Visualization + - Each line represents a random walk + - The spread of lines shows increasing disorder over time + - This mirrors how entropy increases in physical systems + """) + +def quantum_time_visualization(): + st.header("Quantum Time Uncertainty") + st.markdown(""" + Explore time uncertainty in quantum mechanics. + + In quantum mechanics, time is not as precisely defined as in classical physics. + """) + + # Heisenberg Uncertainty Principle visualization + st.subheader("Energy-Time Uncertainty") + + # Energy range slider + energy_uncertainty = st.slider( + "Energy Uncertainty (ℏ units)", + min_value=0.1, + max_value=10.0, + value=1.0, + step=0.1 + ) + + # Calculate time uncertainty + # Using ℏ (reduced Planck constant) = 1 for simplification + time_uncertainty = 1 / (2 * energy_uncertainty) + + st.write(f"Time Uncertainty: {time_uncertainty:.4f}") + + # Visualization of uncertainty + plt.figure(figsize=(10, 6)) + energies = np.linspace(0.1, 10, 100) + times = 1 / (2 * energies) + + plt.plot(energies, times, 'r-') + plt.title('Energy-Time Uncertainty Relationship') + plt.xlabel('Energy Uncertainty (ℏ units)') + plt.ylabel('Time Uncertainty') + plt.grid(True) + st.pyplot(plt) + + st.write(""" + ### Quantum Time Insights + - Smaller energy uncertainties lead to larger time uncertainties + - This demonstrates the fundamental limit of precision in quantum systems + """) + +def main(): + st.title("Physics of Time") + + # Sidebar navigation + page = st.sidebar.selectbox( + "Choose a Time Visualization", + [ + "Time Dilation", + "Entropy and Time's Arrow", + "Quantum Time Uncertainty" + ] + ) + + # Page routing + if page == "Time Dilation": + time_dilation_visualization() + elif page == "Entropy and Time's Arrow": + entropy_and_time_visualization() + elif page == "Quantum Time Uncertainty": + quantum_time_visualization() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pages/time1.py b/pages/time1.py new file mode 100644 index 0000000000000000000000000000000000000000..61987827476e298de493abe05872f557b7c72db2 --- /dev/null +++ b/pages/time1.py @@ -0,0 +1,137 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import plotly.graph_objs as go +import plotly.express as px +import pandas as pd + +def time_dilation_visualization(): + """ + Visualize time dilation effect using relativistic time calculation + """ + st.header("Time Dilation Visualization") + st.markdown(""" + Time dilation demonstrates how time passes differently + depending on relative motion and gravitational fields. + """) + + # Slider for velocity as a fraction of speed of light + velocity = st.slider( + "Velocity (as a fraction of speed of light)", + min_value=0.0, + max_value=0.99, + value=0.0, + step=0.01 + ) + + # Speed of light + c = 299792458 # meters per second + + # Calculate time dilation factor + if velocity > 0: + time_dilation_factor = 1 / np.sqrt(1 - (velocity ** 2)) + + # Visualize time passage + st.write(f"Velocity: {velocity * c:,.0f} m/s") + st.write(f"Time Dilation Factor: {time_dilation_factor:.4f}") + + # Create a comparative visualization + fig, ax = plt.subplots(figsize=(10, 6)) + + # Normal time (stationary reference frame) + normal_time = np.linspace(0, 10, 100) + dilated_time = normal_time * time_dilation_factor + + ax.plot(normal_time, normal_time, label='Stationary Time', color='blue') + ax.plot(normal_time, dilated_time, label='Dilated Time', color='red', linestyle='--') + + ax.set_xlabel('Proper Time') + ax.set_ylabel('Observed Time') + ax.set_title('Time Dilation Comparison') + ax.legend() + ax.grid(True) + + st.pyplot(fig) + + st.markdown(""" + **Interpretation:** + - Blue line represents time in a stationary reference frame + - Red dashed line shows time for an object moving at high velocity + - As velocity increases, time appears to slow down for the moving object + """) + else: + st.write("Adjust velocity to see time dilation effect") + +def entropy_and_time(): + """ + Visualize entropy as a representation of time's arrow + """ + st.header("Entropy: The Arrow of Time") + + # Simulation of entropy increase + st.markdown(""" + Entropy demonstrates why time seems to flow in one direction. + The second law of thermodynamics suggests that entropy (disorder) + always increases in a closed system. + """) + + # Create an entropy visualization + states = st.slider( + "Number of System States", + min_value=10, + max_value=500, + value=100 + ) + + # Generate entropy data + np.random.seed(42) + entropy_data = np.cumsum(np.random.random(states)) + + # Plotly visualization + fig = go.Figure() + fig.add_trace(go.Scatter( + x=list(range(states)), + y=entropy_data, + mode='lines', + name='Entropy Progression' + )) + + fig.update_layout( + title='Entropy Progression Over Time', + xaxis_title='Time Steps', + yaxis_title='Cumulative Entropy' + ) + + st.plotly_chart(fig) + + st.markdown(""" + **Key Observations:** + - Entropy consistently increases over time + - This irreversible increase creates a 'time arrow' + - Demonstrates why we can remember the past but not the future + """) + +def main(): + st.title("Time in Physics: A Multidimensional Exploration") + + # Sidebar for navigation + page = st.sidebar.selectbox( + "Choose a Visualization", + [ + "Time Dilation", + "Entropy and Time's Arrow" + ] + ) + + # Page routing + if page == "Time Dilation": + time_dilation_visualization() + elif page == "Entropy and Time's Arrow": + entropy_and_time() + +if __name__ == "__main__": + main() + +# Note: To run this, you'll need to install: +# streamlit, numpy, matplotlib, plotly +# Use command: streamlit run time_visualization.py \ No newline at end of file diff --git a/pages/torque.py b/pages/torque.py new file mode 100644 index 0000000000000000000000000000000000000000..edd75e38d64c7457304bd85bcff8b7cbaaf66b9b --- /dev/null +++ b/pages/torque.py @@ -0,0 +1,73 @@ +import streamlit as st +import matplotlib.pyplot as plt +import numpy as np + +# Title and description +st.title("Torque Visualization") +st.write(""" +This simulation demonstrates torque, the rotational equivalent of force. Torque (τ) is calculated as τ = r × F, +where r is the distance from the pivot to the force application point, and F is the force magnitude. +The direction of the force determines whether the torque causes clockwise or counterclockwise rotation. +""") + +# Define constants +L = 1.0 # Length of the bar in meters + +# User inputs +r = st.slider("Distance from pivot (m)", 0.0, L, 0.5, help="Distance from the pivot where the force is applied.") +F = st.slider("Force magnitude (N)", 0.0, 100.0, 50.0, help="Magnitude of the applied force in Newtons.") +direction = st.selectbox("Force direction", ["downward", "upward"], help="Direction of the force relative to the bar.") +show_rotation = st.checkbox("Show rotation effect", help="Toggle to see the bar rotated based on the torque.") + +# Calculate torque (assuming force is perpendicular, so sin(θ) = 1) +if direction == "downward": + tau = -r * F # Negative torque indicates clockwise rotation + force_dir = -1 +else: + tau = r * F # Positive torque indicates counterclockwise rotation + force_dir = 1 + +# Create the plot +fig, ax = plt.subplots(figsize=(8, 4)) + +# Plot the bar based on rotation option +if show_rotation: + k = 0.005 # Scaling factor for rotation angle (radians per Nm) + theta = k * tau # Rotation angle in radians + x_end = L * np.cos(theta) + y_end = L * np.sin(theta) + ax.plot([0, x_end], [0, y_end], 'b-', linewidth=5, label="Bar (rotated)") +else: + ax.plot([0, L], [0, 0], 'b-', linewidth=5, label="Bar") + +# Plot the pivot point +ax.plot(0, 0, 'ro', markersize=10, label="Pivot") + +# Plot the force arrow (only if not showing rotation, to keep it clear) +if not show_rotation: + arrow_length = 0.1 + ax.arrow(r, 0, 0, force_dir * arrow_length, head_width=0.05, head_length=0.05, fc='g', ec='g', label="Force") + +# Add torque annotation +torque_text = f"Torque: {abs(tau):.2f} Nm ({'clockwise' if tau < 0 else 'counterclockwise'})" +ax.text(0.1, 0.15 if not show_rotation else -0.15, torque_text, fontsize=10) + +# Customize the plot +ax.set_xlim(-0.1, L + 0.1) +ax.set_ylim(-0.2, 0.2) +ax.set_aspect('equal') +ax.set_xlabel("x (m)") +ax.set_ylabel("y (m)") +ax.legend() +ax.grid(True) + +# Display the plot in Streamlit +st.pyplot(fig) + +# Explanatory text +st.write(""" +- **Blue line**: The bar, pivoted at the red dot. +- **Green arrow**: The applied force (visible when 'Show rotation effect' is unchecked). +- **Torque**: Calculated as τ = r × F. Positive τ means counterclockwise rotation; negative τ means clockwise. +- **Rotation effect**: When checked, the bar rotates by an angle proportional to the torque (θ = k × τ, where k = 0.005 rad/Nm). +""") \ No newline at end of file diff --git a/pages/uncertainity-principle.py b/pages/uncertainity-principle.py new file mode 100644 index 0000000000000000000000000000000000000000..c8810846c2a8721fb1a94274141227d5b6a602d5 --- /dev/null +++ b/pages/uncertainity-principle.py @@ -0,0 +1,50 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +st.title("Uncertainty Principle Visualization") +st.write("This simulation demonstrates the trade-off between the localization of a quantum wave packet in position space and its spread in momentum space. Adjust the slider to change the width (σ) of the Gaussian wave packet.") + +# Slider for wave packet width (sigma) +sigma = st.slider("Select the width (σ) of the Gaussian wave packet in position space", min_value=0.1, max_value=5.0, value=1.0, step=0.1) + +# Set up the spatial grid +N = 1024 # number of points +x = np.linspace(-10, 10, N) +dx = x[1] - x[0] + +# Define the Gaussian wave function in position space +psi_x = (1/(2*np.pi*sigma**2)**0.25) * np.exp(-x**2/(4*sigma**2)) +prob_x = np.abs(psi_x)**2 # probability density in position space + +# Fourier transform to get the momentum space wave function (assuming ħ = 1) +# Use fftshift and ifftshift to center the zero frequency component +psi_p = np.fft.fftshift(np.fft.fft(np.fft.ifftshift(psi_x))) * dx / np.sqrt(2*np.pi) + +# Define momentum grid corresponding to the Fourier frequencies +p = np.fft.fftshift(np.fft.fftfreq(N, d=dx)) * 2*np.pi +prob_p = np.abs(psi_p)**2 # probability density in momentum space + +# Plot the position probability density +fig1, ax1 = plt.subplots() +ax1.plot(x, prob_x, color='blue') +ax1.set_title("Position Probability Density |ψ(x)|²") +ax1.set_xlabel("Position (x)") +ax1.set_ylabel("Probability Density") +st.pyplot(fig1) + +# Plot the momentum probability density +fig2, ax2 = plt.subplots() +ax2.plot(p, prob_p, color='red') +ax2.set_title("Momentum Probability Density |ψ(p)|²") +ax2.set_xlabel("Momentum (p)") +ax2.set_ylabel("Probability Density") +st.pyplot(fig2) + +# Calculate standard deviations in position and momentum +sigma_x = np.sqrt(np.sum(x**2 * prob_x) * dx) +dp = p[1] - p[0] +sigma_p = np.sqrt(np.sum(p**2 * prob_p) * dp) +uncertainty_product = sigma_x * sigma_p + +st.write(f"Calculated uncertainty product σₓ * σₚ = {uncertainty_product:.3f} (expected ~0.5 for a Gaussian wave packet)") diff --git a/pages/viscosity.py b/pages/viscosity.py new file mode 100644 index 0000000000000000000000000000000000000000..6ae69d1340ccd58b51b594341fe7809ab8eb7dc4 --- /dev/null +++ b/pages/viscosity.py @@ -0,0 +1,78 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("Viscosity Simulation: Falling Sphere in a Viscous Fluid") + +st.sidebar.header("Simulation Parameters") +eta = st.sidebar.slider("Viscosity (η) [Pa·s]", min_value=0.1, max_value=10.0, value=1.0, step=0.1) +radius = st.sidebar.slider("Ball Radius (r) [m]", min_value=0.01, max_value=0.1, value=0.05, step=0.005) +mass = st.sidebar.slider("Ball Mass (m) [kg]", min_value=0.1, max_value=10.0, value=1.0, step=0.1) +total_time = st.sidebar.slider("Total Simulation Time [s]", min_value=5.0, max_value=30.0, value=10.0, step=1.0) + +gravity = 9.81 # m/s^2 +dt = 0.05 # time step (s) + +def simulate(m, r, eta, g, dt, T): + """ + Simulate the fall of a sphere in a viscous fluid using Euler's method. + The drag force is given by F_drag = 6*pi*eta*r*v. + """ + num_steps = int(T / dt) + t_vals = np.linspace(0, T, num_steps) + v = 0.0 + x = 0.0 + positions = [] + velocities = [] + drag_coeff = 6 * np.pi * eta * r # constant from Stokes' drag + + for _ in t_vals: + # Compute acceleration from gravity and drag force: + a = g - (drag_coeff / m) * v + v = v + a * dt + x = x + v * dt + positions.append(x) + velocities.append(v) + return t_vals, positions, velocities + +simulate_button = st.button("Run Simulation") + +if simulate_button: + t_vals, positions, velocities = simulate(mass, radius, eta, gravity, dt, total_time) + + # Plot position and velocity vs. time + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 8)) + + ax1.plot(t_vals, positions, label="Position (m)") + ax1.set_xlabel("Time (s)") + ax1.set_ylabel("Position (m)") + ax1.legend() + ax1.grid(True) + + ax2.plot(t_vals, velocities, label="Velocity (m/s)", color="orange") + ax2.set_xlabel("Time (s)") + ax2.set_ylabel("Velocity (m/s)") + ax2.legend() + ax2.grid(True) + + st.pyplot(fig) + + # Animation of the falling ball + st.subheader("Ball Animation") + placeholder = st.empty() + # Determine the plot limits based on the simulation + max_height = max(positions) * 1.1 if positions else 10 + for i in range(len(t_vals)): + fig_anim, ax_anim = plt.subplots(figsize=(3, 6)) + ax_anim.set_xlim(-1, 1) + ax_anim.set_ylim(0, max_height) + ax_anim.set_xticks([]) + ax_anim.set_ylabel("Height (m)") + # Draw the ball as a circle + ball = plt.Circle((0, positions[i]), radius*20, color="blue", alpha=0.7) + ax_anim.add_artist(ball) + ax_anim.set_title(f"Time: {t_vals[i]:.2f} s") + placeholder.pyplot(fig_anim) + plt.close(fig_anim) + time.sleep(0.05) diff --git a/pages/wave-concept.py b/pages/wave-concept.py new file mode 100644 index 0000000000000000000000000000000000000000..4a59088b2055ad74d6953f765e20790f3c1209fb --- /dev/null +++ b/pages/wave-concept.py @@ -0,0 +1,46 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("Wave Simulation") + +# Sidebar sliders for simulation parameters +amplitude = st.sidebar.slider("Amplitude (A)", 0.1, 5.0, 1.0, step=0.1) +frequency = st.sidebar.slider("Frequency (ω)", 0.1, 10.0, 1.0, step=0.1) +phase = st.sidebar.slider("Phase (φ)", 0.0, 2 * np.pi, 0.0, step=0.1) +speed = st.sidebar.slider("Speed (v)", 0.1, 5.0, 1.0, step=0.1) + +st.markdown(r"**Wave Equation:** $y(x,t) = A \sin\big(\omega (x - vt) + \phi\big)$") + +# Placeholder for the plot that will be updated in the loop +plot_placeholder = st.empty() + +# Define x range for the wave +x = np.linspace(0, 4 * np.pi, 400) +t = 0 + +# Infinite loop to update the plot continuously. +# Adjust time.sleep() to control the speed of animation. +while True: + # Compute the wave at time t + y = amplitude * np.sin(frequency * (x - speed * t) + phase) + + # Create a new figure for each update + fig, ax = plt.subplots() + ax.plot(x, y, label=f"t = {t:.2f}") + ax.set_ylim(-amplitude - 1, amplitude + 1) + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_title("Traveling Wave Simulation") + ax.legend() + + # Update the Streamlit plot + plot_placeholder.pyplot(fig) + + # Close the figure to avoid resource warnings + plt.close(fig) + + # Increment time and sleep briefly to animate + t += 0.1 + time.sleep(0.1) diff --git a/pages/wave-particle-duality.py b/pages/wave-particle-duality.py new file mode 100644 index 0000000000000000000000000000000000000000..a451f8b49b8b48e6317b540ecff2e8a3bcf9c598 --- /dev/null +++ b/pages/wave-particle-duality.py @@ -0,0 +1,53 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +st.title("Wave-Particle Duality: Double-Slit Experiment Simulation") + +# Sidebar controls for parameters +st.sidebar.header("Experiment Parameters") +wavelength = st.sidebar.slider("Wavelength (λ) [m]", 0.1, 2.0, 0.5, step=0.1) +slit_separation = st.sidebar.slider("Slit Separation (d) [m]", 0.5, 5.0, 1.0, step=0.1) +screen_distance = st.sidebar.slider("Distance to Screen (L) [m]", 10, 100, 50, step=1) +num_particles = st.sidebar.slider("Number of Particles", 100, 10000, 1000, step=100) +simulate_particles = st.sidebar.button("Simulate Particle Impacts") + +# Define spatial domain on the screen (in meters) +x = np.linspace(-0.05, 0.05, 500) + +# Calculate the phase difference for the two slits +# Under the small-angle approximation: sin(theta) ~ x / L +phase = (np.pi * slit_separation * x) / (wavelength * screen_distance) +# Intensity from two-slit interference (ignoring single-slit envelope) +intensity = np.cos(phase) ** 2 + +st.subheader("Wave Interference Pattern") +fig_wave, ax_wave = plt.subplots(figsize=(8, 4)) +ax_wave.plot(x, intensity, color='blue', lw=2) +ax_wave.set_xlabel("Position on Screen (m)") +ax_wave.set_ylabel("Intensity (a.u.)") +ax_wave.set_title("Theoretical Interference Pattern") +st.pyplot(fig_wave) + +st.subheader("Particle Detection Simulation") +if simulate_particles: + # Normalize the intensity to create a probability distribution + prob_distribution = intensity / np.sum(intensity) + # Sample particle landing positions based on the intensity distribution + particle_positions = np.random.choice(x, size=num_particles, p=prob_distribution) + + # Plot the histogram of particle impacts + fig_particles, ax_particles = plt.subplots(figsize=(8, 4)) + bins = np.linspace(x.min(), x.max(), 50) + ax_particles.hist(particle_positions, bins=bins, density=True, alpha=0.6, color='red', label="Particle Hits") + + # Overlay the normalized theoretical intensity pattern + norm_intensity = intensity / intensity.max() + ax_particles.plot(x, norm_intensity, color='blue', lw=2, label="Wave Intensity (normalized)") + ax_particles.set_xlabel("Position on Screen (m)") + ax_particles.set_ylabel("Probability Density (normalized)") + ax_particles.set_title("Accumulated Particle Impacts vs. Wave Intensity") + ax_particles.legend() + st.pyplot(fig_particles) +else: + st.write("Use the sidebar button to simulate particle impacts based on the interference pattern.") diff --git a/pages/wave-speed.py b/pages/wave-speed.py new file mode 100644 index 0000000000000000000000000000000000000000..6b687d0479de97769c5d9cac1791d1ebb77588e4 --- /dev/null +++ b/pages/wave-speed.py @@ -0,0 +1,44 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt +import time + +st.title("Wave Speed Visualization") + +# Sidebar: Set simulation parameters +st.sidebar.header("Simulation Parameters") +amplitude = st.sidebar.slider("Amplitude", 0.1, 5.0, 1.0, step=0.1) +frequency = st.sidebar.slider("Frequency (Hz)", 0.1, 5.0, 1.0, step=0.1) +wave_speed = st.sidebar.slider("Wave Speed", 0.1, 10.0, 2.0, step=0.1) + +# Define the spatial domain for the wave +x = np.linspace(0, 10, 500) + +# Create a placeholder for updating the plot +placeholder = st.empty() + +# Record the starting time +start_time = time.time() + +# Animation loop +while True: + # Calculate elapsed time + t = time.time() - start_time + + # Compute wave values: y = A * sin(2πf(x - vt)) + y = amplitude * np.sin(2 * np.pi * frequency * (x - wave_speed * t)) + + # Create the plot + fig, ax = plt.subplots() + ax.plot(x, y, label=f"t = {t:.2f} s") + ax.set_ylim([-amplitude * 1.2, amplitude * 1.2]) + ax.set_title("Wave Propagation") + ax.set_xlabel("Position") + ax.set_ylabel("Amplitude") + ax.legend() + + # Update the plot in the Streamlit app + placeholder.pyplot(fig) + + # Small delay to control animation speed + time.sleep(0.05) diff --git a/pages/work-energy-theorem.py b/pages/work-energy-theorem.py new file mode 100644 index 0000000000000000000000000000000000000000..c3bfbd20539061e5c21b52a011179bd79f558b4e --- /dev/null +++ b/pages/work-energy-theorem.py @@ -0,0 +1,88 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +# Title and description +st.title("Work-Energy Theorem Simulation") +st.markdown(""" +This simulation demonstrates the **Work-Energy Theorem**, which states that the net work done on an object equals its change in kinetic energy: +**\( W_{\text{net}} = \Delta K \)**. +Adjust the parameters below to explore how an applied force (with optional friction) affects a block's motion and energy. +""") + +# Input fields +st.subheader("Input Parameters") +m = st.number_input("Mass of the block (kg)", min_value=0.1, value=1.0, step=0.1) +F = st.number_input("Applied force (N)", min_value=0.0, value=10.0, step=1.0) +mu_k = st.number_input("Coefficient of kinetic friction (μ_k)", min_value=0.0, value=0.0, step=0.05) +T = st.number_input("Total time (s)", min_value=0.1, value=5.0, step=0.1) + +# Constants +g = 9.8 # Acceleration due to gravity (m/s²) + +# Calculate kinetic friction force +f_k = mu_k * m * g + +# Determine net force +if F > f_k: + F_net = F - f_k + st.write(f"Net force: {F_net:.2f} N") +else: + F_net = 0 + st.warning("The applied force is not sufficient to overcome friction, so the block does not move.") + +# Calculate acceleration +a = F_net / m if F_net > 0 else 0 + +# Time array for simulation +t = np.linspace(0, T, 100) + +# Calculate motion and energy +if F_net > 0: + x = 0.5 * a * t**2 # Position: x(t) = (1/2) a t² (starting from rest) + v = a * t # Velocity: v(t) = a t +else: + x = np.zeros_like(t) + v = np.zeros_like(t) + +K = 0.5 * m * v**2 # Kinetic energy: K(t) = (1/2) m v² +W_net = F_net * x # Net work done: W_net(t) = F_net * x(t) + +# Plotting +st.subheader("Visualizations") +# fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), height_ratios=[1, 1]) +fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), gridspec_kw={'height_ratios': [1, 1]}) + +# Plot 1: Position vs. Time +ax1.plot(t, x, label="Position", color="blue") +ax1.set_xlabel("Time (s)") +ax1.set_ylabel("Position (m)") +ax1.set_title("Position of the Block Over Time") +ax1.grid(True) +ax1.legend() + +# Plot 2: Net Work and Kinetic Energy vs. Time +ax2.plot(t, W_net, label="Net Work Done", color="green") +ax2.plot(t, K, label="Kinetic Energy", color="orange", linestyle="--") +ax2.set_xlabel("Time (s)") +ax2.set_ylabel("Energy (J)") +ax2.set_title("Net Work Done vs. Kinetic Energy") +ax2.grid(True) +ax2.legend() + +# Adjust layout and display plot +plt.tight_layout() +st.pyplot(fig) + +# Display final values +st.subheader(f"Results at t = {T} s") +if F_net > 0: + st.write(f"Final position: {x[-1]:.2f} m") + st.write(f"Final velocity: {v[-1]:.2f} m/s") + st.write(f"Net work done: {W_net[-1]:.2f} J") + st.write(f"Change in kinetic energy: {K[-1] - K[0]:.2f} J") + st.success("Notice that the net work done equals the change in kinetic energy, confirming the Work-Energy Theorem!") +else: + st.write("Since the block does not move:") + st.write("Net work done: 0.00 J") + st.write("Change in kinetic energy: 0.00 J") \ No newline at end of file diff --git a/pages/work.py b/pages/work.py new file mode 100644 index 0000000000000000000000000000000000000000..66e63e4ce7f4333eb70b28a7ff7c2389074cab3f --- /dev/null +++ b/pages/work.py @@ -0,0 +1,126 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +def calculate_work(force, displacement, angle): + """ + Calculate work done by a force + + Parameters: + force (float): Magnitude of force (Newtons) + displacement (float): Distance moved (meters) + angle (float): Angle between force and displacement (degrees) + + Returns: + float: Work done (Joules) + """ + # Convert angle to radians + angle_rad = np.deg2rad(angle) + + # Calculate work: W = F * d * cos(θ) + work = force * displacement * np.cos(angle_rad) + + return work + +def plot_work_visualization(force, displacement, angle): + """ + Create a visual representation of work + + Parameters: + force (float): Magnitude of force (Newtons) + displacement (float): Distance moved (meters) + angle (float): Angle between force and displacement (degrees) + + Returns: + matplotlib figure + """ + # Calculate work + work = calculate_work(force, displacement, angle) + + # Create figure + fig, ax = plt.subplots(figsize=(10, 6)) + + # Draw displacement vector + ax.arrow(0, 0, displacement, 0, head_width=0.2, head_length=0.3, fc='blue', ec='blue', label='Displacement') + + # Draw force vector + force_x = force * np.cos(np.deg2rad(angle)) + force_y = force * np.sin(np.deg2rad(angle)) + ax.arrow(0, 0, force_x, force_y, head_width=0.2, head_length=0.3, fc='red', ec='red', label='Force') + + # Set plot properties + ax.set_xlim(-1, displacement + 1) + ax.set_ylim(-1, max(force_y, displacement) + 1) + ax.set_xlabel('Distance (m)') + ax.set_ylabel('Force (N)') + ax.set_title('Work Visualization') + ax.grid(True, linestyle='--', alpha=0.7) + ax.legend() + + # Annotate work + ax.text(displacement/2, max(force_y, displacement)/2, + f'Work = {work:.2f} J\nW = F * d * cos(θ)', + bbox=dict(facecolor='white', alpha=0.5)) + + return fig + +def main(): + # Set up the Streamlit app + st.title('Work Visualization in Physics') + + # Sidebar for inputs + st.sidebar.header('Work Parameters') + force = st.sidebar.slider('Force (Newtons)', min_value=0.0, max_value=100.0, value=50.0, step=0.1) + displacement = st.sidebar.slider('Displacement (meters)', min_value=0.0, max_value=20.0, value=10.0, step=0.1) + angle = st.sidebar.slider('Angle between Force and Displacement (degrees)', + min_value=0, max_value=180, value=0, step=1) + + # Calculate work + work = calculate_work(force, displacement, angle) + + # Display results + st.header('Work Calculation') + st.write(f'Force: {force} N') + st.write(f'Displacement: {displacement} m') + st.write(f'Angle: {angle}°') + st.write(f'Work Done: {work:.2f} J') + + # Explain work formula + st.markdown(""" + ### Work Formula Explanation + Work is calculated using the formula: + **W = F * d * cos(θ)** + + Where: + - W is Work (in Joules, J) + - F is Force (in Newtons, N) + - d is Displacement (in meters, m) + - θ is the angle between the force and displacement vectors + """) + + # Work visualization + st.header('Work Visualization') + fig = plot_work_visualization(force, displacement, angle) + st.pyplot(fig) + + # Interactive explanation + st.markdown(""" + ### Key Insights + - When the force is parallel to displacement (0°), work is maximum + - When the force is perpendicular to displacement (90°), no work is done + - When the force is opposite to displacement (180°), work is negative + """) + +# Run the app +if __name__ == '__main__': + main() + +# Save instructions for running the app +''' +To run this Streamlit app: +1. Save this script as work_visualization.py +2. Install required libraries: + pip install streamlit numpy matplotlib +3. Run the app: + streamlit run work_visualization.py +''' \ No newline at end of file diff --git a/pages/zero-law-of-thermodynamics.py b/pages/zero-law-of-thermodynamics.py new file mode 100644 index 0000000000000000000000000000000000000000..34465435a168f25dca96bf7c024729f2696f26bc --- /dev/null +++ b/pages/zero-law-of-thermodynamics.py @@ -0,0 +1,56 @@ +import streamlit as st +import numpy as np +import matplotlib.pyplot as plt + +st.title("Zeroth Law of Thermodynamics Simulation") +st.write(""" +This simulation visualizes how three systems, each with an initial temperature, +exchange heat and eventually reach thermal equilibrium. +The zeroth law of thermodynamics states that if system A is in equilibrium with system B, +and system B is in equilibrium with system C, then A and C are in equilibrium. +""") + +# Adjustable parameters +k = st.slider("Heat Transfer Coefficient (k)", min_value=0.01, max_value=1.0, value=0.1, step=0.01) +T_A0 = st.number_input("Initial Temperature of System A (°C)", value=100.0) +T_B0 = st.number_input("Initial Temperature of System B (°C)", value=50.0) +T_C0 = st.number_input("Initial Temperature of System C (°C)", value=25.0) +total_time = st.number_input("Total Simulation Time (seconds)", value=20.0) +dt = 0.1 # time step + +# Time array +time_array = np.arange(0, total_time, dt) +num_steps = len(time_array) + +# Initialize temperature arrays for each system +T_A = np.zeros(num_steps) +T_B = np.zeros(num_steps) +T_C = np.zeros(num_steps) + +T_A[0] = T_A0 +T_B[0] = T_B0 +T_C[0] = T_C0 + +# Simulation: update the temperatures using a simple Euler method. +# System A exchanges heat with B. +# System C exchanges heat with B. +# System B is connected to both A and C. +for i in range(1, num_steps): + dT_A = k * (T_B[i-1] - T_A[i-1]) + dT_B = k * ((T_A[i-1] - T_B[i-1]) + (T_C[i-1] - T_B[i-1])) + dT_C = k * (T_B[i-1] - T_C[i-1]) + + T_A[i] = T_A[i-1] + dT_A * dt + T_B[i] = T_B[i-1] + dT_B * dt + T_C[i] = T_C[i-1] + dT_C * dt + +# Plot the results using Matplotlib +fig, ax = plt.subplots() +ax.plot(time_array, T_A, label="System A", color='red') +ax.plot(time_array, T_B, label="System B", color='green') +ax.plot(time_array, T_C, label="System C", color='blue') +ax.set_xlabel("Time (s)") +ax.set_ylabel("Temperature (°C)") +ax.set_title("Thermal Equilibrium Through Heat Exchange") +ax.legend() +st.pyplot(fig)