Spaces:
Sleeping
Sleeping
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from dataclasses import dataclass | |
| from typing import List, Tuple, Optional | |
| class Motion1D: | |
| """1D Kinematic motion calculator for point mass""" | |
| initial_position: float = 0.0 # x0 (m) | |
| initial_velocity: float = 0.0 # v0 (m/s) | |
| acceleration: float = 0.0 # a (m/s²) | |
| def position(self, t: float) -> float: | |
| """Calculate position at time t using x = x0 + v0*t + 0.5*a*t²""" | |
| return self.initial_position + self.initial_velocity * t + 0.5 * self.acceleration * t**2 | |
| def velocity(self, t: float) -> float: | |
| """Calculate velocity at time t using v = v0 + a*t""" | |
| return self.initial_velocity + self.acceleration * t | |
| def time_arrays(self, duration: float, dt: float = 0.01) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: | |
| """Generate time arrays for position, velocity, and acceleration""" | |
| t = np.arange(0, duration + dt, dt) | |
| x = np.array([self.position(time) for time in t]) | |
| v = np.array([self.velocity(time) for time in t]) | |
| a = np.full_like(t, self.acceleration) | |
| return t, x, v, a | |
| class Motion2D: | |
| """2D Kinematic motion calculator (projectile motion)""" | |
| launch_speed: float = 0.0 # Initial speed magnitude (m/s) | |
| launch_angle: float = 0.0 # Launch angle in degrees | |
| launch_height: float = 0.0 # Launch height above ground (m) | |
| launch_x: float = 0.0 # Horizontal launch position (m) | |
| gravity: float = 9.81 # Acceleration due to gravity (m/s²) | |
| air_resistance: bool = False # Enable/disable air resistance | |
| drag_coefficient: float = 0.1 # Simple drag coefficient (1/s) | |
| is_sphere: bool = False # Toggle between point mass and sphere | |
| sphere_radius: float = 0.037 # Sphere radius in meters (default: baseball ~37mm) | |
| sphere_density: float = 700 # Sphere density in kg/m³ (default: baseball ~700 kg/m³) | |
| air_density: float = 1.225 # Air density in kg/m³ (sea level) | |
| sphere_drag_coeff: float = 0.47 # Aerodynamic drag coefficient for smooth sphere | |
| def __post_init__(self): | |
| """Calculate velocity components and sphere properties from launch parameters""" | |
| angle_rad = np.radians(self.launch_angle) | |
| self.initial_velocity_x = self.launch_speed * np.cos(angle_rad) | |
| self.initial_velocity_y = self.launch_speed * np.sin(angle_rad) | |
| self.initial_position = (self.launch_x, self.launch_height) | |
| self.acceleration = (0.0, -self.gravity) | |
| # Calculate sphere properties if using sphere model | |
| if self.is_sphere: | |
| self.sphere_volume = (4/3) * np.pi * self.sphere_radius**3 | |
| self.sphere_mass = self.sphere_density * self.sphere_volume | |
| self.sphere_cross_section = np.pi * self.sphere_radius**2 | |
| # For sphere: F_drag = 0.5 * rho * Cd * A * v² | |
| # In our linear approximation: F_drag = k * v * m (where k has units 1/s) | |
| # So: k = (0.5 * rho * Cd * A) / m | |
| self.effective_drag_coeff = (0.5 * self.air_density * self.sphere_drag_coeff * | |
| self.sphere_cross_section) / self.sphere_mass | |
| else: | |
| # Point mass defaults | |
| self.sphere_mass = 1.0 | |
| self.sphere_volume = 0.0 | |
| self.sphere_cross_section = 0.0 | |
| self.effective_drag_coeff = self.drag_coefficient | |
| def position(self, t: float) -> Tuple[float, float]: | |
| """Calculate 2D position at time t""" | |
| if not self.air_resistance: | |
| # No air resistance - standard kinematic equations | |
| x = self.initial_position[0] + self.initial_velocity_x * t | |
| y = self.initial_position[1] + self.initial_velocity_y * t - 0.5 * self.gravity * t**2 | |
| else: | |
| # With air resistance - improved model | |
| k = self.effective_drag_coeff | |
| if k == 0: | |
| # Fallback to no air resistance | |
| x = self.initial_position[0] + self.initial_velocity_x * t | |
| y = self.initial_position[1] + self.initial_velocity_y * t - 0.5 * self.gravity * t**2 | |
| else: | |
| # Terminal velocity | |
| v_terminal = self.gravity / k | |
| exp_kt = np.exp(-k * t) | |
| # Horizontal motion (no forces except drag) | |
| x = self.initial_position[0] + (self.initial_velocity_x / k) * (1 - exp_kt) | |
| # Vertical motion (gravity + drag) | |
| # Analytical solution: y = y0 + (v0y + vt)/k * (1 - e^(-kt)) - vt*t | |
| y = (self.initial_position[1] + | |
| (self.initial_velocity_y + v_terminal) / k * (1 - exp_kt) - | |
| v_terminal * t) | |
| return x, y | |
| def velocity(self, t: float) -> Tuple[float, float]: | |
| """Calculate 2D velocity at time t""" | |
| if not self.air_resistance: | |
| # No air resistance | |
| vx = self.initial_velocity_x | |
| vy = self.initial_velocity_y - self.gravity * t | |
| else: | |
| # With air resistance | |
| k = self.effective_drag_coeff | |
| exp_kt = np.exp(-k * t) | |
| # Horizontal velocity | |
| vx = self.initial_velocity_x * exp_kt | |
| # Vertical velocity | |
| v_terminal = self.gravity / k if k > 0 else float('inf') | |
| vy = (self.initial_velocity_y + v_terminal) * exp_kt - v_terminal | |
| return vx, vy | |
| # Lines 140-152 - Update get_sphere_info method | |
| def get_sphere_info(self) -> dict: | |
| """Return sphere physical properties""" | |
| if not self.is_sphere: | |
| return {} | |
| # Calculate terminal velocity: vt = mg/(0.5 * rho * Cd * A) = g/k | |
| terminal_vel = self.gravity / self.effective_drag_coeff if self.effective_drag_coeff > 0 else float('inf') | |
| return { | |
| 'radius_mm': self.sphere_radius * 1000, | |
| 'diameter_mm': self.sphere_radius * 2000, | |
| 'mass_g': self.sphere_mass * 1000, | |
| 'volume_cm3': self.sphere_volume * 1000000, | |
| 'cross_section_cm2': self.sphere_cross_section * 10000, | |
| 'density_kg_m3': self.sphere_density, | |
| 'terminal_velocity_ms': terminal_vel | |
| } | |
| def get_launch_info(self) -> dict: | |
| """Return comprehensive launch information""" | |
| flight_time = self.calculate_flight_time() | |
| info = { | |
| 'launch_speed': self.launch_speed, | |
| 'launch_angle': self.launch_angle, | |
| 'launch_height': self.launch_height, | |
| 'initial_velocity_x': self.initial_velocity_x, | |
| 'initial_velocity_y': self.initial_velocity_y, | |
| 'flight_time': flight_time, | |
| 'range': self.calculate_range(), | |
| 'max_height': self.calculate_max_height(), | |
| 'air_resistance': self.air_resistance, | |
| 'is_sphere': self.is_sphere, | |
| 'effective_drag_coeff': self.effective_drag_coeff if self.air_resistance else 0 | |
| } | |
| # Add sphere info if applicable | |
| if self.is_sphere: | |
| info.update(self.get_sphere_info()) | |
| return info | |
| # Static method for common sphere presets | |
| def get_sphere_presets() -> dict: | |
| """Return common sphere presets with realistic properties""" | |
| return { | |
| 'ping_pong': { | |
| 'name': '🏓 Ping Pong Ball', | |
| 'radius': 0.02, # 20mm radius (40mm diameter) | |
| 'density': 84, # Very light | |
| 'drag_coeff': 0.47 | |
| }, | |
| 'golf': { | |
| 'name': '⛳ Golf Ball', | |
| 'radius': 0.0214, # 21.4mm radius (42.8mm diameter) | |
| 'density': 1130, # Dense | |
| 'drag_coeff': 0.24 # Dimpled surface reduces drag | |
| }, | |
| 'baseball': { | |
| 'name': '⚾ Baseball', | |
| 'radius': 0.037, # 37mm radius (74mm diameter) | |
| 'density': 700, # Medium density | |
| 'drag_coeff': 0.35 # Stitched surface | |
| }, | |
| 'tennis': { | |
| 'name': '🎾 Tennis Ball', | |
| 'radius': 0.033, # 33mm radius (66mm diameter) | |
| 'density': 370, # Light with hollow core | |
| 'drag_coeff': 0.51 # Fuzzy surface increases drag | |
| }, | |
| 'basketball': { | |
| 'name': '🏀 Basketball', | |
| 'radius': 0.12, # 120mm radius (240mm diameter) | |
| 'density': 60, # Very light (hollow) | |
| 'drag_coeff': 0.47 | |
| }, | |
| 'bowling': { | |
| 'name': '🎳 Bowling Ball', | |
| 'radius': 0.108, # 108mm radius (216mm diameter) | |
| 'density': 1400, # Very dense | |
| 'drag_coeff': 0.47 | |
| } | |
| } | |
| def trajectory_data(self, duration: float, dt: float = 0.01) -> dict: | |
| """Generate complete trajectory data, stopping when projectile hits ground""" | |
| t = np.arange(0, duration + dt, dt) | |
| positions = [] | |
| velocities = [] | |
| times = [] | |
| for time in t: | |
| pos = self.position(time) | |
| vel = self.velocity(time) | |
| # Stop if projectile goes below ground level (y < 0) | |
| if pos[1] < 0 and len(positions) > 0: | |
| break | |
| positions.append(pos) | |
| velocities.append(vel) | |
| times.append(time) | |
| if len(positions) == 0: | |
| # Handle edge case | |
| positions = [(self.launch_x, self.launch_height)] | |
| velocities = [(self.initial_velocity_x, self.initial_velocity_y)] | |
| times = [0] | |
| positions = np.array(positions) | |
| velocities = np.array(velocities) | |
| times = np.array(times) | |
| return { | |
| 'time': times, | |
| 'x': positions[:, 0], | |
| 'y': positions[:, 1], | |
| 'vx': velocities[:, 0], | |
| 'vy': velocities[:, 1], | |
| 'speed': np.sqrt(velocities[:, 0]**2 + velocities[:, 1]**2) | |
| } | |
| def calculate_flight_time(self) -> float: | |
| """Calculate approximate flight time""" | |
| if not self.air_resistance: | |
| # Original calculation without air resistance | |
| h = self.launch_height | |
| vy0 = self.initial_velocity_y | |
| g = self.gravity | |
| if g == 0: | |
| if vy0 == 0: | |
| return float('inf') if h >= 0 else 0 | |
| return h / vy0 if vy0 < 0 else float('inf') | |
| discriminant = vy0**2 + 2*g*h | |
| if discriminant < 0: | |
| return 0 | |
| t1 = (vy0 + np.sqrt(discriminant)) / g | |
| t2 = (vy0 - np.sqrt(discriminant)) / g | |
| return max(t1, t2) if max(t1, t2) > 0 else 0 | |
| else: | |
| # With air resistance, use numerical approach to find landing time | |
| # This is an approximation - we'll simulate until we hit ground | |
| max_time = 20.0 # Maximum simulation time | |
| dt = 0.01 | |
| for t in np.arange(0, max_time, dt): | |
| _, y = self.position(t) | |
| if y <= 0 and t > 0: | |
| return t | |
| return max_time # Fallback | |
| def calculate_range(self) -> float: | |
| """Calculate horizontal range of projectile""" | |
| flight_time = self.calculate_flight_time() | |
| if flight_time <= 0: | |
| return 0 | |
| # Get final position | |
| final_x, _ = self.position(flight_time) | |
| return final_x - self.launch_x | |
| def get_launch_info(self) -> dict: | |
| """Return comprehensive launch information""" | |
| flight_time = self.calculate_flight_time() | |
| info = { | |
| 'launch_speed': self.launch_speed, | |
| 'launch_angle': self.launch_angle, | |
| 'launch_height': self.launch_height, | |
| 'initial_velocity_x': self.initial_velocity_x, | |
| 'initial_velocity_y': self.initial_velocity_y, | |
| 'flight_time': flight_time, | |
| 'range': self.calculate_range(), | |
| 'max_height': self.calculate_max_height(), | |
| 'air_resistance': self.air_resistance, | |
| 'is_sphere': self.is_sphere, | |
| 'effective_drag_coeff': self.effective_drag_coeff if self.air_resistance else 0 | |
| } | |
| # Add sphere info if applicable | |
| if self.is_sphere: | |
| sphere_info = self.get_sphere_info() | |
| info.update(sphere_info) | |
| return info | |
| def calculate_max_height(self) -> float: | |
| """Calculate maximum height reached by projectile""" | |
| if not self.air_resistance: | |
| # Analytical solution for no air resistance | |
| if self.gravity == 0: | |
| return float('inf') if self.initial_velocity_y > 0 else self.launch_height | |
| # Time to reach maximum height: when vy = 0 | |
| t_max = self.initial_velocity_y / self.gravity | |
| if t_max <= 0: # Projectile going downward initially | |
| return self.launch_height | |
| _, max_y = self.position(t_max) | |
| return max(max_y, self.launch_height) | |
| else: | |
| # Numerical approach for air resistance | |
| flight_time = self.calculate_flight_time() | |
| dt = 0.01 | |
| max_height = self.launch_height | |
| for t in np.arange(0, flight_time + dt, dt): | |
| _, y = self.position(t) | |
| if y > max_height: | |
| max_height = y | |
| return max_height | |
| class KinematicsVisualizer: | |
| """Create visualizations for kinematic motion""" | |
| def plot_1d_motion(motion: Motion1D, duration: float, title: str = "1D Kinematic Motion"): | |
| """Create comprehensive 1D motion plots""" | |
| t, x, v, a = motion.time_arrays(duration) | |
| fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12)) | |
| fig.suptitle(title, fontsize=16, fontweight='bold') | |
| # Position vs Time | |
| ax1.plot(t, x, 'b-', linewidth=2, label='Position') | |
| ax1.set_ylabel('Position (m)') | |
| ax1.grid(True, alpha=0.3) | |
| ax1.legend() | |
| ax1.set_title('Position vs Time') | |
| # Velocity vs Time | |
| ax2.plot(t, v, 'r-', linewidth=2, label='Velocity') | |
| ax2.set_ylabel('Velocity (m/s)') | |
| ax2.grid(True, alpha=0.3) | |
| ax2.legend() | |
| ax2.set_title('Velocity vs Time') | |
| # Acceleration vs Time | |
| ax3.plot(t, a, 'g-', linewidth=2, label='Acceleration') | |
| ax3.set_xlabel('Time (s)') | |
| ax3.set_ylabel('Acceleration (m/s²)') | |
| ax3.grid(True, alpha=0.3) | |
| ax3.legend() | |
| ax3.set_title('Acceleration vs Time') | |
| plt.tight_layout() | |
| return fig | |
| def plot_2d_trajectory(motion: Motion2D, duration: float = None, title: str = "2D Projectile Motion"): | |
| """Create 2D trajectory visualization with launch info""" | |
| if duration is None: | |
| duration = motion.calculate_flight_time() | |
| data = motion.trajectory_data(duration) | |
| info = motion.get_launch_info() | |
| fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10)) | |
| # Create detailed title with launch parameters | |
| detailed_title = (f"{title}\n" | |
| f"Launch: {info['launch_speed']:.1f} m/s at {info['launch_angle']:.1f}° " | |
| f"from {info['launch_height']:.1f}m height") | |
| fig.suptitle(detailed_title, fontsize=14, fontweight='bold') | |
| # Trajectory plot with key points marked | |
| ax1.plot(data['x'], data['y'], 'b-', linewidth=2, label='Trajectory') | |
| # Mark launch point | |
| ax1.plot(motion.launch_x, motion.launch_height, 'go', markersize=8, label='Launch') | |
| # Mark landing point | |
| if len(data['x']) > 0: | |
| ax1.plot(data['x'][-1], data['y'][-1], 'ro', markersize=8, label='Landing') | |
| # Mark maximum height | |
| max_height_idx = np.argmax(data['y']) | |
| if len(data['x']) > 0: | |
| ax1.plot(data['x'][max_height_idx], data['y'][max_height_idx], 'mo', markersize=6, label='Max Height') | |
| ax1.set_xlabel('Horizontal Position (m)') | |
| ax1.set_ylabel('Vertical Position (m)') | |
| ax1.grid(True, alpha=0.3) | |
| ax1.legend() | |
| ax1.set_title(f'Trajectory (Range: {info["range"]:.1f}m, Max Height: {info["max_height"]:.1f}m)') | |
| ax1.set_ylim(bottom=0) # Ensure ground is at y=0 | |
| # Speed vs Time | |
| ax2.plot(data['time'], data['speed'], 'r-', linewidth=2) | |
| ax2.set_xlabel('Time (s)') | |
| ax2.set_ylabel('Speed (m/s)') | |
| ax2.grid(True, alpha=0.3) | |
| ax2.set_title(f'Speed vs Time (Flight Time: {info["flight_time"]:.1f}s)') | |
| # Velocity components | |
| ax3.plot(data['time'], data['vx'], 'g-', linewidth=2, label=f'Vx (const: {info["initial_velocity_x"]:.1f})') | |
| ax3.plot(data['time'], data['vy'], 'm-', linewidth=2, label=f'Vy (initial: {info["initial_velocity_y"]:.1f})') | |
| ax3.axhline(y=0, color='k', linestyle='--', alpha=0.5) | |
| ax3.set_xlabel('Time (s)') | |
| ax3.set_ylabel('Velocity (m/s)') | |
| ax3.grid(True, alpha=0.3) | |
| ax3.legend() | |
| ax3.set_title('Velocity Components vs Time') | |
| # Position components | |
| ax4.plot(data['time'], data['x'], 'c-', linewidth=2, label='X position') | |
| ax4.plot(data['time'], data['y'], 'orange', linewidth=2, label='Y position') | |
| ax4.axhline(y=0, color='k', linestyle='--', alpha=0.5, label='Ground level') | |
| ax4.set_xlabel('Time (s)') | |
| ax4.set_ylabel('Position (m)') | |
| ax4.grid(True, alpha=0.3) | |
| ax4.legend() | |
| ax4.set_title('Position Components vs Time') | |
| plt.tight_layout() | |
| return fig | |
| # Example usage and test cases | |
| if __name__ == "__main__": | |
| # Example 1: Constant acceleration (car accelerating) | |
| car_motion = Motion1D(initial_position=0, initial_velocity=5, acceleration=2) | |
| # Example 2: Free fall | |
| free_fall = Motion1D(initial_position=100, initial_velocity=0, acceleration=-9.81) | |
| # Example 3: Classic projectile motion (45-degree launch from ground) | |
| projectile_45 = Motion2D( | |
| launch_speed=25, # m/s | |
| launch_angle=45, # degrees | |
| launch_height=0, # meters (ground level) | |
| launch_x=0 | |
| ) | |
| # Example 4: Projectile launched from height at 30 degrees | |
| projectile_height = Motion2D( | |
| launch_speed=20, # m/s | |
| launch_angle=30, # degrees | |
| launch_height=10, # meters | |
| launch_x=0 | |
| ) | |
| # Example 5: Horizontal launch (like dropping a ball while moving) | |
| horizontal_launch = Motion2D( | |
| launch_speed=15, # m/s | |
| launch_angle=0, # degrees (horizontal) | |
| launch_height=20, # meters | |
| launch_x=0 | |
| ) | |
| # Create visualizations | |
| visualizer = KinematicsVisualizer() | |
| # Plot 1D examples | |
| visualizer.plot_1d_motion(car_motion, 10, "Car Acceleration") | |
| visualizer.plot_1d_motion(free_fall, 4.5, "Free Fall Motion") | |
| # Plot 2D examples with different launch conditions | |
| visualizer.plot_2d_trajectory(projectile_45, title="45° Launch from Ground") | |
| visualizer.plot_2d_trajectory(projectile_height, title="30° Launch from 10m Height") | |
| visualizer.plot_2d_trajectory(horizontal_launch, title="Horizontal Launch from 20m") | |
| # Print launch information for educational purposes | |
| print("\n=== LAUNCH ANALYSIS ===") | |
| for name, motion in [("45° Ground Launch", projectile_45), | |
| ("30° Height Launch", projectile_height), | |
| ("Horizontal Launch", horizontal_launch)]: | |
| info = motion.get_launch_info() | |
| print(f"\n{name}:") | |
| print(f" Launch Speed: {info['launch_speed']:.1f} m/s at {info['launch_angle']:.1f}°") | |
| print(f" Initial Velocity: ({info['initial_velocity_x']:.1f}, {info['initial_velocity_y']:.1f}) m/s") | |
| print(f" Flight Time: {info['flight_time']:.2f} s") | |
| print(f" Range: {info['range']:.1f} m") | |
| print(f" Max Height: {info['max_height']:.1f} m") | |
| plt.show() |