Brute Force vs. Pythonic Python
Simulate projectile motion while mastering 7 Pythonic patterns.
Introduction
30 Second Summary
Python has dozens of elegant patterns that collapse 10 lines of code into one. But reading about them in a tutorial never makes them stick. You need a real problem where the verbose way hurts enough that the elegant way feels like a relief.
In this project, you will build a projectile motion analyzer that solves every physics computation twice: first the long, manual way with loops and counters, then the clean Pythonic way with comprehensions, zip(), and sorted() with lambdas.
What You'll Build
A single-file Python script that simulates projectile motion using real kinematics, solving every computation twice to demonstrate 7+ Pythonic patterns that each collapse 5-12 lines into 1-3.
By the end of this project, you'll have:
- A working projectile motion analyzer that computes trajectories, compares 15 launch angles, ranks them by range, and finds the optimal angle to 0.5-degree precision.
- Every computation refactored from brute-force loops into Pythonic one-liners, with both versions side by side so you can see exactly how and why each pattern works.
- A reference file demonstrating 7+ Pythonic patterns including list comprehensions, tuple unpacking, dict comprehensions, and generator expressions applied to real physics problems you can revisit anytime.
- Secret Mission: Add quadratic air resistance using Euler's method and discover why the optimal launch angle shifts below 45 degrees with drag.
Are there any prerequisites?
You should be comfortable with basic Python: loops, lists, tuples, functions, and imports. You do not need any physics background beyond what the project teaches you.
Set Up Physics Foundations
To build a projectile motion analyzer, you need a working Python environment and a set of physics formulas to compute trajectories.
In this step, you will create your project file, verify Python is ready, and implement the core kinematic equations that power every calculation in the project.
In this step, get ready to:
- Create your project file and verify Python is installed.
- Write three physics helper functions for flight time, max height, and range.
- Write both a brute-force and a Pythonic version of range computation and confirm they match.
Create the project file and verify Python
Every computation in this project lives in a single file. You will create it now and confirm Python can run it.
- In VS Code, open the integrated terminal by pressing Ctrl+` (macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create a project folder and move into it by running these commands:
mkdir projectile-motion
cd projectile-motion
- Create the project file by running:
touch projectile_analyzer.py
- Confirm the file exists by running:
ls
You should see projectile_analyzer.py in the output.
- Open the file in VS Code by running:
code projectile_analyzer.py
Now verify that Python 3.14 is available on your machine.
- Check your Python version by running:
python --version
✔️ I see version 3.14 or higher
Your terminal shows something like Python 3.14.6. You are ready to continue.
ⓧ I see an older version
Your system has a Python version below 3.14. You need to install Python 3.14 alongside it.
- Visit the official download page at https://www.python.org/downloads/.
- Download the Python 3.14 installer for your operating system (macOS installer or Windows installer).
- Run the installer and follow the prompts.
- On macOS, the new install may require you to run python3.14 --version to invoke the correct version, since the system default may still point to an older Python.
- On Windows, re-open your terminal and run python --version again to confirm it now shows 3.14.
ⓧ Command not found
Python is not installed on your machine yet.
- Visit the official download page at https://www.python.org/downloads/.
- Download the Python 3.14 installer for your operating system (macOS installer or Windows installer).
- Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal, then run python --version again to confirm it shows 3.14.
With Python confirmed, add the opening structure to your file.
- In projectile_analyzer.py, paste the following imports and constants:
"""
Projectile Motion Analyzer
Brute Force vs. Pythonic Python
"""
import math
# ============================================================
# CONSTANTS
# ============================================================
G = 9.81 # gravitational acceleration (m/s^2)
DT = 0.01 # time step for simulation (s)
What does this code do?
The import math line loads Python's built-in math library, which gives you access to trigonometric functions like sin, cos, and radians.
The two constants define the physics environment. G is gravitational acceleration (9.81 m/s squared on Earth). DT is the time step used later for trajectory simulations.
- Save projectile_analyzer.py.
Write the physics helper functions
Projectile motion has three key quantities: how long the projectile flies, how high it goes, and how far it lands. Each formula takes an initial velocity and a launch angle, then returns a single number.
- Below the constants section in projectile_analyzer.py, add these three functions:
# ============================================================
# STEP 1: PHYSICS WARM-UP FUNCTIONS
# ============================================================
def time_of_flight(v0, theta_deg):
"""Time of flight (s) - no air resistance."""
theta = math.radians(theta_deg)
return 2 * v0 * math.sin(theta) / G
def max_height(v0, theta_deg):
"""Maximum height (m) reached."""
theta = math.radians(theta_deg)
return (v0 * math.sin(theta)) ** 2 / (2 * G)
def horizontal_range(v0, theta_deg):
"""Horizontal range (m)."""
theta = math.radians(theta_deg)
return v0 ** 2 * math.sin(2 * theta) / G
What does this code do?
- Each function converts the angle from degrees to radians using math.radians(), because Python's trig functions expect radians.
- time_of_flight() calculates how long a projectile stays airborne using the formula 2v₀sin(θ)/g.
- max_height() calculates the peak altitude using (v₀sin(θ))² / (2g).
- horizontal_range() calculates landing distance using v₀²sin(2θ)/g.
- Save projectile_analyzer.py.
- Test the functions with a 45-degree launch at 50 m/s by running this in your terminal:
python -c "from projectile_analyzer import *; print(f'Time: {time_of_flight(50, 45):.2f} s'); print(f'Height: {max_height(50, 45):.2f} m'); print(f'Range: {horizontal_range(50, 45):.2f} m')"
You should see approximately: Time 7.21 s, Height 63.71 m, Range 254.84 m.
Seeing an import error?
- Make sure your terminal is in the same folder as projectile_analyzer.py. Run ls to confirm the file is listed.
- Check for typos in the function names or the import math line at the top of your file.
Help me debug my Python import error for projectile_analyzer.py
Write brute-force and Pythonic range computation
Here is the core pattern you will repeat throughout this project: solve a problem the verbose way first, then collapse it into a single Pythonic expression. The brute-force version uses a loop with .append(). The Pythonic version uses a list comprehension to achieve the same result in one line.
- Below the three helper functions in projectile_analyzer.py, add both range-computation functions:
# --- First taste: brute force vs Pythonic ---
def compute_ranges_brute(v0, angles):
"""Brute force: loop + append."""
ranges = []
for angle in angles:
r = horizontal_range(v0, angle)
ranges.append(r)
return ranges
def compute_ranges(v0, angles):
"""Pythonic: list comprehension."""
return [horizontal_range(v0, angle) for angle in angles]
What does this code do?
- compute_ranges_brute() uses the classic pattern: create an empty list, loop through each angle, compute the range, and append it. This takes 5 lines of logic.
- compute_ranges() does the exact same thing in a single return statement using a list comprehension. The expression [horizontal_range(v0, angle) for angle in angles] reads like English: give me the horizontal range for each angle in angles.
Both functions produce identical output. The list comprehension is not just shorter. It is also faster because Python optimizes comprehensions internally.
- Save projectile_analyzer.py.
- Verify both functions produce identical results by running:
python -c "from projectile_analyzer import *; angles = list(range(10, 81, 5)); print(f'Match: {compute_ranges_brute(50, angles) == compute_ranges(50, angles)}'); print(f'Angles tested: {len(angles)}')"
You should see Match: True and Angles tested: 15. Both functions computed the range for 15 different launch angles and the results are identical.
Seeing Match: False?
- Double-check that compute_ranges_brute() calls horizontal_range(v0, angle) with the same arguments as the comprehension in compute_ranges().
- Make sure you saved the file before running the terminal command. Unsaved changes are not picked up by python -c.
Help me figure out why my brute-force and Pythonic range functions return different results
✔️ Awesome, I've got everything!
Great. Double check you have saved projectile_analyzer.py and both test outputs matched the expected values.
ⓧ I'd like to double check the full code
Here is the complete projectile_analyzer.py file at this point:
"""
Projectile Motion Analyzer
Brute Force vs. Pythonic Python
"""
import math
# ============================================================
# CONSTANTS
# ============================================================
G = 9.81 # gravitational acceleration (m/s^2)
DT = 0.01 # time step for simulation (s)
# ============================================================
# STEP 1: PHYSICS WARM-UP FUNCTIONS
# ============================================================
def time_of_flight(v0, theta_deg):
"""Time of flight (s) - no air resistance."""
theta = math.radians(theta_deg)
return 2 * v0 * math.sin(theta) / G
def max_height(v0, theta_deg):
"""Maximum height (m) reached."""
theta = math.radians(theta_deg)
return (v0 * math.sin(theta)) ** 2 / (2 * G)
def horizontal_range(v0, theta_deg):
"""Horizontal range (m)."""
theta = math.radians(theta_deg)
return v0 ** 2 * math.sin(2 * theta) / G
# --- First taste: brute force vs Pythonic ---
def compute_ranges_brute(v0, angles):
"""Brute force: loop + append."""
ranges = []
for angle in angles:
r = horizontal_range(v0, angle)
ranges.append(r)
return ranges
def compute_ranges(v0, angles):
"""Pythonic: list comprehension."""
return [horizontal_range(v0, angle) for angle in angles]
Your physics foundations are locked in. Every formula checks out against the analytical values, and you have seen the first brute-force-to-Pythonic transformation in action. Next up, you will generate full trajectory data point by point.
Generate Trajectory Points
In Step 1, you computed single values like range and height for each angle. Now you need something richer: the full path a projectile traces through the air, captured as hundreds of coordinate points over time.
Generating a trajectory means stepping through time and recording position at each tick. You'll write this logic twice: first as a verbose while loop with manual index tracking, then as a single list comprehension that packs the entire computation into one return statement.
In this step, get ready to:
- Write a brute-force trajectory generator using a while loop and index counter.
- Refactor it into a single list comprehension with a nested generator expression.
- Use tuple unpacking to extract trajectory endpoints and verify both functions match.
Write the brute-force trajectory generator
A trajectory is a list of (time, x-position, y-position) tuples representing every point the projectile passes through. The brute-force approach uses a while loop that manually increments an index variable, computes coordinates at each time step, and appends valid points one by one.
- In projectile_analyzer.py, scroll to the bottom of the file (below the compute_ranges function).
- Add the Step 2 section header and the brute-force trajectory function by copying this code:
# ============================================================
# STEP 2: TRAJECTORY GENERATION
# ============================================================
def generate_trajectory_brute(v0, theta_deg):
"""Brute force: while loop with index counter."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
points = []
i = 0
while i < num_steps:
t = i * DT
x = vx * t
y = vy * t - 0.5 * G * t ** 2
if y >= 0:
points.append((t, x, y))
i += 1
return points
What does this code do?
This function simulates a projectile's flight by stepping through time in small increments (DT = 0.01 seconds).
- vx and vy decompose the launch velocity into horizontal and vertical components using trig.
- The while loop manually increments i each iteration, computing position at each time step using kinematic equations.
- The if y >= 0 check filters out points where the projectile has gone underground.
- Each valid point is packed as a tuple (t, x, y) and appended to the list.
You won't see a change yet. The next function rewrites this logic as a comprehension, and then you'll run both together.
Write the Pythonic trajectory generator
The Pythonic version collapses the entire while loop into a single return statement. It uses a generator expression inside the comprehension to produce time values on the fly, and an if clause to filter out below-ground points.
- Directly below generate_trajectory_brute, add the Pythonic version:
def generate_trajectory(v0, theta_deg):
"""Pythonic: list comprehension + tuple packing + generator for time."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
return [
(t, vx * t, vy * t - 0.5 * G * t ** 2)
for t in (i * DT for i in range(num_steps))
if vy * t - 0.5 * G * t ** 2 >= 0
]
What's happening here?
The entire function body is a single return statement. Three patterns working together make this possible:
- Tuple packing: (t, vx * t, vy * t - 0.5 * G * t ** 2) builds each point directly in the expression.
- Generator expression: (i * DT for i in range(num_steps)) produces time values lazily without building a separate list.
- Filter clause: if vy * t - 0.5 * G * t ** 2 >= 0 discards underground points without a separate if block.
This replaces the while loop, the index variable, the manual increment, and the conditional append. Eight lines of logic become one expression.
You won't see a change yet. The next chunk adds main() to call both functions and print the results.
Call both functions in main() and verify with tuple unpacking
Now you need a main() function that runs everything. It will print the Step 1 physics check, then call both trajectory functions, use tuple unpacking to extract the first and last points, and confirm the results match.
- Below generate_trajectory, add the main() function and the entry-point guard by copying this code:
# ============================================================
# MAIN
# ============================================================
def main():
v0 = 50 # m/s initial velocity
# --- Step 1: Warm-up ---
print("=" * 55)
print(" PROJECTILE MOTION ANALYZER")
print(" Brute Force vs. Pythonic Python")
print("=" * 55)
print(f"\n Launch speed: {v0} m/s | Gravity: {G} m/s^2")
print(f"\n Quick check at 45 degrees:")
print(f" Time of flight: {time_of_flight(v0, 45):.2f} s")
print(f" Max height: {max_height(v0, 45):.2f} m")
print(f" Range: {horizontal_range(v0, 45):.2f} m")
angles = list(range(10, 81, 5))
ranges_brute = compute_ranges_brute(v0, angles)
ranges_pythonic = compute_ranges(v0, angles)
print(f"\n Ranges match: {ranges_brute == ranges_pythonic}")
# --- Step 2: Trajectory ---
print(f"\n{'=' * 55}")
print(" TRAJECTORY GENERATION (45 deg)")
print("=" * 55)
traj_brute = generate_trajectory_brute(v0, 45)
traj = generate_trajectory(v0, 45)
print(f" Points (brute): {len(traj_brute)}")
print(f" Points (pythonic): {len(traj)}")
t0, x0, y0 = traj[0]
t_end, x_end, y_end = traj[-1]
print(f" Start: t={t0:.2f}s, x={x0:.1f}m, y={y0:.1f}m")
print(f" End: t={t_end:.2f}s, x={x_end:.1f}m, y={y_end:.1f}m")
if __name__ == "__main__":
main()
What does this code do?
The main() function orchestrates all the analysis. The Step 1 section prints the physics sanity check from the previous step and confirms the two range functions match.
The Step 2 section introduces tuple unpacking. The line t0, x0, y0 = traj[0] pulls the three values out of the first tuple and gives each a name in one shot. No index access, no temporary variables.
The if __name__ == "__main__": guard ensures main() only runs when you execute the file directly, not when importing it as a module.
- Save projectile_analyzer.py.
- Run the script by entering this command in your terminal:
python projectile_analyzer.py
- You should see the Step 1 physics check (range ~254.84m, height ~63.71m, time ~7.21s), followed by the trajectory section showing both functions producing the same number of points (around 720).
Seeing a NameError or SyntaxError?
- If you see NameError: name 'generate_trajectory_brute' is not defined, make sure the two trajectory functions are placed ABOVE main() in your file.
- If you see a SyntaxError in the list comprehension, check that the for and if lines inside the square brackets are indented consistently.
- If the point counts don't match between brute and pythonic, double-check that both functions use the same num_steps calculation: int(t_total / DT) + 1.
My trajectory script is throwing an error when I run it. Can you help me debug?
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete projectile_analyzer.py up to this point:
"""
Projectile Motion Analyzer
Brute Force vs. Pythonic Python
"""
import math
# ============================================================
# CONSTANTS
# ============================================================
G = 9.81 # gravitational acceleration (m/s^2)
DT = 0.01 # time step for simulation (s)
# ============================================================
# STEP 1: PHYSICS WARM-UP FUNCTIONS
# ============================================================
def time_of_flight(v0, theta_deg):
"""Time of flight (s) - no air resistance."""
theta = math.radians(theta_deg)
return 2 * v0 * math.sin(theta) / G
def max_height(v0, theta_deg):
"""Maximum height (m) reached."""
theta = math.radians(theta_deg)
return (v0 * math.sin(theta)) ** 2 / (2 * G)
def horizontal_range(v0, theta_deg):
"""Horizontal range (m)."""
theta = math.radians(theta_deg)
return v0 ** 2 * math.sin(2 * theta) / G
# --- First taste: brute force vs Pythonic ---
def compute_ranges_brute(v0, angles):
"""Brute force: loop + append."""
ranges = []
for angle in angles:
r = horizontal_range(v0, angle)
ranges.append(r)
return ranges
def compute_ranges(v0, angles):
"""Pythonic: list comprehension."""
return [horizontal_range(v0, angle) for angle in angles]
# ============================================================
# STEP 2: TRAJECTORY GENERATION
# ============================================================
def generate_trajectory_brute(v0, theta_deg):
"""Brute force: while loop with index counter."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
points = []
i = 0
while i < num_steps:
t = i * DT
x = vx * t
y = vy * t - 0.5 * G * t ** 2
if y >= 0:
points.append((t, x, y))
i += 1
return points
def generate_trajectory(v0, theta_deg):
"""Pythonic: list comprehension + tuple packing + generator for time."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
return [
(t, vx * t, vy * t - 0.5 * G * t ** 2)
for t in (i * DT for i in range(num_steps))
if vy * t - 0.5 * G * t ** 2 >= 0
]
# ============================================================
# MAIN
# ============================================================
def main():
v0 = 50 # m/s initial velocity
# --- Step 1: Warm-up ---
print("=" * 55)
print(" PROJECTILE MOTION ANALYZER")
print(" Brute Force vs. Pythonic Python")
print("=" * 55)
print(f"\n Launch speed: {v0} m/s | Gravity: {G} m/s^2")
print(f"\n Quick check at 45 degrees:")
print(f" Time of flight: {time_of_flight(v0, 45):.2f} s")
print(f" Max height: {max_height(v0, 45):.2f} m")
print(f" Range: {horizontal_range(v0, 45):.2f} m")
angles = list(range(10, 81, 5))
ranges_brute = compute_ranges_brute(v0, angles)
ranges_pythonic = compute_ranges(v0, angles)
print(f"\n Ranges match: {ranges_brute == ranges_pythonic}")
# --- Step 2: Trajectory ---
print(f"\n{'=' * 55}")
print(" TRAJECTORY GENERATION (45 deg)")
print("=" * 55)
traj_brute = generate_trajectory_brute(v0, 45)
traj = generate_trajectory(v0, 45)
print(f" Points (brute): {len(traj_brute)}")
print(f" Points (pythonic): {len(traj)}")
t0, x0, y0 = traj[0]
t_end, x_end, y_end = traj[-1]
print(f" Start: t={t0:.2f}s, x={x0:.1f}m, y={y0:.1f}m")
print(f" End: t={t_end:.2f}s, x={x_end:.1f}m, y={y_end:.1f}m")
if __name__ == "__main__":
main()
Why tuple unpacking matters
Without tuple unpacking, you would write traj[0][0], traj[0][1], traj[0][2] to access time, x, and y. With unpacking, t0, x0, y0 = traj[0] gives each value a meaningful name instantly.
This pattern shows up everywhere in Python when you work with structured data. You'll use it repeatedly throughout the rest of this project.
Both functions produce identical trajectory data, but the Pythonic version does it in a single expression. Next up, you'll apply dict comprehensions and enumerate() to compare multiple launches and display a formatted table.
Build Multi-Launch Comparison
You can now generate a full trajectory for a single launch. The next question is: how does the trajectory change across different launch angles?
In this step, you will build functions that compare 15 different launch angles side by side. You will construct a dictionary holding range, height, and flight time for each angle, then display it as a formatted table. The brute-force version uses a manual loop and counter variable. The Pythonic version uses a dict comprehension and enumerate() with tuple unpacking.
In this step, get ready to:
- Write brute-force and Pythonic functions that build a comparison dictionary for multiple launch angles.
- Write brute-force and Pythonic functions that display the comparison as a numbered table.
- Run the full comparison in main() and verify a 15-row output table.
Write the comparison functions
A single launch gives you one set of metrics. Comparing multiple angles requires storing each angle's results in a structured way. A dictionary maps each angle to its range, height, and flight time.
- In projectile_analyzer.py, find the comment that says # STEP 2: TRAJECTORY GENERATION and scroll below the generate_trajectory() function.
- Add the following section header and two comparison functions below it:
# ============================================================
# STEP 3: MULTI-LAUNCH COMPARISON
# ============================================================
def compare_launches_brute(v0, angles):
"""Brute force: manual loop building a dictionary."""
results = {}
for angle in angles:
r = horizontal_range(v0, angle)
h = max_height(v0, angle)
t = time_of_flight(v0, angle)
results[angle] = {"range": r, "height": h, "time": t}
return results
def compare_launches(v0, angles):
"""Pythonic: dict comprehension."""
return {
angle: {
"range": horizontal_range(v0, angle),
"height": max_height(v0, angle),
"time": time_of_flight(v0, angle),
}
for angle in angles
}
What does this code do?
Both functions produce the same dictionary structure: keys are angles, values are dicts with range, height, and time.
The brute-force version initializes an empty dict, loops over angles, computes each metric separately, and assigns the result. That takes 7 lines of logic.
The Pythonic version uses a dict comprehension. The key expression is angle, and the value expression nests the physics calls directly. Same output, one return statement.
You won't see a change yet. The next chunk adds display functions that print the results.
Write the display functions
A dictionary full of data is useless if you can't read it. Display functions turn the raw dict into a numbered table. The challenge is tracking the row number as you iterate.
- Directly below the compare_launches() function you just wrote, add these two display functions:
def display_comparison_brute(results):
"""Brute force: manual counter."""
counter = 1
for angle in results:
data = results[angle]
print(f" {counter}. {angle} deg: range={data['range']:.1f}m, height={data['height']:.1f}m")
counter += 1
def display_comparison(results):
"""Pythonic: enumerate + tuple unpacking."""
for i, (angle, data) in enumerate(results.items(), start=1):
print(f" {i}. {angle} deg: range={data['range']:.1f}m, height={data['height']:.1f}m")
What does this code do?
The brute-force version manually initializes counter = 1, increments it every iteration, and accesses the dict value by key. Three extra lines of bookkeeping.
The Pythonic version uses enumerate(results.items(), start=1). The enumerate() call provides the row number automatically. Tuple unpacking in for i, (angle, data) destructures each dict item into its key and value in one shot.
What is enumerate()?
Python's enumerate() wraps any iterable and yields (index, value) pairs. The start parameter controls where the count begins. It eliminates the need for a separate counter variable.
You won't see output yet. The next chunk wires these functions into main().
Run the comparison in main()
The comparison and display functions are defined. Now you need to call them from main() with the same list of angles you used in earlier steps.
- In your main() function, find the last line of the Step 2 trajectory section (the line that prints End: t=...).
- Add the following block directly below it:
# --- Step 3: Multi-launch ---
print(f"\n{'=' * 55}")
print(" MULTI-LAUNCH COMPARISON")
print("=" * 55)
comparison = compare_launches(v0, angles)
display_comparison(comparison)
What does this code do?
This calls the Pythonic compare_launches() to build the full comparison dictionary. The angles variable was already defined earlier in main as list(range(10, 81, 5)), giving 15 angles from 10 to 80 degrees.
Then display_comparison() prints each angle's range and height in a numbered row.
- Save projectile_analyzer.py.
- Run the script by running this command in your terminal:
python projectile_analyzer.py
- You should see a MULTI-LAUNCH COMPARISON section with 15 numbered rows. Each row shows an angle, its range, and its max height. The 45-degree row should show range of about 255m.
Not seeing the comparison table?
- Make sure you saved projectile_analyzer.py before running.
- Check that the new code inside main() is indented to match the existing lines (one level of indentation inside the function body).
- Verify that your compare_launches() and display_comparison() functions are defined ABOVE the main() function, not inside it.
Help me debug my multi-launch comparison output
✔️ Awesome, I've got everything!
Great. Double check you have saved your file.
ⓧ I'd like to double check the full code
Here is the complete projectile_analyzer.py up to this point:
"""
Projectile Motion Analyzer
Brute Force vs. Pythonic Python
"""
import math
# ============================================================
# CONSTANTS
# ============================================================
G = 9.81 # gravitational acceleration (m/s^2)
DT = 0.01 # time step for simulation (s)
# ============================================================
# STEP 1: PHYSICS WARM-UP FUNCTIONS
# ============================================================
def time_of_flight(v0, theta_deg):
"""Time of flight (s) - no air resistance."""
theta = math.radians(theta_deg)
return 2 * v0 * math.sin(theta) / G
def max_height(v0, theta_deg):
"""Maximum height (m) reached."""
theta = math.radians(theta_deg)
return (v0 * math.sin(theta)) ** 2 / (2 * G)
def horizontal_range(v0, theta_deg):
"""Horizontal range (m)."""
theta = math.radians(theta_deg)
return v0 ** 2 * math.sin(2 * theta) / G
# --- First taste: brute force vs Pythonic ---
def compute_ranges_brute(v0, angles):
"""Brute force: loop + append."""
ranges = []
for angle in angles:
r = horizontal_range(v0, angle)
ranges.append(r)
return ranges
def compute_ranges(v0, angles):
"""Pythonic: list comprehension."""
return [horizontal_range(v0, angle) for angle in angles]
# ============================================================
# STEP 2: TRAJECTORY GENERATION
# ============================================================
def generate_trajectory_brute(v0, theta_deg):
"""Brute force: while loop with index counter."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
points = []
i = 0
while i < num_steps:
t = i * DT
x = vx * t
y = vy * t - 0.5 * G * t ** 2
if y >= 0:
points.append((t, x, y))
i += 1
return points
def generate_trajectory(v0, theta_deg):
"""Pythonic: list comprehension + tuple packing + generator for time."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
return [
(t, vx * t, vy * t - 0.5 * G * t ** 2)
for t in (i * DT for i in range(num_steps))
if vy * t - 0.5 * G * t ** 2 >= 0
]
# ============================================================
# STEP 3: MULTI-LAUNCH COMPARISON
# ============================================================
def compare_launches_brute(v0, angles):
"""Brute force: manual loop building a dictionary."""
results = {}
for angle in angles:
r = horizontal_range(v0, angle)
h = max_height(v0, angle)
t = time_of_flight(v0, angle)
results[angle] = {"range": r, "height": h, "time": t}
return results
def compare_launches(v0, angles):
"""Pythonic: dict comprehension."""
return {
angle: {
"range": horizontal_range(v0, angle),
"height": max_height(v0, angle),
"time": time_of_flight(v0, angle),
}
for angle in angles
}
def display_comparison_brute(results):
"""Brute force: manual counter."""
counter = 1
for angle in results:
data = results[angle]
print(f" {counter}. {angle} deg: range={data['range']:.1f}m, height={data['height']:.1f}m")
counter += 1
def display_comparison(results):
"""Pythonic: enumerate + tuple unpacking."""
for i, (angle, data) in enumerate(results.items(), start=1):
print(f" {i}. {angle} deg: range={data['range']:.1f}m, height={data['height']:.1f}m")
# ============================================================
# MAIN
# ============================================================
def main():
v0 = 50 # m/s initial velocity
# --- Step 1: Warm-up ---
print("=" * 55)
print(" PROJECTILE MOTION ANALYZER")
print(" Brute Force vs. Pythonic Python")
print("=" * 55)
print(f"\n Launch speed: {v0} m/s | Gravity: {G} m/s^2")
print(f"\n Quick check at 45 degrees:")
print(f" Time of flight: {time_of_flight(v0, 45):.2f} s")
print(f" Max height: {max_height(v0, 45):.2f} m")
print(f" Range: {horizontal_range(v0, 45):.2f} m")
angles = list(range(10, 81, 5))
ranges_brute = compute_ranges_brute(v0, angles)
ranges_pythonic = compute_ranges(v0, angles)
print(f"\n Ranges match: {ranges_brute == ranges_pythonic}")
# --- Step 2: Trajectory ---
print(f"\n{'=' * 55}")
print(" TRAJECTORY GENERATION (45 deg)")
print("=" * 55)
traj_brute = generate_trajectory_brute(v0, 45)
traj = generate_trajectory(v0, 45)
print(f" Points (brute): {len(traj_brute)}")
print(f" Points (pythonic): {len(traj)}")
t0, x0, y0 = traj[0]
t_end, x_end, y_end = traj[-1]
print(f" Start: t={t0:.2f}s, x={x0:.1f}m, y={y0:.1f}m")
print(f" End: t={t_end:.2f}s, x={x_end:.1f}m, y={y_end:.1f}m")
# --- Step 3: Multi-launch ---
print(f"\n{'=' * 55}")
print(" MULTI-LAUNCH COMPARISON")
print("=" * 55)
comparison = compare_launches(v0, angles)
display_comparison(comparison)
if __name__ == "__main__":
main()
You now have a full comparison table for 15 launch angles. Next up, you will rank these angles by range and find the single optimal launch angle to half-degree precision.
Rank Launches and Find Optimal Angle
Your comparison table shows 15 angles with their ranges and heights. But which angle is actually the best? And what if you need them ranked from longest to shortest range?
In this step, you will write optimization and ranking functions. The brute-force versions manually track maximums and implement bubble sort. The Pythonic versions collapse all of that into max() with a key function and sorted() with a lambda. One line replaces seven.
In this step, get ready to:
- Write brute-force and Pythonic functions to find the single best launch angle.
- Write brute-force and Pythonic functions to rank all angles by range.
- Run the optimizer with fine-grained 0.5-degree steps and confirm the physics.
Find the optimal launch angle
Finding the "best" element in a collection is one of the most common programming tasks. The brute-force approach uses a tracking variable and a comparison loop. Python's max() with a key function does the same thing in a single expression.
- In projectile_analyzer.py, find the closing line of the display_comparison() function.
- Add a section comment and both optimization functions below it by copying this code:
# ============================================================
# STEP 4: RANK AND OPTIMIZE
# ============================================================
def find_best_angle_brute(v0, angles):
"""Brute force: manual max-finding."""
best_angle = angles[0]
best_range = horizontal_range(v0, angles[0])
for angle in angles[1:]:
r = horizontal_range(v0, angle)
if r > best_range:
best_range = r
best_angle = angle
return best_angle, best_range
def find_best_angle(v0, angles):
"""Pythonic: max() with key."""
best = max(angles, key=lambda a: horizontal_range(v0, a))
return best, horizontal_range(v0, best)
What does this code do?
The brute-force version initializes best_angle and best_range with the first element, then loops through the rest comparing each range. Seven lines of tracking logic.
The Pythonic version does the same thing with max(angles, key=lambda a: horizontal_range(v0, a)). The key parameter tells max() how to evaluate each element. Python handles the comparison loop internally.
You won't see output yet. The next chunk adds the ranking functions, and then you will wire everything into main().
Rank all angles by distance
Ranking takes the optimization idea further. Instead of finding just the best angle, you want all 15 angles sorted from longest range to shortest.
- Directly below the find_best_angle() function you just added, copy both ranking functions:
def rank_by_range_brute(v0, angles):
"""Brute force: build list, then bubble sort."""
pairs = []
for angle in angles:
pairs.append((angle, horizontal_range(v0, angle)))
for i in range(len(pairs)):
for j in range(i + 1, len(pairs)):
if pairs[j][1] > pairs[i][1]:
pairs[i], pairs[j] = pairs[j], pairs[i]
return pairs
def rank_by_range(v0, angles):
"""Pythonic: sorted() with lambda key."""
return sorted(
[(angle, horizontal_range(v0, angle)) for angle in angles],
key=lambda pair: pair[1],
reverse=True,
)
What does this code do?
The brute-force version builds a list of (angle, range) tuples, then implements bubble sort with nested loops. The swap uses Python's tuple-swap syntax: pairs[i], pairs[j] = pairs[j], pairs[i].
The Pythonic version uses sorted() with three arguments: a list comprehension that builds the pairs, a key lambda that extracts the range from each pair, and reverse=True for descending order. The entire bubble sort collapses into one expression.
You won't see output yet. The next chunk wires both pairs of functions into main() so you can verify the results.
Display rankings and the optimal angle in main()
Now you will call both the ranking and optimization functions, printing the top 5 angles and the fine-grained optimal angle.
- In main(), find the line that says display_comparison(comparison).
- Add the Step 4 section below it by copying this code:
# --- Step 4: Rank and optimize ---
print(f"\n{'=' * 55}")
print(" RANKING AND OPTIMIZATION")
print("=" * 55)
rankings = rank_by_range(v0, angles)
print(" Top 5 angles by range:")
for rank, (angle, distance) in enumerate(rankings[:5], start=1):
print(f" #{rank}: {angle} deg -> {distance:.2f} m")
fine_angles = [i * 0.5 for i in range(1, 180)]
best_angle, best_range = find_best_angle(v0, fine_angles)
print(f"\n Optimal angle (0.5 deg precision): {best_angle} deg")
print(f" Maximum range: {best_range:.2f} m")
What does this code do?
- rank_by_range(v0, angles) returns all 15 angles sorted by range (longest first). The for rank, (angle, distance) in enumerate(rankings[:5], start=1) loop uses tuple unpacking to display just the top 5.
- fine_angles creates 179 angles from 0.5 to 89.5 in half-degree steps. Passing this to find_best_angle() finds the optimal angle to 0.5-degree precision.
- Save projectile_analyzer.py.
- Run the script by entering this command in your terminal:
python projectile_analyzer.py
- Look for the new RANKING AND OPTIMIZATION section at the bottom of the output. You should see the top 5 angles listed (with 45 degrees at #1) and the optimal angle reported as 45.0 degrees.
Why is 45 degrees optimal?
In a vacuum (no air resistance), 45 degrees perfectly balances horizontal speed and hang time. The range formula v0^2 * sin(2*theta) / G is maximized when sin(2*theta) = 1, which happens at theta = 45 degrees.
The Secret Mission adds air resistance, where this neat symmetry breaks and the optimal angle shifts below 45 degrees.
Not seeing 45.0 as the optimal angle?
- Check that fine_angles uses range(1, 180) (not range(1, 90)). The multiplication by 0.5 generates values from 0.5 to 89.5.
- Make sure find_best_angle() uses horizontal_range in its lambda, not max_height.
- Verify the new code is inside the main() function (indented by 4 spaces), not at the module level.
Why isn't my optimal angle showing as 45.0 degrees?
✔️ Awesome, I've got everything!
Great. Double check you have saved your file.
ⓧ I'd like to double check the full code
"""
Projectile Motion Analyzer
Brute Force vs. Pythonic Python
"""
import math
# ============================================================
# CONSTANTS
# ============================================================
G = 9.81 # gravitational acceleration (m/s^2)
DT = 0.01 # time step for simulation (s)
# ============================================================
# STEP 1: PHYSICS WARM-UP FUNCTIONS
# ============================================================
def time_of_flight(v0, theta_deg):
"""Time of flight (s) - no air resistance."""
theta = math.radians(theta_deg)
return 2 * v0 * math.sin(theta) / G
def max_height(v0, theta_deg):
"""Maximum height (m) reached."""
theta = math.radians(theta_deg)
return (v0 * math.sin(theta)) ** 2 / (2 * G)
def horizontal_range(v0, theta_deg):
"""Horizontal range (m)."""
theta = math.radians(theta_deg)
return v0 ** 2 * math.sin(2 * theta) / G
# --- First taste: brute force vs Pythonic ---
def compute_ranges_brute(v0, angles):
"""Brute force: loop + append."""
ranges = []
for angle in angles:
r = horizontal_range(v0, angle)
ranges.append(r)
return ranges
def compute_ranges(v0, angles):
"""Pythonic: list comprehension."""
return [horizontal_range(v0, angle) for angle in angles]
# ============================================================
# STEP 2: TRAJECTORY GENERATION
# ============================================================
def generate_trajectory_brute(v0, theta_deg):
"""Brute force: while loop with index counter."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
points = []
i = 0
while i < num_steps:
t = i * DT
x = vx * t
y = vy * t - 0.5 * G * t ** 2
if y >= 0:
points.append((t, x, y))
i += 1
return points
def generate_trajectory(v0, theta_deg):
"""Pythonic: list comprehension + tuple packing + generator for time."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
return [
(t, vx * t, vy * t - 0.5 * G * t ** 2)
for t in (i * DT for i in range(num_steps))
if vy * t - 0.5 * G * t ** 2 >= 0
]
# ============================================================
# STEP 3: MULTI-LAUNCH COMPARISON
# ============================================================
def compare_launches_brute(v0, angles):
"""Brute force: manual loop building a dictionary."""
results = {}
for angle in angles:
r = horizontal_range(v0, angle)
h = max_height(v0, angle)
t = time_of_flight(v0, angle)
results[angle] = {"range": r, "height": h, "time": t}
return results
def compare_launches(v0, angles):
"""Pythonic: dict comprehension."""
return {
angle: {
"range": horizontal_range(v0, angle),
"height": max_height(v0, angle),
"time": time_of_flight(v0, angle),
}
for angle in angles
}
def display_comparison_brute(results):
"""Brute force: manual counter."""
counter = 1
for angle in results:
data = results[angle]
print(f" {counter}. {angle} deg: range={data['range']:.1f}m, height={data['height']:.1f}m")
counter += 1
def display_comparison(results):
"""Pythonic: enumerate + tuple unpacking."""
for i, (angle, data) in enumerate(results.items(), start=1):
print(f" {i}. {angle} deg: range={data['range']:.1f}m, height={data['height']:.1f}m")
# ============================================================
# STEP 4: RANK AND OPTIMIZE
# ============================================================
def find_best_angle_brute(v0, angles):
"""Brute force: manual max-finding."""
best_angle = angles[0]
best_range = horizontal_range(v0, angles[0])
for angle in angles[1:]:
r = horizontal_range(v0, angle)
if r > best_range:
best_range = r
best_angle = angle
return best_angle, best_range
def find_best_angle(v0, angles):
"""Pythonic: max() with key."""
best = max(angles, key=lambda a: horizontal_range(v0, a))
return best, horizontal_range(v0, best)
def rank_by_range_brute(v0, angles):
"""Brute force: build list, then bubble sort."""
pairs = []
for angle in angles:
pairs.append((angle, horizontal_range(v0, angle)))
for i in range(len(pairs)):
for j in range(i + 1, len(pairs)):
if pairs[j][1] > pairs[i][1]:
pairs[i], pairs[j] = pairs[j], pairs[i]
return pairs
def rank_by_range(v0, angles):
"""Pythonic: sorted() with lambda key."""
return sorted(
[(angle, horizontal_range(v0, angle)) for angle in angles],
key=lambda pair: pair[1],
reverse=True,
)
# ============================================================
# MAIN
# ============================================================
def main():
v0 = 50 # m/s initial velocity
# --- Step 1: Warm-up ---
print("=" * 55)
print(" PROJECTILE MOTION ANALYZER")
print(" Brute Force vs. Pythonic Python")
print("=" * 55)
print(f"\n Launch speed: {v0} m/s | Gravity: {G} m/s^2")
print(f"\n Quick check at 45 degrees:")
print(f" Time of flight: {time_of_flight(v0, 45):.2f} s")
print(f" Max height: {max_height(v0, 45):.2f} m")
print(f" Range: {horizontal_range(v0, 45):.2f} m")
angles = list(range(10, 81, 5))
ranges_brute = compute_ranges_brute(v0, angles)
ranges_pythonic = compute_ranges(v0, angles)
print(f"\n Ranges match: {ranges_brute == ranges_pythonic}")
# --- Step 2: Trajectory ---
print(f"\n{'=' * 55}")
print(" TRAJECTORY GENERATION (45 deg)")
print("=" * 55)
traj_brute = generate_trajectory_brute(v0, 45)
traj = generate_trajectory(v0, 45)
print(f" Points (brute): {len(traj_brute)}")
print(f" Points (pythonic): {len(traj)}")
t0, x0, y0 = traj[0]
t_end, x_end, y_end = traj[-1]
print(f" Start: t={t0:.2f}s, x={x0:.1f}m, y={y0:.1f}m")
print(f" End: t={t_end:.2f}s, x={x_end:.1f}m, y={y_end:.1f}m")
# --- Step 3: Multi-launch ---
print(f"\n{'=' * 55}")
print(" MULTI-LAUNCH COMPARISON")
print("=" * 55)
comparison = compare_launches(v0, angles)
display_comparison(comparison)
# --- Step 4: Rank and optimize ---
print(f"\n{'=' * 55}")
print(" RANKING AND OPTIMIZATION")
print("=" * 55)
rankings = rank_by_range(v0, angles)
print(" Top 5 angles by range:")
for rank, (angle, distance) in enumerate(rankings[:5], start=1):
print(f" #{rank}: {angle} deg -> {distance:.2f} m")
fine_angles = [i * 0.5 for i in range(1, 180)]
best_angle, best_range = find_best_angle(v0, fine_angles)
print(f"\n Optimal angle (0.5 deg precision): {best_angle} deg")
print(f" Maximum range: {best_range:.2f} m")
if __name__ == "__main__":
main()
You have ranking and optimization working. Next, you will tackle the most elegant pattern in the project: using zip() to pair consecutive trajectory points and derive velocities from position data.
Analyze Velocities and Compute Stats
Your optimizer found the perfect launch angle, and your ranking confirmed it. But so far, all our analysis has been about where the projectile lands.
In this step, you will derive velocity from raw position data and compute flight statistics. This is where the most elegant Pythonic pattern lives: zip() pairing consecutive trajectory points with nested tuple unpacking to name every coordinate in a single line.
In this step, get ready to:
- Compute velocities from trajectory points using index-based iteration, then refactor with zip() and nested tuple unpacking.
- Compute trajectory statistics using manual accumulators, then refactor with generator expressions and the underscore convention.
- Run the full analyzer and confirm minimum speed occurs at the projectile's apex.
Write the velocity computation functions
Velocity is the rate of change of position. Given two consecutive trajectory points, you can approximate velocity by dividing the change in position by the change in time.
- In projectile_analyzer.py, find the comment block that says # STEP 4: RANK AND OPTIMIZE and scroll past all the Step 4 functions.
- Below the rank_by_range() function, add the Step 5 section header and the brute-force velocity function by pasting this code:
# ============================================================
# STEP 5: VELOCITY ANALYSIS AND STATS
# ============================================================
def compute_velocities_brute(trajectory):
"""Brute force: index-based iteration."""
velocities = []
for i in range(1, len(trajectory)):
t_prev = trajectory[i - 1][0]
x_prev = trajectory[i - 1][1]
y_prev = trajectory[i - 1][2]
t_curr = trajectory[i][0]
x_curr = trajectory[i][1]
y_curr = trajectory[i][2]
dt = t_curr - t_prev
vx = (x_curr - x_prev) / dt
vy = (y_curr - y_prev) / dt
speed = math.sqrt(vx ** 2 + vy ** 2)
velocities.append((t_curr, speed))
return velocities
What does this code do?
This function computes speed at each time step by comparing consecutive trajectory points. It accesses tuple elements by index (trajectory[i - 1][0] for the previous time, trajectory[i][1] for the current x-position).
The index-based access works, but it is hard to read. You cannot tell at a glance that index 0 is time, 1 is x, and 2 is y. The Pythonic version fixes this with named unpacking.
- Directly below compute_velocities_brute(), add the Pythonic version that replaces index-based access with zip() and nested tuple unpacking:
def compute_velocities(trajectory):
"""Pythonic: zip consecutive pairs + nested tuple unpacking."""
return [
(t2, math.sqrt(((x2 - x1) / (t2 - t1)) ** 2 + ((y2 - y1) / (t2 - t1)) ** 2))
for (t1, x1, y1), (t2, x2, y2) in zip(trajectory[:-1], trajectory[1:])
]
How does zip() pair consecutive points?
zip(trajectory[:-1], trajectory[1:]) creates pairs of adjacent elements. trajectory[:-1] is everything except the last point. trajectory[1:] is everything except the first point. Zipping them together gives you (point0, point1), (point1, point2), (point2, point3), and so on.
The nested unpacking for (t1, x1, y1), (t2, x2, y2) names every coordinate from both points in a single line. No index variables, no temporary assignments. This is the most elegant pattern in the project: 15 lines of brute force collapsed into 4.
You will not see any output yet. These functions get called in the final substep when you extend main().
Write the trajectory statistics functions
Statistics like average height and peak height require iterating through all trajectory points. The brute-force way uses manual accumulators. The Pythonic way uses generator expressions that compute results without building intermediate lists.
- Below compute_velocities(), add the brute-force stats function:
def trajectory_stats_brute(trajectory):
"""Brute force: manual accumulators."""
total_y = 0
max_y = 0
count_above_50 = 0
for point in trajectory:
y = point[2]
total_y += y
if y > max_y:
max_y = y
if y > 50:
count_above_50 += 1
avg_y = total_y / len(trajectory)
time_above_50 = count_above_50 * DT
return avg_y, max_y, time_above_50
What does this code do?
This function tracks three accumulators manually: a running total for averaging, a max tracker, and a counter for points above 50 meters. It extracts the y-coordinate using point[2] (the same index-based access problem from the velocity function).
It returns a tuple of three values: average height, peak height, and time spent above 50m. This pattern of returning multiple values as a tuple is standard Python.
- Below trajectory_stats_brute(), add the Pythonic version using generator expressions:
def trajectory_stats(trajectory):
"""Pythonic: generator expressions with sum/max + tuple unpacking."""
avg_y = sum(y for _, _, y in trajectory) / len(trajectory)
peak = max(y for _, _, y in trajectory)
time_above_50 = sum(DT for _, _, y in trajectory if y > 50)
return avg_y, peak, time_above_50
What is the underscore convention?
In for _, _, y in trajectory, the underscores tell readers you intentionally discard those values. Each trajectory point is a (t, x, y) tuple, but for height statistics you only need y.
The generator expressions inside sum() and max() never build intermediate lists. They compute the result one element at a time, which is more memory-efficient than creating a full list just to sum it.
You still will not see any output. The next substep calls both velocity and stats functions inside main().
Extend main() with velocity analysis and stats output
Now you will wire everything together. The main function already has a traj variable holding the 45-degree trajectory from Step 2. You will pass it into your new functions and use min() and max() with lambda key functions to find extreme speeds.
- In main(), find the last two lines of the Step 4 section:
print(f"\n Optimal angle (0.5 deg precision): {best_angle} deg")
print(f" Maximum range: {best_range:.2f} m")
- Below those lines, add the Step 5 velocity analysis and stats section plus the mission-complete banner:
# --- Step 5: Velocity and stats ---
print(f"\n{'=' * 55}")
print(" VELOCITY ANALYSIS AND STATS")
print("=" * 55)
velocities = compute_velocities(traj)
min_time, min_speed = min(velocities, key=lambda v: v[1])
max_time, max_speed = max(velocities, key=lambda v: v[1])
print(f" Min speed: {min_speed:.2f} m/s at t={min_time:.2f}s (apex!)")
print(f" Max speed: {max_speed:.2f} m/s at t={max_time:.2f}s")
avg_y, peak, time_above_50 = trajectory_stats(traj)
print(f"\n Average height: {avg_y:.2f} m")
print(f" Peak height: {peak:.2f} m")
print(f" Time above 50m: {time_above_50:.2f} s")
print(f"\n{'=' * 55}")
print(" MISSION COMPLETE!")
print("=" * 55)
What does this code do?
- min(velocities, key=lambda v: v[1]) finds the tuple with the smallest speed. The key=lambda v: v[1] tells Python to compare by the second element (speed) rather than the first (time).
- min_time, min_speed = ... unpacks the winning tuple directly into named variables. No indexing needed.
- avg_y, peak, time_above_50 = trajectory_stats(traj) unpacks the three-value return tuple into descriptive names in a single line.
- Save projectile_analyzer.py.
- Run the script by executing this command in your terminal:
python projectile_analyzer.py
- In the VELOCITY ANALYSIS AND STATS section of the output, you should see minimum speed of approximately 35.36 m/s occurring near t=3.60s (the apex), and peak height matching the 63.71m from the Step 1 warm-up check.
Why does minimum speed happen at the apex?
At the apex, the vertical velocity component is zero. The only speed remaining is the horizontal component, which is constant in vacuum: v0 * cos(45) = 50 * 0.707 = 35.36 m/s. This confirms your numerical differentiation matches the analytical prediction.
Seeing an IndexError or empty output?
- Make sure your new functions are placed ABOVE the main() function definition. Python reads top to bottom, so functions must be defined before they are called.
- Check that the Step 5 code inside main() uses the variable traj (defined earlier in main from the Step 2 section), not trajectory.
- Confirm the indentation of the Step 5 block is at the same level as the Step 4 code inside main() (4 spaces in).
Why is my velocity analysis output wrong or missing?
✔️ Awesome, I've got everything!
Great. Double check you have saved your file and the output shows the MISSION COMPLETE banner at the bottom.
ⓧ I'd like to double check the full code
Here is the complete projectile_analyzer.py file after this step. Compare yours line by line:
"""
Projectile Motion Analyzer
Brute Force vs. Pythonic Python
"""
import math
# ============================================================
# CONSTANTS
# ============================================================
G = 9.81 # gravitational acceleration (m/s^2)
DT = 0.01 # time step for simulation (s)
# ============================================================
# STEP 1: PHYSICS WARM-UP FUNCTIONS
# ============================================================
def time_of_flight(v0, theta_deg):
"""Time of flight (s) - no air resistance."""
theta = math.radians(theta_deg)
return 2 * v0 * math.sin(theta) / G
def max_height(v0, theta_deg):
"""Maximum height (m) reached."""
theta = math.radians(theta_deg)
return (v0 * math.sin(theta)) ** 2 / (2 * G)
def horizontal_range(v0, theta_deg):
"""Horizontal range (m)."""
theta = math.radians(theta_deg)
return v0 ** 2 * math.sin(2 * theta) / G
# --- First taste: brute force vs Pythonic ---
def compute_ranges_brute(v0, angles):
"""Brute force: loop + append."""
ranges = []
for angle in angles:
r = horizontal_range(v0, angle)
ranges.append(r)
return ranges
def compute_ranges(v0, angles):
"""Pythonic: list comprehension."""
return [horizontal_range(v0, angle) for angle in angles]
# ============================================================
# STEP 2: TRAJECTORY GENERATION
# ============================================================
def generate_trajectory_brute(v0, theta_deg):
"""Brute force: while loop with index counter."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
points = []
i = 0
while i < num_steps:
t = i * DT
x = vx * t
y = vy * t - 0.5 * G * t ** 2
if y >= 0:
points.append((t, x, y))
i += 1
return points
def generate_trajectory(v0, theta_deg):
"""Pythonic: list comprehension + tuple packing + generator for time."""
theta = math.radians(theta_deg)
vx = v0 * math.cos(theta)
vy = v0 * math.sin(theta)
t_total = time_of_flight(v0, theta_deg)
num_steps = int(t_total / DT) + 1
return [
(t, vx * t, vy * t - 0.5 * G * t ** 2)
for t in (i * DT for i in range(num_steps))
if vy * t - 0.5 * G * t ** 2 >= 0
]
# ============================================================
# STEP 3: MULTI-LAUNCH COMPARISON
# ============================================================
def compare_launches_brute(v0, angles):
"""Brute force: manual loop building a dictionary."""
results = {}
for angle in angles:
r = horizontal_range(v0, angle)
h = max_height(v0, angle)
t = time_of_flight(v0, angle)
results[angle] = {"range": r, "height": h, "time": t}
return results
def compare_launches(v0, angles):
"""Pythonic: dict comprehension."""
return {
angle: {
"range": horizontal_range(v0, angle),
"height": max_height(v0, angle),
"time": time_of_flight(v0, angle),
}
for angle in angles
}
def display_comparison_brute(results):
"""Brute force: manual counter."""
counter = 1
for angle in results:
data = results[angle]
print(f" {counter}. {angle} deg: range={data['range']:.1f}m, height={data['height']:.1f}m")
counter += 1
def display_comparison(results):
"""Pythonic: enumerate + tuple unpacking."""
for i, (angle, data) in enumerate(results.items(), start=1):
print(f" {i}. {angle} deg: range={data['range']:.1f}m, height={data['height']:.1f}m")
# ============================================================
# STEP 4: RANK AND OPTIMIZE
# ============================================================
def find_best_angle_brute(v0, angles):
"""Brute force: manual max-finding."""
best_angle = angles[0]
best_range = horizontal_range(v0, angles[0])
for angle in angles[1:]:
r = horizontal_range(v0, angle)
if r > best_range:
best_range = r
best_angle = angle
return best_angle, best_range
def find_best_angle(v0, angles):
"""Pythonic: max() with key."""
best = max(angles, key=lambda a: horizontal_range(v0, a))
return best, horizontal_range(v0, best)
def rank_by_range_brute(v0, angles):
"""Brute force: build list, then bubble sort."""
pairs = []
for angle in angles:
pairs.append((angle, horizontal_range(v0, angle)))
for i in range(len(pairs)):
for j in range(i + 1, len(pairs)):
if pairs[j][1] > pairs[i][1]:
pairs[i], pairs[j] = pairs[j], pairs[i]
return pairs
def rank_by_range(v0, angles):
"""Pythonic: sorted() with lambda key."""
return sorted(
[(angle, horizontal_range(v0, angle)) for angle in angles],
key=lambda pair: pair[1],
reverse=True,
)
# ============================================================
# STEP 5: VELOCITY ANALYSIS AND STATS
# ============================================================
def compute_velocities_brute(trajectory):
"""Brute force: index-based iteration."""
velocities = []
for i in range(1, len(trajectory)):
t_prev = trajectory[i - 1][0]
x_prev = trajectory[i - 1][1]
y_prev = trajectory[i - 1][2]
t_curr = trajectory[i][0]
x_curr = trajectory[i][1]
y_curr = trajectory[i][2]
dt = t_curr - t_prev
vx = (x_curr - x_prev) / dt
vy = (y_curr - y_prev) / dt
speed = math.sqrt(vx ** 2 + vy ** 2)
velocities.append((t_curr, speed))
return velocities
def compute_velocities(trajectory):
"""Pythonic: zip consecutive pairs + nested tuple unpacking."""
return [
(t2, math.sqrt(((x2 - x1) / (t2 - t1)) ** 2 + ((y2 - y1) / (t2 - t1)) ** 2))
for (t1, x1, y1), (t2, x2, y2) in zip(trajectory[:-1], trajectory[1:])
]
def trajectory_stats_brute(trajectory):
"""Brute force: manual accumulators."""
total_y = 0
max_y = 0
count_above_50 = 0
for point in trajectory:
y = point[2]
total_y += y
if y > max_y:
max_y = y
if y > 50:
count_above_50 += 1
avg_y = total_y / len(trajectory)
time_above_50 = count_above_50 * DT
return avg_y, max_y, time_above_50
def trajectory_stats(trajectory):
"""Pythonic: generator expressions with sum/max + tuple unpacking."""
avg_y = sum(y for _, _, y in trajectory) / len(trajectory)
peak = max(y for _, _, y in trajectory)
time_above_50 = sum(DT for _, _, y in trajectory if y > 50)
return avg_y, peak, time_above_50
# ============================================================
# MAIN
# ============================================================
def main():
v0 = 50 # m/s initial velocity
# --- Step 1: Warm-up ---
print("=" * 55)
print(" PROJECTILE MOTION ANALYZER")
print(" Brute Force vs. Pythonic Python")
print("=" * 55)
print(f"\n Launch speed: {v0} m/s | Gravity: {G} m/s^2")
print(f"\n Quick check at 45 degrees:")
print(f" Time of flight: {time_of_flight(v0, 45):.2f} s")
print(f" Max height: {max_height(v0, 45):.2f} m")
print(f" Range: {horizontal_range(v0, 45):.2f} m")
angles = list(range(10, 81, 5))
ranges_brute = compute_ranges_brute(v0, angles)
ranges_pythonic = compute_ranges(v0, angles)
print(f"\n Ranges match: {ranges_brute == ranges_pythonic}")
# --- Step 2: Trajectory ---
print(f"\n{'=' * 55}")
print(" TRAJECTORY GENERATION (45 deg)")
print("=" * 55)
traj_brute = generate_trajectory_brute(v0, 45)
traj = generate_trajectory(v0, 45)
print(f" Points (brute): {len(traj_brute)}")
print(f" Points (pythonic): {len(traj)}")
t0, x0, y0 = traj[0]
t_end, x_end, y_end = traj[-1]
print(f" Start: t={t0:.2f}s, x={x0:.1f}m, y={y0:.1f}m")
print(f" End: t={t_end:.2f}s, x={x_end:.1f}m, y={y_end:.1f}m")
# --- Step 3: Multi-launch ---
print(f"\n{'=' * 55}")
print(" MULTI-LAUNCH COMPARISON")
print("=" * 55)
comparison = compare_launches(v0, angles)
display_comparison(comparison)
# --- Step 4: Rank and optimize ---
print(f"\n{'=' * 55}")
print(" RANKING AND OPTIMIZATION")
print("=" * 55)
rankings = rank_by_range(v0, angles)
print(" Top 5 angles by range:")
for rank, (angle, distance) in enumerate(rankings[:5], start=1):
print(f" #{rank}: {angle} deg -> {distance:.2f} m")
fine_angles = [i * 0.5 for i in range(1, 180)]
best_angle, best_range = find_best_angle(v0, fine_angles)
print(f"\n Optimal angle (0.5 deg precision): {best_angle} deg")
print(f" Maximum range: {best_range:.2f} m")
# --- Step 5: Velocity and stats ---
print(f"\n{'=' * 55}")
print(" VELOCITY ANALYSIS AND STATS")
print("=" * 55)
velocities = compute_velocities(traj)
min_time, min_speed = min(velocities, key=lambda v: v[1])
max_time, max_speed = max(velocities, key=lambda v: v[1])
print(f" Min speed: {min_speed:.2f} m/s at t={min_time:.2f}s (apex!)")
print(f" Max speed: {max_speed:.2f} m/s at t={max_time:.2f}s")
avg_y, peak, time_above_50 = trajectory_stats(traj)
print(f"\n Average height: {avg_y:.2f} m")
print(f" Peak height: {peak:.2f} m")
print(f" Time above 50m: {time_above_50:.2f} s")
print(f"\n{'=' * 55}")
print(" MISSION COMPLETE!")
print("=" * 55)
if __name__ == "__main__":
main()
Secret mission
Add Air Resistance
Your analyzer assumes a perfect vacuum. Real projectiles fight air resistance every millisecond of flight. Add quadratic drag using Euler's method and discover why the textbook answer of 45 degrees is wrong in the real world.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Python's standard library. There are no cloud resources, paid APIs, or ongoing costs to worry about.
Resources you used:
- projectile_analyzer.py file (local Python script on your machine).
Keep everything running
No action needed. Choose this if you want to keep experimenting with new angles, adding more physics models, or referencing the Pythonic patterns.
- Your projectile_analyzer.py file stays on your machine, ready to run anytime with python projectile_analyzer.py.
- No background processes are running. The script only executes when you run it manually.
Pause - I'll come back to this later
Since this is a single local file with no running processes, there is nothing to pause. Your file will be waiting for you whenever you return.
- No services to stop or containers to shut down.
- The file uses zero resources when you are not actively running it.
Delete - I don't want to use this again
Remove the project file entirely to start fresh.
- Delete projectile_analyzer.py by running this command in the terminal from the folder where the file lives:
macOS
rm projectile_analyzer.py
Windows
del projectile_analyzer.py
- Confirm the file is gone by running ls (macOS) or dir (Windows). You should no longer see projectile_analyzer.py in the output.
Nice Work!
Nice Work!
You just built a complete projectile motion analyzer from scratch using nothing but Python's standard library. Every computation runs twice: once the verbose way, once the Pythonic way.
You've learned how to:
- Replace verbose loops with list comprehensions, dict comprehensions, and generator expressions to collapse 5-12 lines of code into 1-3 expressive lines that solve real physics problems.
- Use zip() with nested tuple unpacking to pair consecutive trajectory points and derive velocities from raw position data.
- Apply sorted(), max(), and enumerate() with lambda key functions to rank, optimize, and display data without manual tracking variables.
- Secret Mission: Implemented numerical integration with Euler's method and discovered how air resistance shifts the optimal launch angle below 45 degrees.
Ready to quiz yourself?