NavyDevilDoc commited on
Commit
1ac3c60
·
verified ·
1 Parent(s): 3fcd619

Upload kinematics_visualizer.py

Browse files
Files changed (1) hide show
  1. src/kinematics_visualizer.py +509 -0
src/kinematics_visualizer.py ADDED
@@ -0,0 +1,509 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from dataclasses import dataclass
4
+ from typing import List, Tuple, Optional
5
+
6
+ @dataclass
7
+ class Motion1D:
8
+ """1D Kinematic motion calculator for point mass"""
9
+ initial_position: float = 0.0 # x0 (m)
10
+ initial_velocity: float = 0.0 # v0 (m/s)
11
+ acceleration: float = 0.0 # a (m/s²)
12
+
13
+ def position(self, t: float) -> float:
14
+ """Calculate position at time t using x = x0 + v0*t + 0.5*a*t²"""
15
+ return self.initial_position + self.initial_velocity * t + 0.5 * self.acceleration * t**2
16
+
17
+ def velocity(self, t: float) -> float:
18
+ """Calculate velocity at time t using v = v0 + a*t"""
19
+ return self.initial_velocity + self.acceleration * t
20
+
21
+ def time_arrays(self, duration: float, dt: float = 0.01) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
22
+ """Generate time arrays for position, velocity, and acceleration"""
23
+ t = np.arange(0, duration + dt, dt)
24
+ x = np.array([self.position(time) for time in t])
25
+ v = np.array([self.velocity(time) for time in t])
26
+ a = np.full_like(t, self.acceleration)
27
+ return t, x, v, a
28
+
29
+ @dataclass
30
+ class Motion2D:
31
+ """2D Kinematic motion calculator (projectile motion)"""
32
+ launch_speed: float = 0.0 # Initial speed magnitude (m/s)
33
+ launch_angle: float = 0.0 # Launch angle in degrees
34
+ launch_height: float = 0.0 # Launch height above ground (m)
35
+ launch_x: float = 0.0 # Horizontal launch position (m)
36
+ gravity: float = 9.81 # Acceleration due to gravity (m/s²)
37
+ air_resistance: bool = False # Enable/disable air resistance
38
+ drag_coefficient: float = 0.1 # Simple drag coefficient (1/s)
39
+ is_sphere: bool = False # Toggle between point mass and sphere
40
+ sphere_radius: float = 0.037 # Sphere radius in meters (default: baseball ~37mm)
41
+ sphere_density: float = 700 # Sphere density in kg/m³ (default: baseball ~700 kg/m³)
42
+ air_density: float = 1.225 # Air density in kg/m³ (sea level)
43
+ sphere_drag_coeff: float = 0.47 # Aerodynamic drag coefficient for smooth sphere
44
+
45
+
46
+ def __post_init__(self):
47
+ """Calculate velocity components and sphere properties from launch parameters"""
48
+ angle_rad = np.radians(self.launch_angle)
49
+ self.initial_velocity_x = self.launch_speed * np.cos(angle_rad)
50
+ self.initial_velocity_y = self.launch_speed * np.sin(angle_rad)
51
+ self.initial_position = (self.launch_x, self.launch_height)
52
+ self.acceleration = (0.0, -self.gravity)
53
+
54
+ # Calculate sphere properties if using sphere model
55
+ if self.is_sphere:
56
+ self.sphere_volume = (4/3) * np.pi * self.sphere_radius**3
57
+ self.sphere_mass = self.sphere_density * self.sphere_volume
58
+ self.sphere_cross_section = np.pi * self.sphere_radius**2
59
+
60
+ # Calculate realistic drag coefficient based on physics
61
+ # For sphere: drag = 0.5 * rho * Cd * A * v²
62
+ # We convert this to our linear model: drag = k * v
63
+ # where k depends on velocity, but we'll use a representative value
64
+ representative_velocity = self.launch_speed * 0.7 # Rough average during flight
65
+ if representative_velocity > 0:
66
+ # k = (0.5 * rho * Cd * A) / mass * (v / mass) for dimensional consistency
67
+ self.effective_drag_coeff = (0.5 * self.air_density * self.sphere_drag_coeff *
68
+ self.sphere_cross_section * representative_velocity) / self.sphere_mass
69
+ else:
70
+ self.effective_drag_coeff = 0.1
71
+ else:
72
+ # Point mass defaults
73
+ self.sphere_mass = 1.0 # Arbitrary unit mass
74
+ self.sphere_volume = 0.0
75
+ self.sphere_cross_section = 0.0
76
+ self.effective_drag_coeff = self.drag_coefficient
77
+
78
+ def position(self, t: float) -> Tuple[float, float]:
79
+ """Calculate 2D position at time t"""
80
+ if not self.air_resistance:
81
+ # No air resistance - original kinematic equations
82
+ x = self.initial_position[0] + self.initial_velocity_x * t
83
+ y = self.initial_position[1] + self.initial_velocity_y * t - 0.5 * self.gravity * t**2
84
+ else:
85
+ # With air resistance - exponential decay model
86
+ # This is a simplified model: v(t) = v0 * exp(-k*t)
87
+ k = self.effective_drag_coeff
88
+ # Horizontal motion with air resistance
89
+ if k == 0:
90
+ x = self.initial_position[0] + self.initial_velocity_x * t
91
+ else:
92
+ x = self.initial_position[0] + (self.initial_velocity_x / k) * (1 - np.exp(-k * t))
93
+
94
+ # Vertical motion with air resistance and gravity
95
+ # More complex - approximation using numerical integration would be more accurate
96
+ # For educational purposes, we'll use a simplified approach
97
+ if k == 0:
98
+ y = self.initial_position[1] + self.initial_velocity_y * t - 0.5 * self.gravity * t**2
99
+ else:
100
+ # Terminal velocity
101
+ v_terminal = self.gravity / k
102
+ # Simplified vertical motion with air resistance
103
+ exp_term = np.exp(-k * t)
104
+ y = (self.initial_position[1] +
105
+ (self.initial_velocity_y + v_terminal) / k * (1 - exp_term) -
106
+ v_terminal * t)
107
+
108
+ return x, y
109
+
110
+ def velocity(self, t: float) -> Tuple[float, float]:
111
+ """Calculate 2D velocity at time t"""
112
+ if not self.air_resistance:
113
+ # No air resistance
114
+ vx = self.initial_velocity_x
115
+ vy = self.initial_velocity_y - self.gravity * t
116
+ else:
117
+ # With air resistance
118
+ k = self.effective_drag_coeff
119
+ exp_term = np.exp(-k * t)
120
+
121
+ # Horizontal velocity with air resistance
122
+ vx = self.initial_velocity_x * exp_term
123
+
124
+ # Vertical velocity with air resistance and gravity
125
+ v_terminal = self.gravity / k
126
+ vy = (self.initial_velocity_y + v_terminal) * exp_term - v_terminal
127
+
128
+ return vx, vy
129
+
130
+ def get_sphere_info(self) -> dict:
131
+ """Return sphere physical properties"""
132
+ if not self.is_sphere:
133
+ return {}
134
+
135
+ return {
136
+ 'radius_mm': self.sphere_radius * 1000, # Convert to mm for display
137
+ 'diameter_mm': self.sphere_radius * 2000,
138
+ 'mass_g': self.sphere_mass * 1000, # Convert to grams
139
+ 'volume_cm3': self.sphere_volume * 1000000, # Convert to cm³
140
+ 'cross_section_cm2': self.sphere_cross_section * 10000, # Convert to cm²
141
+ 'density_kg_m3': self.sphere_density,
142
+ 'terminal_velocity': self.gravity / self.effective_drag_coeff if self.effective_drag_coeff > 0 else float('inf')
143
+ }
144
+
145
+
146
+ def get_launch_info(self) -> dict:
147
+ """Return comprehensive launch information"""
148
+ flight_time = self.calculate_flight_time()
149
+ info = {
150
+ 'launch_speed': self.launch_speed,
151
+ 'launch_angle': self.launch_angle,
152
+ 'launch_height': self.launch_height,
153
+ 'initial_velocity_x': self.initial_velocity_x,
154
+ 'initial_velocity_y': self.initial_velocity_y,
155
+ 'flight_time': flight_time,
156
+ 'range': self.calculate_range(),
157
+ 'max_height': self.calculate_max_height(),
158
+ 'air_resistance': self.air_resistance,
159
+ 'is_sphere': self.is_sphere,
160
+ 'effective_drag_coeff': self.effective_drag_coeff if self.air_resistance else 0
161
+ }
162
+
163
+ # Add sphere info if applicable
164
+ if self.is_sphere:
165
+ info.update(self.get_sphere_info())
166
+
167
+ return info
168
+
169
+
170
+ # Static method for common sphere presets
171
+ @staticmethod
172
+ def get_sphere_presets() -> dict:
173
+ """Return common sphere presets with realistic properties"""
174
+ return {
175
+ 'ping_pong': {
176
+ 'name': '🏓 Ping Pong Ball',
177
+ 'radius': 0.02, # 20mm radius (40mm diameter)
178
+ 'density': 84, # Very light
179
+ 'drag_coeff': 0.47
180
+ },
181
+ 'golf': {
182
+ 'name': '⛳ Golf Ball',
183
+ 'radius': 0.0214, # 21.4mm radius (42.8mm diameter)
184
+ 'density': 1130, # Dense
185
+ 'drag_coeff': 0.24 # Dimpled surface reduces drag
186
+ },
187
+ 'baseball': {
188
+ 'name': '⚾ Baseball',
189
+ 'radius': 0.037, # 37mm radius (74mm diameter)
190
+ 'density': 700, # Medium density
191
+ 'drag_coeff': 0.35 # Stitched surface
192
+ },
193
+ 'tennis': {
194
+ 'name': '🎾 Tennis Ball',
195
+ 'radius': 0.033, # 33mm radius (66mm diameter)
196
+ 'density': 370, # Light with hollow core
197
+ 'drag_coeff': 0.51 # Fuzzy surface increases drag
198
+ },
199
+ 'basketball': {
200
+ 'name': '🏀 Basketball',
201
+ 'radius': 0.12, # 120mm radius (240mm diameter)
202
+ 'density': 60, # Very light (hollow)
203
+ 'drag_coeff': 0.47
204
+ },
205
+ 'bowling': {
206
+ 'name': '🎳 Bowling Ball',
207
+ 'radius': 0.108, # 108mm radius (216mm diameter)
208
+ 'density': 1400, # Very dense
209
+ 'drag_coeff': 0.47
210
+ }
211
+ }
212
+
213
+
214
+ def trajectory_data(self, duration: float, dt: float = 0.01) -> dict:
215
+ """Generate complete trajectory data, stopping when projectile hits ground"""
216
+ t = np.arange(0, duration + dt, dt)
217
+ positions = []
218
+ velocities = []
219
+ times = []
220
+
221
+ for time in t:
222
+ pos = self.position(time)
223
+ vel = self.velocity(time)
224
+
225
+ # Stop if projectile goes below ground level (y < 0)
226
+ if pos[1] < 0 and len(positions) > 0:
227
+ break
228
+
229
+ positions.append(pos)
230
+ velocities.append(vel)
231
+ times.append(time)
232
+
233
+ if len(positions) == 0:
234
+ # Handle edge case
235
+ positions = [(self.launch_x, self.launch_height)]
236
+ velocities = [(self.initial_velocity_x, self.initial_velocity_y)]
237
+ times = [0]
238
+
239
+ positions = np.array(positions)
240
+ velocities = np.array(velocities)
241
+ times = np.array(times)
242
+
243
+ return {
244
+ 'time': times,
245
+ 'x': positions[:, 0],
246
+ 'y': positions[:, 1],
247
+ 'vx': velocities[:, 0],
248
+ 'vy': velocities[:, 1],
249
+ 'speed': np.sqrt(velocities[:, 0]**2 + velocities[:, 1]**2)
250
+ }
251
+
252
+ def calculate_flight_time(self) -> float:
253
+ """Calculate approximate flight time"""
254
+ if not self.air_resistance:
255
+ # Original calculation without air resistance
256
+ h = self.launch_height
257
+ vy0 = self.initial_velocity_y
258
+ g = self.gravity
259
+
260
+ if g == 0:
261
+ if vy0 == 0:
262
+ return float('inf') if h >= 0 else 0
263
+ return h / vy0 if vy0 < 0 else float('inf')
264
+
265
+ discriminant = vy0**2 + 2*g*h
266
+ if discriminant < 0:
267
+ return 0
268
+
269
+ t1 = (vy0 + np.sqrt(discriminant)) / g
270
+ t2 = (vy0 - np.sqrt(discriminant)) / g
271
+
272
+ return max(t1, t2) if max(t1, t2) > 0 else 0
273
+ else:
274
+ # With air resistance, use numerical approach to find landing time
275
+ # This is an approximation - we'll simulate until we hit ground
276
+ max_time = 20.0 # Maximum simulation time
277
+ dt = 0.01
278
+
279
+ for t in np.arange(0, max_time, dt):
280
+ _, y = self.position(t)
281
+ if y <= 0 and t > 0:
282
+ return t
283
+
284
+ return max_time # Fallback
285
+
286
+ def calculate_range(self) -> float:
287
+ """Calculate horizontal range of projectile"""
288
+ flight_time = self.calculate_flight_time()
289
+ if flight_time <= 0:
290
+ return 0
291
+
292
+ # Get final position
293
+ final_x, _ = self.position(flight_time)
294
+ return final_x - self.launch_x
295
+
296
+ def get_launch_info(self) -> dict:
297
+ """Return comprehensive launch information"""
298
+ flight_time = self.calculate_flight_time()
299
+ info = {
300
+ 'launch_speed': self.launch_speed,
301
+ 'launch_angle': self.launch_angle,
302
+ 'launch_height': self.launch_height,
303
+ 'initial_velocity_x': self.initial_velocity_x,
304
+ 'initial_velocity_y': self.initial_velocity_y,
305
+ 'flight_time': flight_time,
306
+ 'range': self.calculate_range(),
307
+ 'max_height': self.calculate_max_height(),
308
+ 'air_resistance': self.air_resistance,
309
+ 'is_sphere': self.is_sphere,
310
+ 'effective_drag_coeff': self.effective_drag_coeff if self.air_resistance else 0
311
+ }
312
+
313
+ # Add sphere info if applicable
314
+ if self.is_sphere:
315
+ sphere_info = self.get_sphere_info()
316
+ info.update(sphere_info)
317
+
318
+ return info
319
+
320
+ def calculate_max_height(self) -> float:
321
+ """Calculate maximum height reached by projectile"""
322
+ if not self.air_resistance:
323
+ # Analytical solution for no air resistance
324
+ if self.gravity == 0:
325
+ return float('inf') if self.initial_velocity_y > 0 else self.launch_height
326
+
327
+ # Time to reach maximum height: when vy = 0
328
+ t_max = self.initial_velocity_y / self.gravity
329
+ if t_max <= 0: # Projectile going downward initially
330
+ return self.launch_height
331
+
332
+ _, max_y = self.position(t_max)
333
+ return max(max_y, self.launch_height)
334
+ else:
335
+ # Numerical approach for air resistance
336
+ flight_time = self.calculate_flight_time()
337
+ dt = 0.01
338
+ max_height = self.launch_height
339
+
340
+ for t in np.arange(0, flight_time + dt, dt):
341
+ _, y = self.position(t)
342
+ if y > max_height:
343
+ max_height = y
344
+
345
+ return max_height
346
+
347
+ class KinematicsVisualizer:
348
+ """Create visualizations for kinematic motion"""
349
+
350
+ @staticmethod
351
+ def plot_1d_motion(motion: Motion1D, duration: float, title: str = "1D Kinematic Motion"):
352
+ """Create comprehensive 1D motion plots"""
353
+ t, x, v, a = motion.time_arrays(duration)
354
+
355
+ fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12))
356
+ fig.suptitle(title, fontsize=16, fontweight='bold')
357
+
358
+ # Position vs Time
359
+ ax1.plot(t, x, 'b-', linewidth=2, label='Position')
360
+ ax1.set_ylabel('Position (m)')
361
+ ax1.grid(True, alpha=0.3)
362
+ ax1.legend()
363
+ ax1.set_title('Position vs Time')
364
+
365
+ # Velocity vs Time
366
+ ax2.plot(t, v, 'r-', linewidth=2, label='Velocity')
367
+ ax2.set_ylabel('Velocity (m/s)')
368
+ ax2.grid(True, alpha=0.3)
369
+ ax2.legend()
370
+ ax2.set_title('Velocity vs Time')
371
+
372
+ # Acceleration vs Time
373
+ ax3.plot(t, a, 'g-', linewidth=2, label='Acceleration')
374
+ ax3.set_xlabel('Time (s)')
375
+ ax3.set_ylabel('Acceleration (m/s²)')
376
+ ax3.grid(True, alpha=0.3)
377
+ ax3.legend()
378
+ ax3.set_title('Acceleration vs Time')
379
+
380
+ plt.tight_layout()
381
+ return fig
382
+
383
+ @staticmethod
384
+ def plot_2d_trajectory(motion: Motion2D, duration: float = None, title: str = "2D Projectile Motion"):
385
+ """Create 2D trajectory visualization with launch info"""
386
+ if duration is None:
387
+ duration = motion.calculate_flight_time()
388
+
389
+ data = motion.trajectory_data(duration)
390
+ info = motion.get_launch_info()
391
+
392
+ fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))
393
+
394
+ # Create detailed title with launch parameters
395
+ detailed_title = (f"{title}\n"
396
+ f"Launch: {info['launch_speed']:.1f} m/s at {info['launch_angle']:.1f}° "
397
+ f"from {info['launch_height']:.1f}m height")
398
+ fig.suptitle(detailed_title, fontsize=14, fontweight='bold')
399
+
400
+ # Trajectory plot with key points marked
401
+ ax1.plot(data['x'], data['y'], 'b-', linewidth=2, label='Trajectory')
402
+
403
+ # Mark launch point
404
+ ax1.plot(motion.launch_x, motion.launch_height, 'go', markersize=8, label='Launch')
405
+
406
+ # Mark landing point
407
+ if len(data['x']) > 0:
408
+ ax1.plot(data['x'][-1], data['y'][-1], 'ro', markersize=8, label='Landing')
409
+
410
+ # Mark maximum height
411
+ max_height_idx = np.argmax(data['y'])
412
+ if len(data['x']) > 0:
413
+ ax1.plot(data['x'][max_height_idx], data['y'][max_height_idx], 'mo', markersize=6, label='Max Height')
414
+
415
+ ax1.set_xlabel('Horizontal Position (m)')
416
+ ax1.set_ylabel('Vertical Position (m)')
417
+ ax1.grid(True, alpha=0.3)
418
+ ax1.legend()
419
+ ax1.set_title(f'Trajectory (Range: {info["range"]:.1f}m, Max Height: {info["max_height"]:.1f}m)')
420
+ ax1.set_ylim(bottom=0) # Ensure ground is at y=0
421
+
422
+ # Speed vs Time
423
+ ax2.plot(data['time'], data['speed'], 'r-', linewidth=2)
424
+ ax2.set_xlabel('Time (s)')
425
+ ax2.set_ylabel('Speed (m/s)')
426
+ ax2.grid(True, alpha=0.3)
427
+ ax2.set_title(f'Speed vs Time (Flight Time: {info["flight_time"]:.1f}s)')
428
+
429
+ # Velocity components
430
+ ax3.plot(data['time'], data['vx'], 'g-', linewidth=2, label=f'Vx (const: {info["initial_velocity_x"]:.1f})')
431
+ ax3.plot(data['time'], data['vy'], 'm-', linewidth=2, label=f'Vy (initial: {info["initial_velocity_y"]:.1f})')
432
+ ax3.axhline(y=0, color='k', linestyle='--', alpha=0.5)
433
+ ax3.set_xlabel('Time (s)')
434
+ ax3.set_ylabel('Velocity (m/s)')
435
+ ax3.grid(True, alpha=0.3)
436
+ ax3.legend()
437
+ ax3.set_title('Velocity Components vs Time')
438
+
439
+ # Position components
440
+ ax4.plot(data['time'], data['x'], 'c-', linewidth=2, label='X position')
441
+ ax4.plot(data['time'], data['y'], 'orange', linewidth=2, label='Y position')
442
+ ax4.axhline(y=0, color='k', linestyle='--', alpha=0.5, label='Ground level')
443
+ ax4.set_xlabel('Time (s)')
444
+ ax4.set_ylabel('Position (m)')
445
+ ax4.grid(True, alpha=0.3)
446
+ ax4.legend()
447
+ ax4.set_title('Position Components vs Time')
448
+
449
+ plt.tight_layout()
450
+ return fig
451
+
452
+ # Example usage and test cases
453
+ if __name__ == "__main__":
454
+ # Example 1: Constant acceleration (car accelerating)
455
+ car_motion = Motion1D(initial_position=0, initial_velocity=5, acceleration=2)
456
+
457
+ # Example 2: Free fall
458
+ free_fall = Motion1D(initial_position=100, initial_velocity=0, acceleration=-9.81)
459
+
460
+ # Example 3: Classic projectile motion (45-degree launch from ground)
461
+ projectile_45 = Motion2D(
462
+ launch_speed=25, # m/s
463
+ launch_angle=45, # degrees
464
+ launch_height=0, # meters (ground level)
465
+ launch_x=0
466
+ )
467
+
468
+ # Example 4: Projectile launched from height at 30 degrees
469
+ projectile_height = Motion2D(
470
+ launch_speed=20, # m/s
471
+ launch_angle=30, # degrees
472
+ launch_height=10, # meters
473
+ launch_x=0
474
+ )
475
+
476
+ # Example 5: Horizontal launch (like dropping a ball while moving)
477
+ horizontal_launch = Motion2D(
478
+ launch_speed=15, # m/s
479
+ launch_angle=0, # degrees (horizontal)
480
+ launch_height=20, # meters
481
+ launch_x=0
482
+ )
483
+
484
+ # Create visualizations
485
+ visualizer = KinematicsVisualizer()
486
+
487
+ # Plot 1D examples
488
+ visualizer.plot_1d_motion(car_motion, 10, "Car Acceleration")
489
+ visualizer.plot_1d_motion(free_fall, 4.5, "Free Fall Motion")
490
+
491
+ # Plot 2D examples with different launch conditions
492
+ visualizer.plot_2d_trajectory(projectile_45, title="45° Launch from Ground")
493
+ visualizer.plot_2d_trajectory(projectile_height, title="30° Launch from 10m Height")
494
+ visualizer.plot_2d_trajectory(horizontal_launch, title="Horizontal Launch from 20m")
495
+
496
+ # Print launch information for educational purposes
497
+ print("\n=== LAUNCH ANALYSIS ===")
498
+ for name, motion in [("45° Ground Launch", projectile_45),
499
+ ("30° Height Launch", projectile_height),
500
+ ("Horizontal Launch", horizontal_launch)]:
501
+ info = motion.get_launch_info()
502
+ print(f"\n{name}:")
503
+ print(f" Launch Speed: {info['launch_speed']:.1f} m/s at {info['launch_angle']:.1f}°")
504
+ print(f" Initial Velocity: ({info['initial_velocity_x']:.1f}, {info['initial_velocity_y']:.1f}) m/s")
505
+ print(f" Flight Time: {info['flight_time']:.2f} s")
506
+ print(f" Range: {info['range']:.1f} m")
507
+ print(f" Max Height: {info['max_height']:.1f} m")
508
+
509
+ plt.show()