import numpy as np import matplotlib.pyplot as plt import streamlit as st import math import random class Vector2D: """A 2D vector class for physics calculations""" def __init__(self, x=0, y=0, magnitude=None, angle=None): if magnitude is not None and angle is not None: # Create vector from magnitude and angle (in degrees) self.x = magnitude * math.cos(math.radians(angle)) self.y = magnitude * math.sin(math.radians(angle)) else: self.x = x self.y = y def __repr__(self): return f"Vector2D(x={self.x:.2f}, y={self.y:.2f}, mag={self.magnitude:.2f}, angle={self.angle:.1f}°)" def __rmul__(self, scalar): """Allow scalar * vector multiplication""" return self.__mul__(scalar) def cross_product_2d(self, other): """2D cross product (returns scalar z-component)""" return self.x * other.y - self.y * other.x def angle_between(self, other): """Calculate angle between two vectors in degrees""" dot_prod = self.dot_product(other) mags = self.magnitude * other.magnitude if mags == 0: return 0 cos_angle = dot_prod / mags # Clamp to avoid floating point errors cos_angle = max(-1, min(1, cos_angle)) return math.degrees(math.acos(cos_angle)) @property def magnitude(self): return math.sqrt(self.x**2 + self.y**2) @property def angle(self): return math.degrees(math.atan2(self.y, self.x)) def __add__(self, other): return Vector2D(self.x + other.x, self.y + other.y) def __sub__(self, other): return Vector2D(self.x - other.x, self.y - other.y) def __mul__(self, scalar): return Vector2D(self.x * scalar, self.y * scalar) def dot_product(self, other): return self.x * other.x + self.y * other.y def unit_vector(self): mag = self.magnitude if mag == 0: return Vector2D(0, 0) return Vector2D(self.x / mag, self.y / mag) class UnitConverter: """Unit conversion utilities for physics calculations""" @staticmethod def speed_conversions(): return { "m/s": 1.0, "km/h": 3.6, "mph": 2.237, "ft/s": 3.281, "knots": 1.944 } @staticmethod def distance_conversions(): return { "meters": 1.0, "kilometers": 0.001, "miles": 0.000621, "feet": 3.281, "yards": 1.094 } @staticmethod def convert_speed(value, from_unit, to_unit): conversions = UnitConverter.speed_conversions() # Convert to m/s first, then to target unit ms_value = value / conversions[from_unit] return ms_value * conversions[to_unit] @staticmethod def convert_distance(value, from_unit, to_unit): conversions = UnitConverter.distance_conversions() # Convert to meters first, then to target unit m_value = value / conversions[from_unit] return m_value * conversions[to_unit] class QuizGenerator: """Generate quiz questions for vector concepts""" def __init__(self): self.question_types = [ "vector_addition", "vector_magnitude", "vector_angle", "boat_crossing", "projectile_range" ] def generate_question(self): question_type = random.choice(self.question_types) if question_type == "vector_addition": return self._vector_addition_question() elif question_type == "vector_magnitude": return self._vector_magnitude_question() elif question_type == "vector_angle": return self._vector_angle_question() elif question_type == "boat_crossing": return self._boat_crossing_question() elif question_type == "projectile_range": return self._projectile_range_question() def _vector_addition_question(self): mag_a = random.uniform(3, 8) angle_a = random.choice([0, 30, 45, 60, 90, 120, 135, 150, 180]) mag_b = random.uniform(3, 8) angle_b = random.choice([0, 30, 45, 60, 90, 120, 135, 150, 180]) vec_a = Vector2D(magnitude=mag_a, angle=angle_a) vec_b = Vector2D(magnitude=mag_b, angle=angle_b) result = vec_a + vec_b return { "type": "vector_addition", "question": f"Vector A has magnitude {mag_a:.1f} at {angle_a}°, Vector B has magnitude {mag_b:.1f} at {angle_b}°. What is the magnitude of their sum?", "answer": result.magnitude, "tolerance": 0.5, "explanation": f"Vector A + Vector B = ({vec_a.x:.2f}, {vec_a.y:.2f}) + ({vec_b.x:.2f}, {vec_b.y:.2f}) = ({result.x:.2f}, {result.y:.2f})\nMagnitude = √({result.x:.2f}² + {result.y:.2f}²) = {result.magnitude:.2f}" } def _vector_magnitude_question(self): x = random.uniform(-10, 10) y = random.uniform(-10, 10) vec = Vector2D(x, y) return { "type": "vector_magnitude", "question": f"A vector has components x = {x:.1f} and y = {y:.1f}. What is its magnitude?", "answer": vec.magnitude, "tolerance": 0.2, "explanation": f"Magnitude = √(x² + y²) = √({x:.1f}² + {y:.1f}²) = √({x**2:.1f} + {y**2:.1f}) = {vec.magnitude:.2f}" } def _vector_angle_question(self): x = random.uniform(-10, 10) y = random.uniform(-10, 10) vec = Vector2D(x, y) return { "type": "vector_angle", "question": f"A vector has components x = {x:.1f} and y = {y:.1f}. What is its angle in degrees?", "answer": vec.angle, "tolerance": 2.0, "explanation": f"Angle = arctan(y/x) = arctan({y:.1f}/{x:.1f}) = {vec.angle:.1f}°" } def _boat_crossing_question(self): boat_speed = random.uniform(4, 8) current_speed = random.uniform(2, 5) boat_angle = 90 # Straight across current_angle = 180 # Opposite direction boat_vel = Vector2D(magnitude=boat_speed, angle=boat_angle) current_vel = Vector2D(magnitude=current_speed, angle=current_angle) result = boat_vel + current_vel return { "type": "boat_crossing", "question": f"A boat aims straight across a river at {boat_speed:.1f} m/s. The current flows at {current_speed:.1f} m/s opposite to the boat. What is the boat's actual speed?", "answer": result.magnitude, "tolerance": 0.3, "explanation": f"Resultant velocity = √({boat_speed}² + {current_speed}²) = {result.magnitude:.2f} m/s" } def _projectile_range_question(self): speed = random.uniform(15, 25) angle = 45 # Optimal angle gravity = 9.81 # Range formula: R = v²sin(2θ)/g range_m = (speed**2 * math.sin(math.radians(2 * angle))) / gravity return { "type": "projectile_range", "question": f"A projectile is launched at {speed:.0f} m/s at 45°. What is its range in meters? (g = 9.81 m/s²)", "answer": range_m, "tolerance": 2.0, "explanation": f"Range = v²sin(2θ)/g = {speed}²×sin(90°)/9.81 = {speed**2}/9.81 = {range_m:.1f} m" } class VectorPhysicsSimulator: """Physics simulator for vector problems""" def __init__(self): self.time_step = 0.1 self.max_time = 20 def boat_crossing_problem(self, boat_speed, boat_angle, current_speed, current_angle): """ Simulate a boat crossing a river with current Returns the resultant velocity and trajectory """ boat_velocity = Vector2D(magnitude=boat_speed, angle=boat_angle) current_velocity = Vector2D(magnitude=current_speed, angle=current_angle) # Resultant velocity resultant_velocity = boat_velocity + current_velocity # Calculate angle between intended and actual heading heading_difference = boat_velocity.angle_between(resultant_velocity) # Calculate trajectory points time_points = np.arange(0, self.max_time, self.time_step) x_points = resultant_velocity.x * time_points y_points = resultant_velocity.y * time_points return { 'boat_velocity': boat_velocity, 'current_velocity': current_velocity, 'resultant_velocity': resultant_velocity, 'heading_difference': heading_difference, 'trajectory_x': x_points, 'trajectory_y': y_points, 'time_points': time_points } def projectile_motion(self, initial_speed, launch_angle, gravity=9.81): """ Simulate projectile motion with error analysis """ initial_velocity = Vector2D(magnitude=initial_speed, angle=launch_angle) # Calculate trajectory time_flight = 2 * initial_velocity.y / gravity if initial_velocity.y > 0 else 0 time_points = np.linspace(0, time_flight, 100) if time_flight > 0 else np.array([0]) x_points = initial_velocity.x * time_points y_points = initial_velocity.y * time_points - 0.5 * gravity * time_points**2 # Error analysis - show effect of 5% measurement uncertainty speed_error = initial_speed * 0.05 angle_error = 2 # 2 degree uncertainty # Upper bound upper_vel = Vector2D(magnitude=initial_speed + speed_error, angle=launch_angle + angle_error) upper_flight = 2 * upper_vel.y / gravity if upper_vel.y > 0 else 0 upper_time = np.linspace(0, upper_flight, 100) if upper_flight > 0 else np.array([0]) upper_x = upper_vel.x * upper_time upper_y = upper_vel.y * upper_time - 0.5 * gravity * upper_time**2 # Lower bound lower_vel = Vector2D(magnitude=max(0.1, initial_speed - speed_error), angle=launch_angle - angle_error) lower_flight = 2 * lower_vel.y / gravity if lower_vel.y > 0 else 0 lower_time = np.linspace(0, lower_flight, 100) if lower_flight > 0 else np.array([0]) lower_x = lower_vel.x * lower_time lower_y = lower_vel.y * lower_time - 0.5 * gravity * lower_time**2 return { 'initial_velocity': initial_velocity, 'trajectory_x': x_points, 'trajectory_y': y_points, 'time_points': time_points, 'max_range': max(x_points) if len(x_points) > 0 else 0, 'max_height': max(y_points) if len(y_points) > 0 else 0, 'error_upper_x': upper_x, 'error_upper_y': upper_y, 'error_lower_x': lower_x, 'error_lower_y': lower_y, 'speed_uncertainty': speed_error, 'angle_uncertainty': angle_error } def plot_vectors(ax, vectors, labels, colors, origin=(0, 0)): """Plot vectors on matplotlib axes""" ax.clear() for vector, label, color in zip(vectors, labels, colors): ax.arrow(origin[0], origin[1], vector.x, vector.y, head_width=0.5, head_length=0.5, fc=color, ec=color, linewidth=2, label=label) ax.set_xlim(-10, 15) ax.set_ylim(-10, 15) ax.grid(True, alpha=0.3) ax.set_aspect('equal') ax.legend() ax.set_xlabel('X (m/s)') ax.set_ylabel('Y (m/s)') def plot_trajectory(ax, x_points, y_points, title="Trajectory", error_bounds=None): """Plot trajectory on matplotlib axes with optional error bounds""" ax.clear() ax.plot(x_points, y_points, 'b-', linewidth=2, label='Trajectory') if error_bounds: ax.plot(error_bounds['upper_x'], error_bounds['upper_y'], 'r--', alpha=0.6, label='Upper bound (+5% speed, +2° angle)') ax.plot(error_bounds['lower_x'], error_bounds['lower_y'], 'g--', alpha=0.6, label='Lower bound (-5% speed, -2° angle)') ax.fill_between(error_bounds['upper_x'], error_bounds['upper_y'], error_bounds['lower_y'][:len(error_bounds['upper_y'])], alpha=0.2, color='yellow', label='Uncertainty range') ax.scatter(x_points[0], y_points[0], color='green', s=100, label='Start', zorder=5) if len(x_points) > 1: ax.scatter(x_points[-1], y_points[-1], color='red', s=100, label='End', zorder=5) ax.grid(True, alpha=0.3) ax.set_xlabel('X Position (m)') ax.set_ylabel('Y Position (m)') ax.set_title(title) ax.legend()