Provided modular architecture for animated bouncing ball analysis
- Organized project into src directory with subpackages (analysis, data, visualization, utils) - Added comprehensive README with project overview and structure - Implemented data loading, bounce detection, and visualization modules - Created example scripts and Jupyter notebook for project usage - Added requirements.txt for dependency management - Included output files for different ball types (golf, lacrosse, metal)
1
src/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Main package initialization
|
||||
1
src/analysis/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Analysis package initialization
|
||||
260
src/analysis/bounce_detection.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
Bounce detection module for the bouncing ball analysis project.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from scipy.signal import find_peaks
|
||||
import pandas as pd
|
||||
|
||||
# Default parameters for bounce detection
|
||||
DEFAULT_PROMINENCE = 0.1
|
||||
DEFAULT_MIN_DISTANCE = 10
|
||||
DEFAULT_MIN_TIME_DIFF = 0.2
|
||||
DEFAULT_MAX_BOUNCES = 7
|
||||
|
||||
# Ball-specific parameters
|
||||
BALL_PARAMS = {
|
||||
'Golf': {
|
||||
'relative_prominence': 0.15,
|
||||
'low_threshold': 12.5,
|
||||
'low_adjustment': 1.1,
|
||||
'high_threshold': 14.5,
|
||||
'high_adjustment': 1.3
|
||||
},
|
||||
'Lacrosse': {
|
||||
'relative_prominence': 0.05,
|
||||
'low_threshold': 18.5,
|
||||
'low_adjustment': 1.1,
|
||||
'high_threshold': 21.5,
|
||||
'high_adjustment': 1.3
|
||||
},
|
||||
'Metal': {
|
||||
'relative_prominence': 0.05,
|
||||
'low_threshold': 5,
|
||||
'low_adjustment': 0.8,
|
||||
'high_threshold': 8,
|
||||
'high_adjustment': 1.2
|
||||
}
|
||||
}
|
||||
|
||||
def detect_bounces(df,
|
||||
prominence=DEFAULT_PROMINENCE,
|
||||
min_distance=DEFAULT_MIN_DISTANCE,
|
||||
min_time_diff=DEFAULT_MIN_TIME_DIFF,
|
||||
max_bounces=DEFAULT_MAX_BOUNCES,
|
||||
fs=None):
|
||||
"""
|
||||
Detects bounce events by locating peaks in the signal.
|
||||
|
||||
Parameters:
|
||||
- df: DataFrame with 'Time' and 'Position'
|
||||
- prominence: required prominence for peaks (in the signal)
|
||||
- min_distance: minimum number of data points between peaks
|
||||
- min_time_diff: minimum time difference (seconds) between bounces
|
||||
- max_bounces: maximum bounces to report
|
||||
- fs: override sampling frequency (Hz). If None, computed from 'Time'.
|
||||
|
||||
Returns:
|
||||
- peak_indices (array of indices),
|
||||
- bounce_heights (array of position values at these bounces),
|
||||
- bounce_times (array of times of these bounces),
|
||||
- signal_data (the data used for detection, here the raw Position values)
|
||||
"""
|
||||
if fs is None:
|
||||
# Filter out duplicate time values and sort
|
||||
unique_times = np.unique(df['Time'].values)
|
||||
if len(unique_times) > 1:
|
||||
dt = np.median(np.diff(unique_times))
|
||||
if dt <= 0:
|
||||
# Fallback to a default sampling rate if time differences are non-positive
|
||||
fs = 1000 # 1000 Hz is a reasonable default for high-speed data
|
||||
else:
|
||||
fs = 1 / dt
|
||||
else:
|
||||
# If there's only one unique time value, use a default sampling rate
|
||||
fs = 1000
|
||||
|
||||
signal_data = df['Position'].values
|
||||
|
||||
peaks, _ = find_peaks(signal_data, prominence=prominence, distance=min_distance)
|
||||
|
||||
filtered_peaks = []
|
||||
filtered_times = []
|
||||
group_start_time = None
|
||||
group_best_idx = None
|
||||
group_best_value = None
|
||||
|
||||
for idx in peaks:
|
||||
current_time = df['Time'].iloc[idx]
|
||||
current_value = signal_data[idx]
|
||||
|
||||
if group_start_time is None:
|
||||
group_start_time = current_time
|
||||
group_best_idx = idx
|
||||
group_best_value = current_value
|
||||
continue
|
||||
|
||||
if (current_time - group_start_time) < min_time_diff:
|
||||
if current_value > group_best_value:
|
||||
group_best_idx = idx
|
||||
group_best_value = current_value
|
||||
else:
|
||||
filtered_peaks.append(group_best_idx)
|
||||
filtered_times.append(df['Time'].iloc[group_best_idx])
|
||||
|
||||
group_start_time = current_time
|
||||
group_best_idx = idx
|
||||
group_best_value = current_value
|
||||
|
||||
if group_best_idx is not None:
|
||||
filtered_peaks.append(group_best_idx)
|
||||
filtered_times.append(df['Time'].iloc[group_best_idx])
|
||||
|
||||
filtered_peaks = filtered_peaks[:max_bounces]
|
||||
filtered_times = filtered_times[:max_bounces]
|
||||
|
||||
bounce_heights = df['Position'].iloc[filtered_peaks].values
|
||||
bounce_times = np.array(filtered_times)
|
||||
|
||||
return np.array(filtered_peaks), bounce_heights, bounce_times, signal_data
|
||||
|
||||
|
||||
def compute_cor(bounce_heights):
|
||||
"""
|
||||
Computes the Coefficient of Restitution (COR) for consecutive bounces:
|
||||
e = sqrt( h_{n+1} / h_n )
|
||||
|
||||
Parameters:
|
||||
bounce_heights (numpy.ndarray): Array of bounce heights
|
||||
|
||||
Returns:
|
||||
numpy.ndarray: Array of COR values
|
||||
"""
|
||||
cor_values = []
|
||||
for i in range(len(bounce_heights) - 1):
|
||||
if bounce_heights[i] > 0:
|
||||
cor_values.append(np.sqrt(bounce_heights[i+1] / bounce_heights[i]))
|
||||
else:
|
||||
cor_values.append(np.nan)
|
||||
return np.array(cor_values)
|
||||
|
||||
|
||||
def process_trial(df,
|
||||
initial_height=None,
|
||||
ball_type=None,
|
||||
prominence=None,
|
||||
min_distance=DEFAULT_MIN_DISTANCE,
|
||||
min_time_diff=DEFAULT_MIN_TIME_DIFF,
|
||||
max_bounces=DEFAULT_MAX_BOUNCES,
|
||||
fs=None):
|
||||
"""
|
||||
Processes a trial by detecting bounces and computing COR values.
|
||||
|
||||
Parameters:
|
||||
df (pandas.DataFrame): DataFrame with 'Time' and 'Position' columns
|
||||
initial_height (float): Initial height of the ball
|
||||
ball_type (str): Type of ball ('Golf', 'Lacrosse', or 'Metal')
|
||||
prominence (float): Prominence for peak detection (if None, calculated from initial_height and ball_type)
|
||||
min_distance (int): Minimum distance between peaks
|
||||
min_time_diff (float): Minimum time difference between bounces
|
||||
max_bounces (int): Maximum number of bounces to detect
|
||||
fs (float): Sampling frequency (Hz)
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing analysis results
|
||||
"""
|
||||
# Calculate prominence if not provided
|
||||
if prominence is None and initial_height is not None and ball_type is not None:
|
||||
prominence = calculate_effective_prominence(initial_height, ball_type)
|
||||
elif prominence is None:
|
||||
prominence = DEFAULT_PROMINENCE
|
||||
|
||||
# Detect bounces
|
||||
peak_indices, bounce_heights, bounce_times, signal_data = detect_bounces(
|
||||
df, prominence, min_distance, min_time_diff, max_bounces, fs
|
||||
)
|
||||
|
||||
# Calculate COR values
|
||||
cor_values = compute_cor(bounce_heights)
|
||||
avg_cor = np.mean(cor_values) if cor_values.size > 0 else np.nan
|
||||
|
||||
# Return results as a dictionary
|
||||
return {
|
||||
'peak_indices': peak_indices,
|
||||
'bounce_heights': bounce_heights,
|
||||
'bounce_times': bounce_times,
|
||||
'cor_values': cor_values,
|
||||
'Average COR': avg_cor,
|
||||
'signal_data': signal_data,
|
||||
'Initial Height': initial_height,
|
||||
'Num Bounces': len(peak_indices)
|
||||
}
|
||||
|
||||
|
||||
def calculate_effective_prominence(initial_height, ball_type):
|
||||
"""
|
||||
Calculate the effective prominence for bounce detection based on initial height and ball type.
|
||||
|
||||
Parameters:
|
||||
initial_height (float): Initial height of the ball
|
||||
ball_type (str): Type of ball ('Golf', 'Lacrosse', or 'Metal')
|
||||
|
||||
Returns:
|
||||
float: Effective prominence value
|
||||
"""
|
||||
if ball_type not in BALL_PARAMS:
|
||||
raise ValueError(f"Unknown ball type: {ball_type}. Must be one of: {list(BALL_PARAMS.keys())}")
|
||||
|
||||
params = BALL_PARAMS[ball_type]
|
||||
|
||||
# Calculate base prominence
|
||||
effective_prominence = params['relative_prominence'] * initial_height
|
||||
|
||||
# Apply adjustments based on height
|
||||
if initial_height < params['low_threshold']:
|
||||
effective_prominence *= params['low_adjustment']
|
||||
elif initial_height > params['high_threshold']:
|
||||
effective_prominence *= params['high_adjustment']
|
||||
|
||||
return effective_prominence
|
||||
|
||||
|
||||
def process_trials(file_paths, ball_type, min_distance=DEFAULT_MIN_DISTANCE,
|
||||
min_time_diff=DEFAULT_MIN_TIME_DIFF, max_bounces=DEFAULT_MAX_BOUNCES, fs=None):
|
||||
"""
|
||||
Process multiple trials and return a summary DataFrame.
|
||||
|
||||
Parameters:
|
||||
file_paths (dict): Dictionary mapping labels to file paths
|
||||
ball_type (str): Type of ball ('Golf', 'Lacrosse', or 'Metal')
|
||||
min_distance (int): Minimum distance between peaks
|
||||
min_time_diff (float): Minimum time difference between bounces
|
||||
max_bounces (int): Maximum number of bounces to detect
|
||||
fs (float): Sampling frequency (Hz)
|
||||
|
||||
Returns:
|
||||
pandas.DataFrame: Summary DataFrame with trial information
|
||||
"""
|
||||
from src.data.loader import load_trial
|
||||
|
||||
results = []
|
||||
|
||||
for init_label, path in file_paths.items():
|
||||
try:
|
||||
initial_height = float(init_label.split()[0])
|
||||
except ValueError:
|
||||
initial_height = np.nan
|
||||
|
||||
# Compute effective prominence
|
||||
effective_prominence = calculate_effective_prominence(initial_height, ball_type)
|
||||
|
||||
# Load and process the trial
|
||||
df = load_trial(path)
|
||||
results.append(process_trial(
|
||||
df, initial_height, ball_type, effective_prominence, min_distance, min_time_diff, max_bounces, fs
|
||||
))
|
||||
|
||||
summary_df = pd.DataFrame(results)
|
||||
summary_df.sort_values(by='Initial Height', inplace=True)
|
||||
|
||||
return summary_df
|
||||
1
src/data/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Data package initialization
|
||||
40000
src/data/golf/golf_11.csv
Normal file
40000
src/data/golf/golf_12.csv
Normal file
40000
src/data/golf/golf_13.csv
Normal file
40000
src/data/golf/golf_14.csv
Normal file
40000
src/data/golf/golf_15.csv
Normal file
BIN
src/data/images/1.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
src/data/images/2.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src/data/images/3.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
src/data/images/golf_11.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
src/data/images/golf_12.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/data/images/golf_13.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
src/data/images/golf_14.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/data/images/golf_15.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src/data/images/l_18.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
src/data/images/l_19.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/data/images/l_20.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/data/images/l_21.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/data/images/l_22.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
src/data/images/metal_10.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/data/images/metal_2.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/data/images/metal_4.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/data/images/metal_6.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/data/images/metal_8.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
50000
src/data/lacrosse/l_18.csv
Normal file
50000
src/data/lacrosse/l_19.csv
Normal file
40000
src/data/lacrosse/l_20.csv
Normal file
40000
src/data/lacrosse/l_21.csv
Normal file
50000
src/data/lacrosse/l_22.csv
Normal file
82
src/data/loader.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Data loader module for the bouncing ball analysis project.
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
# File paths for golf ball data
|
||||
GOLF_BALL_PATHS = {
|
||||
"11 inches": "src/data/golf/golf_11.csv",
|
||||
"12 inches": "src/data/golf/golf_12.csv",
|
||||
"13 inches": "src/data/golf/golf_13.csv",
|
||||
"14 inches": "src/data/golf/golf_14.csv",
|
||||
"15 inches": "src/data/golf/golf_15.csv"
|
||||
}
|
||||
|
||||
# File paths for lacrosse ball data
|
||||
LACROSSE_BALL_PATHS = {
|
||||
"18 inches": "src/data/lacrosse/l_18.csv",
|
||||
"19 inches": "src/data/lacrosse/l_19.csv",
|
||||
"20 inches": "src/data/lacrosse/l_20.csv",
|
||||
"21 inches": "src/data/lacrosse/l_21.csv",
|
||||
"22 inches": "src/data/lacrosse/l_22.csv"
|
||||
}
|
||||
|
||||
# File paths for metal ball data
|
||||
METAL_BALL_PATHS = {
|
||||
"2 inches": "src/data/metal/metal_2.csv",
|
||||
"4 inches": "src/data/metal/metal_4.csv",
|
||||
"6 inches": "src/data/metal/metal_6.csv",
|
||||
"8 inches": "src/data/metal/metal_8.csv",
|
||||
"10 inches": "src/data/metal/metal_10.csv"
|
||||
}
|
||||
|
||||
def read_trial(path):
|
||||
"""
|
||||
Reads a CSV file containing time and position data (with no header)
|
||||
and returns a DataFrame with columns 'Time' and 'Position'.
|
||||
|
||||
Parameters:
|
||||
path (str): Path to the CSV file
|
||||
|
||||
Returns:
|
||||
pandas.DataFrame: DataFrame with 'Time' and 'Position' columns
|
||||
"""
|
||||
return pd.read_csv(path, header=None, names=['Time', 'Position'])
|
||||
|
||||
def get_data_path(file_name):
|
||||
"""
|
||||
Get the absolute path to a data file.
|
||||
|
||||
Parameters:
|
||||
file_name (str): Name of the data file
|
||||
|
||||
Returns:
|
||||
str: Absolute path to the data file
|
||||
"""
|
||||
# Check if the file exists as provided
|
||||
if os.path.exists(file_name):
|
||||
return file_name
|
||||
|
||||
# Check if the file exists in the data directory
|
||||
data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'data')
|
||||
data_path = os.path.join(data_dir, file_name)
|
||||
if os.path.exists(data_path):
|
||||
return data_path
|
||||
|
||||
# If not found, return the original path and let the caller handle the error
|
||||
return file_name
|
||||
|
||||
def load_trial(file_name):
|
||||
"""
|
||||
Load a trial from a CSV file.
|
||||
|
||||
Parameters:
|
||||
file_name (str): Name of the CSV file
|
||||
|
||||
Returns:
|
||||
pandas.DataFrame: DataFrame with 'Time' and 'Position' columns
|
||||
"""
|
||||
# The file_name is now a full path, so we can use it directly
|
||||
return read_trial(file_name)
|
||||
50000
src/data/metal/metal_10.csv
Normal file
50000
src/data/metal/metal_2.csv
Normal file
50000
src/data/metal/metal_4.csv
Normal file
50000
src/data/metal/metal_6.csv
Normal file
50000
src/data/metal/metal_8.csv
Normal file
361
src/examples/bouncing_ball_analysis.ipynb
Normal file
@@ -0,0 +1,361 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Bouncing Ball Analysis Example\n",
|
||||
"\n",
|
||||
"This notebook demonstrates how to use the modular structure of the bouncing ball analysis project to:\n",
|
||||
"1. Load data for different ball types\n",
|
||||
"2. Detect bounces and calculate Coefficient of Restitution (COR)\n",
|
||||
"3. Create static and animated visualizations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's import the necessary modules and set up the environment."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"import sys\n",
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"from IPython.display import display, HTML\n",
|
||||
"\n",
|
||||
"# Add the src directory to the Python path\n",
|
||||
"sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname('__file__'), '..')))\n",
|
||||
"\n",
|
||||
"# Import modules from the project\n",
|
||||
"from data.loader import load_trial, GOLF_BALL_PATHS, LACROSSE_BALL_PATHS, METAL_BALL_PATHS\n",
|
||||
"from analysis.bounce_detection import process_trial, process_trials\n",
|
||||
"from visualization.static_plots import plot_position_vs_time, plot_trial_with_bounces, plot_cor_table, plot_all_cors\n",
|
||||
"from visualization.animation import create_ball_animation, create_ball_animation_with_model, display_animation\n",
|
||||
"\n",
|
||||
"# Create output directory\n",
|
||||
"output_dir = \"../../output\"\n",
|
||||
"os.makedirs(output_dir, exist_ok=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Loading and Visualizing Raw Data\n",
|
||||
"\n",
|
||||
"Let's start by loading data for a golf ball dropped from 11 inches and visualizing the raw position vs. time data."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Select a specific ball and height\n",
|
||||
"ball_type = \"Golf\"\n",
|
||||
"height = \"11 inches\"\n",
|
||||
"file_path = GOLF_BALL_PATHS[height]\n",
|
||||
"\n",
|
||||
"# Load the trial data\n",
|
||||
"df = load_trial(file_path)\n",
|
||||
"print(f\"Loaded data with {len(df)} rows\")\n",
|
||||
"\n",
|
||||
"# Display the first few rows of the data\n",
|
||||
"df.head()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Plot the raw position vs. time data\n",
|
||||
"plot_position_vs_time(df, height, ball_type)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Detecting Bounces and Calculating COR\n",
|
||||
"\n",
|
||||
"Now, let's detect the bounces in the data and calculate the Coefficient of Restitution (COR)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Process the trial to detect bounces and calculate COR\n",
|
||||
"result = process_trial(\n",
|
||||
" df, \n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" ball_type=ball_type\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Print the results\n",
|
||||
"print(f\"Number of bounces detected: {len(result['peak_indices'])}\")\n",
|
||||
"print(f\"Average COR: {result['Average COR']:.4f}\")\n",
|
||||
"print(f\"Initial height: {result['Initial Height']} inches\")\n",
|
||||
"\n",
|
||||
"# Display the bounce heights\n",
|
||||
"if 'bounce_heights' in result:\n",
|
||||
" print(\"\\nBounce Heights (inches):\")\n",
|
||||
" for i, height in enumerate(result['bounce_heights']):\n",
|
||||
" print(f\"Bounce {i+1}: {height:.2f}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Plot the trial with detected bounces\n",
|
||||
"plot_trial_with_bounces(\n",
|
||||
" df, \n",
|
||||
" result['peak_indices'], \n",
|
||||
" df['Position'].values,\n",
|
||||
" height, \n",
|
||||
" ball_type,\n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" cor=result['Average COR']\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Creating Animations\n",
|
||||
"\n",
|
||||
"Let's create an animation of the bouncing ball."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create a simple animation of the bouncing ball\n",
|
||||
"anim = create_ball_animation(\n",
|
||||
" df,\n",
|
||||
" ball_radius=0.5,\n",
|
||||
" fps=30,\n",
|
||||
" duration=2.0, # Only show the first 2 seconds\n",
|
||||
" title=f\"{ball_type} Ball - {height}\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Display the animation\n",
|
||||
"display_animation(anim)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create an animation with both the actual data and an ideal model\n",
|
||||
"anim_with_model = create_ball_animation_with_model(\n",
|
||||
" df, \n",
|
||||
" result['peak_indices'], \n",
|
||||
" result['Average COR'], \n",
|
||||
" float(height.split()[0]),\n",
|
||||
" ball_radius=0.5,\n",
|
||||
" fps=30,\n",
|
||||
" duration=2.0, # Only show the first 2 seconds\n",
|
||||
" title=f\"{ball_type} Ball - {height} (with Model)\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Display the animation\n",
|
||||
"display_animation(anim_with_model)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Analyzing Multiple Trials\n",
|
||||
"\n",
|
||||
"Now, let's analyze all the golf ball trials and compare the COR values."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Process all golf ball trials\n",
|
||||
"golf_trials = []\n",
|
||||
"\n",
|
||||
"for height, path in GOLF_BALL_PATHS.items():\n",
|
||||
" print(f\"Processing golf ball from {height}...\")\n",
|
||||
" \n",
|
||||
" # Load the trial data\n",
|
||||
" df = load_trial(path)\n",
|
||||
" \n",
|
||||
" # Process the trial\n",
|
||||
" result = process_trial(\n",
|
||||
" df, \n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" ball_type=\"Golf\"\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Store the result\n",
|
||||
" result['Initial Height'] = float(height.split()[0])\n",
|
||||
" result['Ball Type'] = \"Golf\"\n",
|
||||
" result['Path'] = path\n",
|
||||
" golf_trials.append(result)\n",
|
||||
"\n",
|
||||
"# Create a summary DataFrame\n",
|
||||
"golf_summary = pd.DataFrame(golf_trials)\n",
|
||||
"\n",
|
||||
"# Display the summary\n",
|
||||
"golf_summary[['Initial Height', 'Average COR', 'Num Bounces']]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Plot COR vs. initial height for golf balls\n",
|
||||
"plot_cor_table(golf_summary, \"Golf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Comparing Different Ball Types\n",
|
||||
"\n",
|
||||
"Finally, let's compare the COR values for different ball types."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Process lacrosse ball trials\n",
|
||||
"lacrosse_trials = []\n",
|
||||
"\n",
|
||||
"for height, path in LACROSSE_BALL_PATHS.items():\n",
|
||||
" print(f\"Processing lacrosse ball from {height}...\")\n",
|
||||
" \n",
|
||||
" # Load the trial data\n",
|
||||
" df = load_trial(path)\n",
|
||||
" \n",
|
||||
" # Process the trial\n",
|
||||
" result = process_trial(\n",
|
||||
" df, \n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" ball_type=\"Lacrosse\"\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Store the result\n",
|
||||
" result['Initial Height'] = float(height.split()[0])\n",
|
||||
" result['Ball Type'] = \"Lacrosse\"\n",
|
||||
" result['Path'] = path\n",
|
||||
" lacrosse_trials.append(result)\n",
|
||||
"\n",
|
||||
"# Create a summary DataFrame\n",
|
||||
"lacrosse_summary = pd.DataFrame(lacrosse_trials)\n",
|
||||
"\n",
|
||||
"# Process metal ball trials\n",
|
||||
"metal_trials = []\n",
|
||||
"\n",
|
||||
"for height, path in METAL_BALL_PATHS.items():\n",
|
||||
" print(f\"Processing metal ball from {height}...\")\n",
|
||||
" \n",
|
||||
" # Load the trial data\n",
|
||||
" df = load_trial(path)\n",
|
||||
" \n",
|
||||
" # Process the trial\n",
|
||||
" result = process_trial(\n",
|
||||
" df, \n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" ball_type=\"Metal\"\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Store the result\n",
|
||||
" result['Initial Height'] = float(height.split()[0])\n",
|
||||
" result['Ball Type'] = \"Metal\"\n",
|
||||
" result['Path'] = path\n",
|
||||
" metal_trials.append(result)\n",
|
||||
"\n",
|
||||
"# Create a summary DataFrame\n",
|
||||
"metal_summary = pd.DataFrame(metal_trials)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Plot all CORs together\n",
|
||||
"plot_all_cors(golf_summary, lacrosse_summary, metal_summary)\n",
|
||||
"\n",
|
||||
"# Save the combined plot\n",
|
||||
"plt.savefig(os.path.join(output_dir, \"all_cors_comparison.png\"), dpi=300)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Conclusion\n",
|
||||
"\n",
|
||||
"In this notebook, we've demonstrated how to use the modular structure of the bouncing ball analysis project to:\n",
|
||||
"1. Load data for different ball types\n",
|
||||
"2. Detect bounces and calculate Coefficient of Restitution (COR)\n",
|
||||
"3. Create static and animated visualizations\n",
|
||||
"4. Compare COR values for different ball types\n",
|
||||
"\n",
|
||||
"The modular structure makes it easy to perform these analyses and visualizations with clean, reusable code."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.8.10"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
399
src/examples/create_notebook.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
Script to create a Jupyter notebook example for the bouncing ball analysis project.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
def create_notebook():
|
||||
"""
|
||||
Create a Jupyter notebook with examples of using the bouncing ball analysis modules.
|
||||
"""
|
||||
notebook = {
|
||||
"cells": [
|
||||
# Title and introduction
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Bouncing Ball Analysis Example\n",
|
||||
"\n",
|
||||
"This notebook demonstrates how to use the modular structure of the bouncing ball analysis project to:\n",
|
||||
"1. Load data for different ball types\n",
|
||||
"2. Detect bounces and calculate Coefficient of Restitution (COR)\n",
|
||||
"3. Create static and animated visualizations"
|
||||
]
|
||||
},
|
||||
|
||||
# Setup section
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's import the necessary modules and set up the environment."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"import sys\n",
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"from IPython.display import display, HTML\n",
|
||||
"\n",
|
||||
"# Add the src directory to the Python path\n",
|
||||
"sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname('__file__'), '..')))\n",
|
||||
"\n",
|
||||
"# Import modules from the project\n",
|
||||
"from data.loader import load_trial, GOLF_BALL_PATHS, LACROSSE_BALL_PATHS, METAL_BALL_PATHS\n",
|
||||
"from analysis.bounce_detection import process_trial, process_trials\n",
|
||||
"from visualization.static_plots import plot_position_vs_time, plot_trial_with_bounces, plot_cor_table, plot_all_cors\n",
|
||||
"from visualization.animation import create_ball_animation, create_ball_animation_with_model, display_animation\n",
|
||||
"\n",
|
||||
"# Create output directory\n",
|
||||
"output_dir = \"../../output\"\n",
|
||||
"os.makedirs(output_dir, exist_ok=True)"
|
||||
]
|
||||
},
|
||||
|
||||
# Section 1: Loading and Visualizing Raw Data
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Loading and Visualizing Raw Data\n",
|
||||
"\n",
|
||||
"Let's start by loading data for a golf ball dropped from 11 inches and visualizing the raw position vs. time data."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Select a specific ball and height\n",
|
||||
"ball_type = \"Golf\"\n",
|
||||
"height = \"11 inches\"\n",
|
||||
"file_path = GOLF_BALL_PATHS[height]\n",
|
||||
"\n",
|
||||
"# Load the trial data\n",
|
||||
"df = load_trial(file_path)\n",
|
||||
"print(f\"Loaded data with {len(df)} rows\")\n",
|
||||
"\n",
|
||||
"# Display the first few rows of the data\n",
|
||||
"df.head()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Plot the raw position vs. time data\n",
|
||||
"plot_position_vs_time(df, height, ball_type)"
|
||||
]
|
||||
},
|
||||
|
||||
# Section 2: Detecting Bounces and Calculating COR
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Detecting Bounces and Calculating COR\n",
|
||||
"\n",
|
||||
"Now, let's detect the bounces in the data and calculate the Coefficient of Restitution (COR)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Process the trial to detect bounces and calculate COR\n",
|
||||
"result = process_trial(\n",
|
||||
" df, \n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" ball_type=ball_type\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Print the results\n",
|
||||
"print(f\"Number of bounces detected: {len(result['peak_indices'])}\")\n",
|
||||
"print(f\"Average COR: {result['Average COR']:.4f}\")\n",
|
||||
"print(f\"Initial height: {result['Initial Height']} inches\")\n",
|
||||
"\n",
|
||||
"# Display the bounce heights\n",
|
||||
"if 'bounce_heights' in result:\n",
|
||||
" print(\"\\nBounce Heights (inches):\")\n",
|
||||
" for i, height in enumerate(result['bounce_heights']):\n",
|
||||
" print(f\"Bounce {i+1}: {height:.2f}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Plot the trial with detected bounces\n",
|
||||
"plot_trial_with_bounces(\n",
|
||||
" df, \n",
|
||||
" result['peak_indices'], \n",
|
||||
" df['Position'].values,\n",
|
||||
" height, \n",
|
||||
" ball_type,\n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" cor=result['Average COR']\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
|
||||
# Section 3: Creating Animations
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Creating Animations\n",
|
||||
"\n",
|
||||
"Let's create an animation of the bouncing ball."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create a simple animation of the bouncing ball\n",
|
||||
"anim = create_ball_animation(\n",
|
||||
" df,\n",
|
||||
" ball_radius=0.5,\n",
|
||||
" fps=30,\n",
|
||||
" duration=2.0, # Only show the first 2 seconds\n",
|
||||
" title=f\"{ball_type} Ball - {height}\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Display the animation\n",
|
||||
"display_animation(anim)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create an animation with both the actual data and an ideal model\n",
|
||||
"anim_with_model = create_ball_animation_with_model(\n",
|
||||
" df, \n",
|
||||
" result['peak_indices'], \n",
|
||||
" result['Average COR'], \n",
|
||||
" float(height.split()[0]),\n",
|
||||
" ball_radius=0.5,\n",
|
||||
" fps=30,\n",
|
||||
" duration=2.0, # Only show the first 2 seconds\n",
|
||||
" title=f\"{ball_type} Ball - {height} (with Model)\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Display the animation\n",
|
||||
"display_animation(anim_with_model)"
|
||||
]
|
||||
},
|
||||
|
||||
# Section 4: Analyzing Multiple Trials
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Analyzing Multiple Trials\n",
|
||||
"\n",
|
||||
"Now, let's analyze all the golf ball trials and compare the COR values."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Process all golf ball trials\n",
|
||||
"golf_trials = []\n",
|
||||
"\n",
|
||||
"for height, path in GOLF_BALL_PATHS.items():\n",
|
||||
" print(f\"Processing golf ball from {height}...\")\n",
|
||||
" \n",
|
||||
" # Load the trial data\n",
|
||||
" df = load_trial(path)\n",
|
||||
" \n",
|
||||
" # Process the trial\n",
|
||||
" result = process_trial(\n",
|
||||
" df, \n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" ball_type=\"Golf\"\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Store the result\n",
|
||||
" result['Initial Height'] = float(height.split()[0])\n",
|
||||
" result['Ball Type'] = \"Golf\"\n",
|
||||
" result['Path'] = path\n",
|
||||
" golf_trials.append(result)\n",
|
||||
"\n",
|
||||
"# Create a summary DataFrame\n",
|
||||
"golf_summary = pd.DataFrame(golf_trials)\n",
|
||||
"\n",
|
||||
"# Display the summary\n",
|
||||
"golf_summary[['Initial Height', 'Average COR', 'Num Bounces']]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Plot COR vs. initial height for golf balls\n",
|
||||
"plot_cor_table(golf_summary, \"Golf\")"
|
||||
]
|
||||
},
|
||||
|
||||
# Section 5: Comparing Different Ball Types
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Comparing Different Ball Types\n",
|
||||
"\n",
|
||||
"Finally, let's compare the COR values for different ball types."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Process lacrosse ball trials\n",
|
||||
"lacrosse_trials = []\n",
|
||||
"\n",
|
||||
"for height, path in LACROSSE_BALL_PATHS.items():\n",
|
||||
" print(f\"Processing lacrosse ball from {height}...\")\n",
|
||||
" \n",
|
||||
" # Load the trial data\n",
|
||||
" df = load_trial(path)\n",
|
||||
" \n",
|
||||
" # Process the trial\n",
|
||||
" result = process_trial(\n",
|
||||
" df, \n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" ball_type=\"Lacrosse\"\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Store the result\n",
|
||||
" result['Initial Height'] = float(height.split()[0])\n",
|
||||
" result['Ball Type'] = \"Lacrosse\"\n",
|
||||
" result['Path'] = path\n",
|
||||
" lacrosse_trials.append(result)\n",
|
||||
"\n",
|
||||
"# Create a summary DataFrame\n",
|
||||
"lacrosse_summary = pd.DataFrame(lacrosse_trials)\n",
|
||||
"\n",
|
||||
"# Process metal ball trials\n",
|
||||
"metal_trials = []\n",
|
||||
"\n",
|
||||
"for height, path in METAL_BALL_PATHS.items():\n",
|
||||
" print(f\"Processing metal ball from {height}...\")\n",
|
||||
" \n",
|
||||
" # Load the trial data\n",
|
||||
" df = load_trial(path)\n",
|
||||
" \n",
|
||||
" # Process the trial\n",
|
||||
" result = process_trial(\n",
|
||||
" df, \n",
|
||||
" initial_height=float(height.split()[0]),\n",
|
||||
" ball_type=\"Metal\"\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Store the result\n",
|
||||
" result['Initial Height'] = float(height.split()[0])\n",
|
||||
" result['Ball Type'] = \"Metal\"\n",
|
||||
" result['Path'] = path\n",
|
||||
" metal_trials.append(result)\n",
|
||||
"\n",
|
||||
"# Create a summary DataFrame\n",
|
||||
"metal_summary = pd.DataFrame(metal_trials)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Plot all CORs together\n",
|
||||
"plot_all_cors(golf_summary, lacrosse_summary, metal_summary)\n",
|
||||
"\n",
|
||||
"# Save the combined plot\n",
|
||||
"plt.savefig(os.path.join(output_dir, \"all_cors_comparison.png\"), dpi=300)"
|
||||
]
|
||||
},
|
||||
|
||||
# Conclusion
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Conclusion\n",
|
||||
"\n",
|
||||
"In this notebook, we've demonstrated how to use the modular structure of the bouncing ball analysis project to:\n",
|
||||
"1. Load data for different ball types\n",
|
||||
"2. Detect bounces and calculate Coefficient of Restitution (COR)\n",
|
||||
"3. Create static and animated visualizations\n",
|
||||
"4. Compare COR values for different ball types\n",
|
||||
"\n",
|
||||
"The modular structure makes it easy to perform these analyses and visualizations with clean, reusable code."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.8.10"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
||||
# Write the notebook to a file
|
||||
notebook_path = "src/examples/bouncing_ball_analysis.ipynb"
|
||||
os.makedirs(os.path.dirname(notebook_path), exist_ok=True)
|
||||
with open(notebook_path, "w") as f:
|
||||
json.dump(notebook, f, indent=1)
|
||||
|
||||
print(f"Jupyter notebook created: {notebook_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_notebook()
|
||||
94
src/examples/simple_analysis.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Simple example script demonstrating how to use the new modular structure.
|
||||
|
||||
This script shows how to:
|
||||
1. Load data for a specific ball type and height
|
||||
2. Detect bounces and calculate COR
|
||||
3. Create static and animated visualizations
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Add the src directory to the Python path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
# Import modules from the project
|
||||
from data.loader import load_trial, GOLF_BALL_PATHS
|
||||
from analysis.bounce_detection import process_trial
|
||||
from visualization.static_plots import plot_trial_with_bounces
|
||||
from visualization.animation import create_ball_animation_with_model, save_animation
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function to demonstrate the use of the modular structure.
|
||||
"""
|
||||
print("Simple Bouncing Ball Analysis Example")
|
||||
print("====================================")
|
||||
|
||||
# Create output directory
|
||||
output_dir = "output"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Select a specific ball and height
|
||||
ball_type = "Golf"
|
||||
height = "11 inches"
|
||||
file_path = GOLF_BALL_PATHS[height]
|
||||
|
||||
print(f"Analyzing {ball_type} ball from {height}...")
|
||||
|
||||
# Load the trial data
|
||||
df = load_trial(file_path)
|
||||
print(f"Loaded data with {len(df)} rows")
|
||||
|
||||
# Process the trial to detect bounces and calculate COR
|
||||
result = process_trial(
|
||||
df,
|
||||
initial_height=float(height.split()[0]),
|
||||
ball_type=ball_type
|
||||
)
|
||||
|
||||
# Print the results
|
||||
print("\nAnalysis Results:")
|
||||
print(f"Number of bounces detected: {len(result['peak_indices'])}")
|
||||
print(f"Average COR: {result['Average COR']:.4f}")
|
||||
print(f"Initial height: {result['Initial Height']} inches")
|
||||
|
||||
# Create a static plot with detected bounces
|
||||
print("\nCreating static plot...")
|
||||
plot_trial_with_bounces(
|
||||
df,
|
||||
result['peak_indices'],
|
||||
df['Position'].values,
|
||||
height,
|
||||
ball_type,
|
||||
initial_height=float(height.split()[0]),
|
||||
cor=result['Average COR']
|
||||
)
|
||||
|
||||
# Save the static plot
|
||||
plt.savefig(os.path.join(output_dir, f"{ball_type.lower()}_{height.split()[0]}_bounces.png"))
|
||||
plt.close()
|
||||
|
||||
# Create an animation
|
||||
print("\nCreating animation...")
|
||||
anim = create_ball_animation_with_model(
|
||||
df,
|
||||
result['peak_indices'],
|
||||
result['Average COR'],
|
||||
float(height.split()[0]),
|
||||
title=f"{ball_type} Ball - {height}"
|
||||
)
|
||||
|
||||
# Save the animation
|
||||
animation_path = os.path.join(output_dir, f"{ball_type.lower()}_{height.split()[0]}_animation.mp4")
|
||||
save_animation(anim, animation_path)
|
||||
print(f"Animation saved to {animation_path}")
|
||||
|
||||
print("\nAnalysis complete! Results saved to the 'output' directory.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
62
src/lab2_part1.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
|
||||
sns.set_theme(style="whitegrid", palette="muted")
|
||||
|
||||
# File paths for golf ball data
|
||||
golf_file_paths = {
|
||||
"11 inches": "golf_11.csv",
|
||||
"12 inches": "golf_12.csv",
|
||||
"13 inches": "golf_13.csv",
|
||||
"14 inches": "golf_14.csv",
|
||||
"15 inches": "golf_15.csv"
|
||||
}
|
||||
|
||||
# File paths for lacrosse ball data
|
||||
lacrosse_file_paths = {
|
||||
"18 inches": "l_18.csv",
|
||||
"19 inches": "l_19.csv",
|
||||
"20 inches": "l_20.csv",
|
||||
"21 inches": "l_21.csv",
|
||||
"22 inches": "l_22.csv"
|
||||
}
|
||||
|
||||
# File paths for metal ball data
|
||||
metal_file_paths = {
|
||||
"2 inches": "metal_2.csv",
|
||||
"4 inches": "metal_4.csv",
|
||||
"6 inches": "metal_6.csv",
|
||||
"8 inches": "metal_8.csv",
|
||||
"10 inches": "metal_10.csv"
|
||||
}
|
||||
|
||||
def plot_position_vs_time(file_paths, ball_type):
|
||||
for label, path in file_paths.items():
|
||||
# Read CSV with no header, assuming columns: Time, Position
|
||||
df = pd.read_csv(path, header=None, names=["Time", "Position"])
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
ax.plot(df["Time"], df["Position"], label=label, marker='o', markersize=3, linewidth=2)
|
||||
|
||||
ax.set_xlabel("Time (seconds)", fontsize=14)
|
||||
ax.set_ylabel("Position (inches)", fontsize=14)
|
||||
ax.set_title(f"{ball_type} - Position vs. Time\nInitial Height: {label}", fontsize=16, pad=15)
|
||||
|
||||
ax.grid(True, which='both', linestyle='--', linewidth=0.5, alpha=0.7)
|
||||
|
||||
ax.legend(fontsize=12)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
#plt.savefig()
|
||||
|
||||
# Plot golf ball data
|
||||
plot_position_vs_time(golf_file_paths, "Golf Ball")
|
||||
|
||||
# Plot lacrosse ball data
|
||||
plot_position_vs_time(lacrosse_file_paths, "Lacrosse Ball")
|
||||
|
||||
# Plot metal ball data
|
||||
plot_position_vs_time(metal_file_paths, "Metal Ball")
|
||||
408
src/lab2_part2.py
Normal file
@@ -0,0 +1,408 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
from scipy.signal import find_peaks
|
||||
|
||||
sns.set_theme(style="whitegrid", palette="muted")
|
||||
|
||||
def read_trial(path):
|
||||
"""
|
||||
Reads a CSV file containing time and position data (with no header)
|
||||
and returns a DataFrame with columns 'Time' and 'Position'.
|
||||
"""
|
||||
return pd.read_csv(path, header=None, names=['Time', 'Position'])
|
||||
|
||||
|
||||
def detect_bounces(df,
|
||||
prominence=0,
|
||||
min_distance=10,
|
||||
min_time_diff=0.2,
|
||||
max_bounces=7,
|
||||
fs=None):
|
||||
"""
|
||||
Detects bounce events by locating peaks in the signal.
|
||||
|
||||
Parameters:
|
||||
- df: DataFrame with 'Time' and 'Position'
|
||||
- prominence: required prominence for peaks (in the signal)
|
||||
- min_distance: minimum number of data points between peaks
|
||||
- min_time_diff: minimum time difference (seconds) between bounces
|
||||
- max_bounces: maximum bounces to report
|
||||
- fs: override sampling frequency (Hz). If None, computed from 'Time'.
|
||||
|
||||
Returns:
|
||||
- peak_indices (array of indices),
|
||||
- bounce_heights (array of position values at these bounces),
|
||||
- bounce_times (array of times of these bounces),
|
||||
- signal_data (the data used for detection, here the raw Position values)
|
||||
"""
|
||||
if fs is None:
|
||||
dt = np.median(np.diff(df['Time']))
|
||||
if dt <= 0:
|
||||
raise ValueError(
|
||||
"Time differences are non-positive. "
|
||||
"Ensure your 'Time' column is in seconds."
|
||||
)
|
||||
fs = 1 / dt
|
||||
|
||||
signal_data = df['Position'].values
|
||||
|
||||
peaks, _ = find_peaks(signal_data, prominence=prominence, distance=min_distance)
|
||||
|
||||
filtered_peaks = []
|
||||
filtered_times = []
|
||||
group_start_time = None
|
||||
group_best_idx = None
|
||||
group_best_value = None
|
||||
|
||||
for idx in peaks:
|
||||
current_time = df['Time'].iloc[idx]
|
||||
current_value = signal_data[idx]
|
||||
|
||||
if group_start_time is None:
|
||||
group_start_time = current_time
|
||||
group_best_idx = idx
|
||||
group_best_value = current_value
|
||||
continue
|
||||
|
||||
if (current_time - group_start_time) < min_time_diff:
|
||||
if current_value > group_best_value:
|
||||
group_best_idx = idx
|
||||
group_best_value = current_value
|
||||
else:
|
||||
filtered_peaks.append(group_best_idx)
|
||||
filtered_times.append(df['Time'].iloc[group_best_idx])
|
||||
|
||||
group_start_time = current_time
|
||||
group_best_idx = idx
|
||||
group_best_value = current_value
|
||||
|
||||
if group_best_idx is not None:
|
||||
filtered_peaks.append(group_best_idx)
|
||||
filtered_times.append(df['Time'].iloc[group_best_idx])
|
||||
|
||||
filtered_peaks = filtered_peaks[:max_bounces]
|
||||
filtered_times = filtered_times[:max_bounces]
|
||||
|
||||
bounce_heights = df['Position'].iloc[filtered_peaks].values
|
||||
bounce_times = np.array(filtered_times)
|
||||
|
||||
return np.array(filtered_peaks), bounce_heights, bounce_times, signal_data
|
||||
|
||||
|
||||
def compute_cor(bounce_heights):
|
||||
"""
|
||||
Computes the Coefficient of Restitution (COR) for consecutive bounces:
|
||||
e = sqrt( h_{n+1} / h_n )
|
||||
|
||||
Returns an array of COR values.
|
||||
"""
|
||||
cor_values = []
|
||||
for i in range(len(bounce_heights) - 1):
|
||||
if bounce_heights[i] > 0:
|
||||
cor_values.append(np.sqrt(bounce_heights[i+1] / bounce_heights[i]))
|
||||
else:
|
||||
cor_values.append(np.nan)
|
||||
return np.array(cor_values)
|
||||
|
||||
|
||||
def process_trial(path,
|
||||
prominence,
|
||||
min_distance=10,
|
||||
min_time_diff=0.2,
|
||||
max_bounces=7,
|
||||
fs=None):
|
||||
"""
|
||||
Reads trial data, detects bounces, computes COR values, and returns:
|
||||
- df (the DataFrame)
|
||||
- peak_indices
|
||||
- bounce_heights
|
||||
- bounce_times
|
||||
- cor_values
|
||||
- avg_cor
|
||||
- signal_data
|
||||
"""
|
||||
df = read_trial(path)
|
||||
peak_indices, bounce_heights, bounce_times, signal_data = detect_bounces(
|
||||
df, prominence, min_distance, min_time_diff, max_bounces, fs
|
||||
)
|
||||
cor_values = compute_cor(bounce_heights)
|
||||
avg_cor = np.mean(cor_values) if cor_values.size > 0 else np.nan
|
||||
|
||||
return df, peak_indices, bounce_heights, bounce_times, cor_values, avg_cor, signal_data
|
||||
|
||||
|
||||
def plot_trial(df, peak_indices, signal_data, label, ball_type,
|
||||
initial_height=None, cor=None, bounces=7, dt=0.001, g=386.09):
|
||||
"""
|
||||
Plots raw data (Position vs. Time) for a single trial and marks the detected bounces with red 'x'.
|
||||
Ensures:
|
||||
- Displays exactly 7 bounces.
|
||||
- X-axis starts just before the first data point and ends slightly after the last peak or data point.
|
||||
- Y-axis is a tight fit to cover the full range of data.
|
||||
"""
|
||||
plt.figure(figsize=(10, 6))
|
||||
|
||||
# 1) Plot raw signal
|
||||
plt.plot(df['Time'], df['Position'], linewidth=1.5, color='blue', label='Raw Data')
|
||||
|
||||
# 2) Mark the detected bounces
|
||||
if len(peak_indices) > 0:
|
||||
plt.plot(
|
||||
df['Time'].iloc[peak_indices],
|
||||
df['Position'].iloc[peak_indices],
|
||||
'gx',
|
||||
markersize=10,
|
||||
label='Detected Bounces'
|
||||
)
|
||||
|
||||
# 3) Simulated Idealized Bounce Trajectory (Ensuring Exactly 7 Bounces)
|
||||
if initial_height is not None and cor is not None and not np.isnan(cor):
|
||||
time_points = []
|
||||
position_points = []
|
||||
|
||||
# Initialize simulation parameters
|
||||
y = initial_height
|
||||
v_y = 0.0 # Starting from rest
|
||||
t = 0.0
|
||||
bounce_count = 0
|
||||
model_first_bounce_time = None
|
||||
|
||||
while bounce_count < bounces: # Ensure exactly 7 bounces
|
||||
time_points.append(t)
|
||||
position_points.append(y)
|
||||
|
||||
# Kinematic update
|
||||
v_y -= g * dt
|
||||
y += v_y * dt
|
||||
t += dt
|
||||
|
||||
# Bounce condition
|
||||
if y <= 0:
|
||||
bounce_count += 1
|
||||
y = 0 # Ground impact
|
||||
v_y = -v_y * cor # Apply COR
|
||||
|
||||
if model_first_bounce_time is None:
|
||||
model_first_bounce_time = t # Store first bounce time
|
||||
|
||||
# Align first model bounce with data's first detected bounce
|
||||
if model_first_bounce_time is not None and len(peak_indices) > 0:
|
||||
data_first_bounce_time = df['Time'].iloc[peak_indices[0]]
|
||||
time_shift = data_first_bounce_time - model_first_bounce_time
|
||||
time_points = [t + time_shift for t in time_points]
|
||||
|
||||
# Plot idealized trajectory
|
||||
plt.plot(time_points, position_points, 'r--', label='Ideal Trajectory')
|
||||
|
||||
# 4) Formatting the plot
|
||||
plt.title(f'{ball_type} Trial: {label}')
|
||||
plt.xlabel('Time (s)')
|
||||
plt.ylabel('Position (inches)')
|
||||
plt.autoscale(True, 'x', True)
|
||||
plt.grid(True)
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
|
||||
|
||||
def process_and_plot_all(file_paths,
|
||||
ball_type,
|
||||
plot_trials=False,
|
||||
min_distance=10,
|
||||
min_time_diff=0.2,
|
||||
relative_prominence=None,
|
||||
low_height_threshold=8,
|
||||
low_height_adjustment=0.5,
|
||||
high_height_threshold=15,
|
||||
high_height_adjustment=1.2,
|
||||
max_bounces=7,
|
||||
fs=None):
|
||||
"""
|
||||
Iterates over the given 'file_paths' dict:
|
||||
- extracts the numeric initial height from the key label (e.g. "11 inches"),
|
||||
- computes effective prominence,
|
||||
- processes each trial,
|
||||
- optionally plots each trial (with or without an ideal bounce overlay).
|
||||
|
||||
Returns a summary DataFrame with:
|
||||
'Trial', 'Initial Height', 'Average COR', 'Bounce Times', 'Bounce Heights', and 'COR Values'.
|
||||
"""
|
||||
results = []
|
||||
for init_label, path in file_paths.items():
|
||||
try:
|
||||
initial_height = float(init_label.split()[0])
|
||||
except ValueError:
|
||||
initial_height = np.nan
|
||||
|
||||
# Compute effective prominence
|
||||
effective_prominence = relative_prominence * initial_height
|
||||
if initial_height < low_height_threshold:
|
||||
effective_prominence *= low_height_adjustment
|
||||
elif initial_height > high_height_threshold:
|
||||
effective_prominence *= high_height_adjustment
|
||||
|
||||
print(f"Processing trial '{init_label}' with effective prominence: {effective_prominence}")
|
||||
|
||||
# Process the trial
|
||||
df, peak_indices, bounce_heights, bounce_times, cor_values, avg_cor, signal_data = process_trial(
|
||||
path, effective_prominence,
|
||||
min_distance=min_distance,
|
||||
min_time_diff=min_time_diff,
|
||||
max_bounces=max_bounces,
|
||||
fs=fs
|
||||
)
|
||||
|
||||
|
||||
results.append({
|
||||
'Trial': init_label,
|
||||
'Initial Height': initial_height,
|
||||
'Average COR': avg_cor,
|
||||
'Bounce Times': bounce_times,
|
||||
'Bounce Heights': bounce_heights,
|
||||
'COR Values': cor_values
|
||||
})
|
||||
|
||||
|
||||
if plot_trials:
|
||||
plot_trial(
|
||||
df,
|
||||
peak_indices,
|
||||
signal_data,
|
||||
label=init_label,
|
||||
ball_type=ball_type,
|
||||
initial_height=initial_height,
|
||||
cor=avg_cor,
|
||||
bounces=5,
|
||||
dt=0.001,
|
||||
g=386.09 #using inches for position
|
||||
)
|
||||
|
||||
summary_df = pd.DataFrame(results)
|
||||
summary_df.sort_values(by='Initial Height', inplace=True)
|
||||
return summary_df
|
||||
|
||||
|
||||
def plot_cor_table(cor_df, ball_type):
|
||||
"""
|
||||
Plots a line (and markers) of Average COR vs. Initial Height from the summary DataFrame.
|
||||
"""
|
||||
plt.figure(figsize=(8, 5))
|
||||
sns.lineplot(x='Initial Height', y='Average COR', marker='o', data=cor_df)
|
||||
plt.title(f'Average COR vs. Initial Height for {ball_type} Ball')
|
||||
plt.xlabel('Initial Height (inches)')
|
||||
plt.ylabel('Average COR')
|
||||
plt.grid(True)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
golf_file_paths = {
|
||||
"11 inches": "golf_11.csv",
|
||||
"12 inches": "golf_12.csv",
|
||||
"13 inches": "golf_13.csv",
|
||||
"14 inches": "golf_14.csv",
|
||||
"15 inches": "golf_15.csv"
|
||||
}
|
||||
lacrosse_file_paths = {
|
||||
"18 inches": "l_18.csv",
|
||||
"19 inches": "l_19.csv",
|
||||
"20 inches": "l_20.csv",
|
||||
"21 inches": "l_21.csv",
|
||||
"22 inches": "l_22.csv"
|
||||
}
|
||||
metal_file_paths = {
|
||||
"2 inches": "metal_2.csv",
|
||||
"4 inches": "metal_4.csv",
|
||||
"6 inches": "metal_6.csv",
|
||||
"8 inches": "metal_8.csv",
|
||||
"10 inches": "metal_10.csv"
|
||||
}
|
||||
|
||||
# General detection/plotting parameters
|
||||
min_distance = 5
|
||||
min_time_diff = 0.05
|
||||
fs = 50
|
||||
|
||||
# Prominence adjustments
|
||||
golf_relative_prominence = 0.15
|
||||
golf_low_threshold = 12.5
|
||||
golf_low_adjustment = 1.1
|
||||
golf_high_threshold = 14.5
|
||||
golf_high_adjustment = 1.3
|
||||
|
||||
lacrosse_relative_prominence = 0.05
|
||||
lacrosse_low_threshold = 18.5
|
||||
lacrosse_low_adjustment = 1.1
|
||||
lacrosse_high_threshold = 21.5
|
||||
lacrosse_high_adjustment = 1.3
|
||||
|
||||
metal_relative_prominence = 0.05
|
||||
metal_low_threshold = 5
|
||||
metal_low_adjustment = 0.8
|
||||
metal_high_threshold = 8
|
||||
metal_high_adjustment = 1.2
|
||||
|
||||
# Process & plot Golf
|
||||
golf_results = process_and_plot_all(
|
||||
golf_file_paths,
|
||||
ball_type='Golf',
|
||||
plot_trials=True,
|
||||
min_distance=min_distance,
|
||||
min_time_diff=min_time_diff,
|
||||
relative_prominence=golf_relative_prominence,
|
||||
low_height_threshold=golf_low_threshold,
|
||||
low_height_adjustment=golf_low_adjustment,
|
||||
high_height_threshold=golf_high_threshold,
|
||||
high_height_adjustment=golf_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=fs
|
||||
)
|
||||
|
||||
# Process & plot Lacrosse
|
||||
lacrosse_results = process_and_plot_all(
|
||||
lacrosse_file_paths,
|
||||
ball_type='Lacrosse',
|
||||
plot_trials=True,
|
||||
min_distance=min_distance,
|
||||
min_time_diff=min_time_diff,
|
||||
relative_prominence=lacrosse_relative_prominence,
|
||||
low_height_threshold=lacrosse_low_threshold,
|
||||
low_height_adjustment=lacrosse_low_adjustment,
|
||||
high_height_threshold=lacrosse_high_threshold,
|
||||
high_height_adjustment=lacrosse_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=fs
|
||||
)
|
||||
|
||||
# Process & plot Metal
|
||||
metal_results = process_and_plot_all(
|
||||
metal_file_paths,
|
||||
ball_type='Metal',
|
||||
plot_trials=True,
|
||||
min_distance=min_distance,
|
||||
min_time_diff=min_time_diff,
|
||||
relative_prominence=metal_relative_prominence,
|
||||
low_height_threshold=metal_low_threshold,
|
||||
low_height_adjustment=metal_low_adjustment,
|
||||
high_height_threshold=metal_high_threshold,
|
||||
high_height_adjustment=metal_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=fs
|
||||
)
|
||||
|
||||
# Print summary tables
|
||||
print("Golf Ball COR Table:")
|
||||
print(golf_results[['Trial', 'Initial Height', 'Average COR']])
|
||||
print("\nLacrosse Ball COR Table:")
|
||||
print(lacrosse_results[['Trial', 'Initial Height', 'Average COR']])
|
||||
print("\nMetal Ball COR Table:")
|
||||
print(metal_results[['Trial', 'Initial Height', 'Average COR']])
|
||||
|
||||
# Plot COR vs. initial height for each ball type
|
||||
plot_cor_table(golf_results, 'Golf')
|
||||
plot_cor_table(lacrosse_results, 'Lacrosse')
|
||||
plot_cor_table(metal_results, 'Metal')
|
||||
136
src/main.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Main script for the bouncing ball analysis project.
|
||||
|
||||
This script serves as the entry point for analyzing bouncing ball data,
|
||||
detecting bounces, calculating COR values, and generating visualizations.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from IPython.display import display, HTML
|
||||
|
||||
# Import modules from the project
|
||||
from data.loader import load_trial, GOLF_BALL_PATHS, LACROSSE_BALL_PATHS, METAL_BALL_PATHS
|
||||
from analysis.bounce_detection import process_trial, process_trials, BALL_PARAMS
|
||||
from visualization.static_plots import (
|
||||
plot_position_vs_time,
|
||||
plot_trial_with_bounces,
|
||||
plot_cor_table,
|
||||
plot_all_cors
|
||||
)
|
||||
from visualization.animation import (
|
||||
create_ball_animation,
|
||||
create_ball_animation_with_model,
|
||||
display_animation,
|
||||
save_animation
|
||||
)
|
||||
|
||||
|
||||
def analyze_ball_type(ball_type, paths_dict, output_dir="output"):
|
||||
"""
|
||||
Analyze a specific type of ball using the provided paths.
|
||||
|
||||
Parameters:
|
||||
ball_type (str): Type of ball (e.g., "Golf", "Lacrosse", "Metal")
|
||||
paths_dict (dict): Dictionary mapping initial heights to file paths
|
||||
output_dir (str): Directory to save output files
|
||||
|
||||
Returns:
|
||||
pandas.DataFrame: Summary DataFrame with trial information
|
||||
"""
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Analyzing {ball_type} Ball Data")
|
||||
print(f"{'='*50}")
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Process all trials for this ball type
|
||||
trials_data = []
|
||||
|
||||
for height, path in paths_dict.items():
|
||||
print(f"\nProcessing {ball_type} ball from {height} inches...")
|
||||
|
||||
# Load the trial data
|
||||
df = load_trial(path)
|
||||
|
||||
# Process the trial
|
||||
result = process_trial(
|
||||
df,
|
||||
initial_height=float(height.split()[0]),
|
||||
ball_type=ball_type
|
||||
)
|
||||
|
||||
# Store the result
|
||||
result['Initial Height'] = float(height.split()[0])
|
||||
result['Ball Type'] = ball_type
|
||||
result['Path'] = path
|
||||
trials_data.append(result)
|
||||
|
||||
# Plot the trial with detected bounces
|
||||
plot_trial_with_bounces(
|
||||
df,
|
||||
result['peak_indices'],
|
||||
df['Position'].values,
|
||||
height,
|
||||
ball_type,
|
||||
initial_height=float(height.split()[0]),
|
||||
cor=result['Average COR']
|
||||
)
|
||||
|
||||
# Create and save animation
|
||||
anim = create_ball_animation_with_model(
|
||||
df,
|
||||
result['peak_indices'],
|
||||
result['Average COR'],
|
||||
float(height.split()[0]),
|
||||
title=f"{ball_type} Ball - {height}"
|
||||
)
|
||||
|
||||
# Save the animation
|
||||
animation_path = os.path.join(output_dir, f"{ball_type.lower()}_{height.split()[0]}_animation.mp4")
|
||||
save_animation(anim, animation_path)
|
||||
print(f"Animation saved to {animation_path}")
|
||||
|
||||
# Create a summary DataFrame
|
||||
summary_df = pd.DataFrame(trials_data)
|
||||
|
||||
# Plot COR vs. initial height
|
||||
plot_cor_table(summary_df, ball_type)
|
||||
|
||||
# Save the summary DataFrame
|
||||
summary_path = os.path.join(output_dir, f"{ball_type.lower()}_summary.csv")
|
||||
summary_df.to_csv(summary_path, index=False)
|
||||
print(f"Summary data saved to {summary_path}")
|
||||
|
||||
return summary_df
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function to run the bouncing ball analysis.
|
||||
"""
|
||||
print("Starting Bouncing Ball Analysis")
|
||||
|
||||
# Create output directory
|
||||
output_dir = "output"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Analyze each ball type
|
||||
golf_summary = analyze_ball_type("Golf", GOLF_BALL_PATHS, output_dir)
|
||||
lacrosse_summary = analyze_ball_type("Lacrosse", LACROSSE_BALL_PATHS, output_dir)
|
||||
metal_summary = analyze_ball_type("Metal", METAL_BALL_PATHS, output_dir)
|
||||
|
||||
# Plot all CORs together
|
||||
plot_all_cors(golf_summary, lacrosse_summary, metal_summary)
|
||||
|
||||
# Save the combined plot
|
||||
plt.savefig(os.path.join(output_dir, "all_cors_comparison.png"), dpi=300)
|
||||
|
||||
print("\nAnalysis complete! Results saved to the 'output' directory.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
src/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Utils package initialization
|
||||
80
src/utils/cleanup.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Cleanup script to remove extraneous files from the root directory.
|
||||
|
||||
This script removes files that have already been organized into the src directory structure.
|
||||
"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import sys
|
||||
|
||||
def confirm_deletion(files):
|
||||
"""
|
||||
Ask for confirmation before deleting files.
|
||||
|
||||
Parameters:
|
||||
files (list): List of files to delete
|
||||
|
||||
Returns:
|
||||
bool: True if user confirms deletion, False otherwise
|
||||
"""
|
||||
if not files:
|
||||
print("No files to delete.")
|
||||
return False
|
||||
|
||||
print("The following files will be deleted:")
|
||||
for file in files:
|
||||
print(f" - {file}")
|
||||
|
||||
response = input("\nAre you sure you want to delete these files? (y/n): ")
|
||||
return response.lower() == 'y'
|
||||
|
||||
|
||||
def cleanup_root_directory():
|
||||
"""
|
||||
Remove extraneous files from the root directory.
|
||||
"""
|
||||
# Files to remove
|
||||
python_files = [
|
||||
"lab2_part1.py",
|
||||
"lab2_part2.py",
|
||||
"lab2_part1_animated.py",
|
||||
"lab2_part2_animated.py",
|
||||
"animate_bouncing_balls.py"
|
||||
]
|
||||
|
||||
# Data files
|
||||
data_files = glob.glob("*.csv")
|
||||
|
||||
# Image files
|
||||
image_files = glob.glob("*.png")
|
||||
|
||||
# Combine all files
|
||||
all_files = python_files + data_files + image_files
|
||||
|
||||
# Filter out files that don't exist
|
||||
files_to_delete = [file for file in all_files if os.path.exists(file)]
|
||||
|
||||
# Ask for confirmation
|
||||
if confirm_deletion(files_to_delete):
|
||||
# Delete files
|
||||
for file in files_to_delete:
|
||||
try:
|
||||
os.remove(file)
|
||||
print(f"Deleted: {file}")
|
||||
except Exception as e:
|
||||
print(f"Error deleting {file}: {e}")
|
||||
|
||||
print("\nCleanup complete!")
|
||||
else:
|
||||
print("\nCleanup cancelled.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check if script is run from the root directory
|
||||
if not os.path.exists("src/utils/cleanup.py"):
|
||||
print("Error: This script must be run from the project root directory.")
|
||||
print("Please run: python src/utils/cleanup.py")
|
||||
sys.exit(1)
|
||||
|
||||
cleanup_root_directory()
|
||||
170
src/utils/helpers.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Helper functions for the bouncing ball analysis project.
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.ticker import MaxNLocator
|
||||
|
||||
|
||||
def ensure_directory(directory):
|
||||
"""
|
||||
Ensure that a directory exists, creating it if necessary.
|
||||
|
||||
Parameters:
|
||||
directory (str): Path to the directory
|
||||
|
||||
Returns:
|
||||
str: Path to the directory
|
||||
"""
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
return directory
|
||||
|
||||
|
||||
def extract_height_from_path(path):
|
||||
"""
|
||||
Extract the initial height from a file path.
|
||||
|
||||
Parameters:
|
||||
path (str): Path to the file
|
||||
|
||||
Returns:
|
||||
float: Initial height in inches
|
||||
"""
|
||||
# Extract the filename without extension
|
||||
filename = os.path.basename(path).split('.')[0]
|
||||
|
||||
# Extract the height value
|
||||
for part in filename.split('_'):
|
||||
try:
|
||||
return float(part)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# If no height found, return None
|
||||
return None
|
||||
|
||||
|
||||
def smooth_data(data, window_size=5):
|
||||
"""
|
||||
Apply a simple moving average to smooth data.
|
||||
|
||||
Parameters:
|
||||
data (numpy.ndarray): Data to smooth
|
||||
window_size (int): Size of the moving average window
|
||||
|
||||
Returns:
|
||||
numpy.ndarray: Smoothed data
|
||||
"""
|
||||
return np.convolve(data, np.ones(window_size)/window_size, mode='valid')
|
||||
|
||||
|
||||
def calculate_statistics(values):
|
||||
"""
|
||||
Calculate basic statistics for a set of values.
|
||||
|
||||
Parameters:
|
||||
values (list or numpy.ndarray): Values to analyze
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing statistics
|
||||
"""
|
||||
values = np.array(values)
|
||||
return {
|
||||
'mean': np.mean(values),
|
||||
'median': np.median(values),
|
||||
'std': np.std(values),
|
||||
'min': np.min(values),
|
||||
'max': np.max(values),
|
||||
'count': len(values)
|
||||
}
|
||||
|
||||
|
||||
def format_statistics(stats):
|
||||
"""
|
||||
Format statistics as a string.
|
||||
|
||||
Parameters:
|
||||
stats (dict): Dictionary containing statistics
|
||||
|
||||
Returns:
|
||||
str: Formatted statistics string
|
||||
"""
|
||||
return (
|
||||
f"Mean: {stats['mean']:.4f}\n"
|
||||
f"Median: {stats['median']:.4f}\n"
|
||||
f"Std Dev: {stats['std']:.4f}\n"
|
||||
f"Min: {stats['min']:.4f}\n"
|
||||
f"Max: {stats['max']:.4f}\n"
|
||||
f"Count: {stats['count']}"
|
||||
)
|
||||
|
||||
|
||||
def create_bar_chart(data, x_label, y_label, title, integer_ticks=True):
|
||||
"""
|
||||
Create a bar chart from data.
|
||||
|
||||
Parameters:
|
||||
data (dict): Dictionary mapping categories to values
|
||||
x_label (str): Label for the x-axis
|
||||
y_label (str): Label for the y-axis
|
||||
title (str): Title for the chart
|
||||
integer_ticks (bool): Whether to use integer ticks on the y-axis
|
||||
|
||||
Returns:
|
||||
matplotlib.figure.Figure: The figure object
|
||||
"""
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
categories = list(data.keys())
|
||||
values = list(data.values())
|
||||
|
||||
bars = ax.bar(categories, values, color='skyblue', edgecolor='black')
|
||||
|
||||
# Add value labels on top of bars
|
||||
for bar in bars:
|
||||
height = bar.get_height()
|
||||
ax.text(
|
||||
bar.get_x() + bar.get_width() / 2.,
|
||||
height + 0.02 * max(values),
|
||||
f'{height:.3f}',
|
||||
ha='center',
|
||||
va='bottom',
|
||||
fontsize=10
|
||||
)
|
||||
|
||||
ax.set_xlabel(x_label, fontsize=12)
|
||||
ax.set_ylabel(y_label, fontsize=12)
|
||||
ax.set_title(title, fontsize=14, pad=15)
|
||||
|
||||
if integer_ticks:
|
||||
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def save_dataframe_to_csv(df, filename, index=False):
|
||||
"""
|
||||
Save a DataFrame to a CSV file.
|
||||
|
||||
Parameters:
|
||||
df (pandas.DataFrame): DataFrame to save
|
||||
filename (str): Path to the output file
|
||||
index (bool): Whether to include the index in the output
|
||||
|
||||
Returns:
|
||||
str: Path to the saved file
|
||||
"""
|
||||
# Ensure the directory exists
|
||||
directory = os.path.dirname(filename)
|
||||
if directory:
|
||||
ensure_directory(directory)
|
||||
|
||||
# Save the DataFrame
|
||||
df.to_csv(filename, index=index)
|
||||
|
||||
return filename
|
||||
96
src/utils/organize_files.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Script to organize data files into the new directory structure.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import glob
|
||||
|
||||
def create_data_directories():
|
||||
"""
|
||||
Create the necessary directories for data organization.
|
||||
"""
|
||||
# Create data directories
|
||||
os.makedirs("src/data/golf", exist_ok=True)
|
||||
os.makedirs("src/data/lacrosse", exist_ok=True)
|
||||
os.makedirs("src/data/metal", exist_ok=True)
|
||||
os.makedirs("src/data/images", exist_ok=True)
|
||||
|
||||
# Create output directory
|
||||
os.makedirs("output", exist_ok=True)
|
||||
|
||||
print("Created directory structure for data organization.")
|
||||
|
||||
|
||||
def move_data_files():
|
||||
"""
|
||||
Move data files to their appropriate directories.
|
||||
"""
|
||||
# Move golf ball data
|
||||
for file in glob.glob("golf_*.csv"):
|
||||
shutil.copy(file, os.path.join("src/data/golf", file))
|
||||
print(f"Copied {file} to src/data/golf/")
|
||||
|
||||
# Move lacrosse ball data
|
||||
for file in glob.glob("l_*.csv"):
|
||||
shutil.copy(file, os.path.join("src/data/lacrosse", file))
|
||||
print(f"Copied {file} to src/data/lacrosse/")
|
||||
|
||||
# Move metal ball data
|
||||
for file in glob.glob("metal_*.csv"):
|
||||
shutil.copy(file, os.path.join("src/data/metal", file))
|
||||
print(f"Copied {file} to src/data/metal/")
|
||||
|
||||
# Move image files
|
||||
for file in glob.glob("*.png"):
|
||||
shutil.copy(file, os.path.join("src/data/images", file))
|
||||
print(f"Copied {file} to src/data/images/")
|
||||
|
||||
|
||||
def move_script_files():
|
||||
"""
|
||||
Move script files to their appropriate directories.
|
||||
"""
|
||||
# Move original scripts to src directory
|
||||
if os.path.exists("lab2_part1.py"):
|
||||
shutil.copy("lab2_part1.py", os.path.join("src", "lab2_part1.py"))
|
||||
print(f"Copied lab2_part1.py to src/")
|
||||
|
||||
if os.path.exists("lab2_part2.py"):
|
||||
shutil.copy("lab2_part2.py", os.path.join("src", "lab2_part2.py"))
|
||||
print(f"Copied lab2_part2.py to src/")
|
||||
|
||||
# Move animated scripts to visualization directory
|
||||
if os.path.exists("lab2_part1_animated.py"):
|
||||
shutil.copy("lab2_part1_animated.py", os.path.join("src/visualization", "lab2_part1_animated.py"))
|
||||
print(f"Copied lab2_part1_animated.py to src/visualization/")
|
||||
|
||||
if os.path.exists("lab2_part2_animated.py"):
|
||||
shutil.copy("lab2_part2_animated.py", os.path.join("src/visualization", "lab2_part2_animated.py"))
|
||||
print(f"Copied lab2_part2_animated.py to src/visualization/")
|
||||
|
||||
if os.path.exists("animate_bouncing_balls.py"):
|
||||
shutil.copy("animate_bouncing_balls.py", os.path.join("src/visualization", "animate_bouncing_balls.py"))
|
||||
print(f"Copied animate_bouncing_balls.py to src/visualization/")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function to organize files.
|
||||
"""
|
||||
print("Starting file organization...")
|
||||
|
||||
# Create directories
|
||||
create_data_directories()
|
||||
|
||||
# Move data files
|
||||
move_data_files()
|
||||
|
||||
# Move script files
|
||||
move_script_files()
|
||||
|
||||
print("File organization complete!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
src/visualization/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Visualization package initialization
|
||||
271
src/visualization/animate_bouncing_balls.py
Normal file
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bouncing Ball Animation Launcher
|
||||
|
||||
This script provides a simple command-line interface to run different
|
||||
animated visualizations of bouncing ball data.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
def check_dependencies():
|
||||
"""Check if all required dependencies are installed."""
|
||||
required_packages = ['pandas', 'numpy', 'matplotlib', 'seaborn', 'scipy']
|
||||
missing_packages = []
|
||||
|
||||
for package in required_packages:
|
||||
try:
|
||||
importlib.import_module(package)
|
||||
except ImportError:
|
||||
missing_packages.append(package)
|
||||
|
||||
if missing_packages:
|
||||
print("Missing required packages:")
|
||||
for package in missing_packages:
|
||||
print(f" - {package}")
|
||||
print("\nPlease install them using:")
|
||||
print(f"pip install {' '.join(missing_packages)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def check_files():
|
||||
"""Check if the animation script files exist."""
|
||||
required_files = ['lab2_part1_animated.py', 'lab2_part2_animated.py']
|
||||
missing_files = []
|
||||
|
||||
for file in required_files:
|
||||
if not os.path.exists(file):
|
||||
missing_files.append(file)
|
||||
|
||||
if missing_files:
|
||||
print("Missing required script files:")
|
||||
for file in missing_files:
|
||||
print(f" - {file}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def check_data_files(ball_type=None):
|
||||
"""Check if the required data files exist."""
|
||||
if ball_type is None or ball_type.lower() == 'golf':
|
||||
golf_files = ["golf_11.csv", "golf_12.csv", "golf_13.csv", "golf_14.csv", "golf_15.csv"]
|
||||
for file in golf_files:
|
||||
if not os.path.exists(file):
|
||||
print(f"Warning: Golf ball data file '{file}' not found.")
|
||||
return False
|
||||
|
||||
if ball_type is None or ball_type.lower() == 'lacrosse':
|
||||
lacrosse_files = ["l_18.csv", "l_19.csv", "l_20.csv", "l_21.csv", "l_22.csv"]
|
||||
for file in lacrosse_files:
|
||||
if not os.path.exists(file):
|
||||
print(f"Warning: Lacrosse ball data file '{file}' not found.")
|
||||
return False
|
||||
|
||||
if ball_type is None or ball_type.lower() == 'metal':
|
||||
metal_files = ["metal_2.csv", "metal_4.csv", "metal_6.csv", "metal_8.csv", "metal_10.csv"]
|
||||
for file in metal_files:
|
||||
if not os.path.exists(file):
|
||||
print(f"Warning: Metal ball data file '{file}' not found.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def run_animation(animation_type, ball_type=None, save=False):
|
||||
"""Run the selected animation."""
|
||||
if animation_type == 'position':
|
||||
# Import the position vs. time animation script
|
||||
spec = importlib.util.spec_from_file_location("lab2_part1_animated", "lab2_part1_animated.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Run the animation
|
||||
if ball_type:
|
||||
if ball_type.lower() == 'golf':
|
||||
print("Running position vs. time animation for Golf balls...")
|
||||
module.animate_position_vs_time(module.golf_file_paths, "Golf Ball", save)
|
||||
elif ball_type.lower() == 'lacrosse':
|
||||
print("Running position vs. time animation for Lacrosse balls...")
|
||||
module.animate_position_vs_time(module.lacrosse_file_paths, "Lacrosse Ball", save)
|
||||
elif ball_type.lower() == 'metal':
|
||||
print("Running position vs. time animation for Metal balls...")
|
||||
module.animate_position_vs_time(module.metal_file_paths, "Metal Ball", save)
|
||||
else:
|
||||
print(f"Unknown ball type: {ball_type}")
|
||||
print("Available ball types: golf, lacrosse, metal")
|
||||
else:
|
||||
print("Running position vs. time animations for all ball types...")
|
||||
module.animate_all_ball_types(save)
|
||||
|
||||
elif animation_type == 'bounce':
|
||||
# Import the bouncing ball animation script
|
||||
spec = importlib.util.spec_from_file_location("lab2_part2_animated", "lab2_part2_animated.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Set save_animations flag
|
||||
module.save_animations = save
|
||||
|
||||
# Run the animation
|
||||
if ball_type:
|
||||
if ball_type.lower() == 'golf':
|
||||
print("Running bouncing ball animation for Golf balls...")
|
||||
module.golf_results = module.process_and_animate_all(
|
||||
module.golf_file_paths,
|
||||
ball_type='Golf',
|
||||
min_distance=module.min_distance,
|
||||
min_time_diff=module.min_time_diff,
|
||||
relative_prominence=module.golf_relative_prominence,
|
||||
low_height_threshold=module.golf_low_threshold,
|
||||
low_height_adjustment=module.golf_low_adjustment,
|
||||
high_height_threshold=module.golf_high_threshold,
|
||||
high_height_adjustment=module.golf_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=module.fs,
|
||||
save_animations=save
|
||||
)
|
||||
module.plot_cor_table(module.golf_results, 'Golf')
|
||||
elif ball_type.lower() == 'lacrosse':
|
||||
print("Running bouncing ball animation for Lacrosse balls...")
|
||||
module.lacrosse_results = module.process_and_animate_all(
|
||||
module.lacrosse_file_paths,
|
||||
ball_type='Lacrosse',
|
||||
min_distance=module.min_distance,
|
||||
min_time_diff=module.min_time_diff,
|
||||
relative_prominence=module.lacrosse_relative_prominence,
|
||||
low_height_threshold=module.lacrosse_low_threshold,
|
||||
low_height_adjustment=module.lacrosse_low_adjustment,
|
||||
high_height_threshold=module.lacrosse_high_threshold,
|
||||
high_height_adjustment=module.lacrosse_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=module.fs,
|
||||
save_animations=save
|
||||
)
|
||||
module.plot_cor_table(module.lacrosse_results, 'Lacrosse')
|
||||
elif ball_type.lower() == 'metal':
|
||||
print("Running bouncing ball animation for Metal balls...")
|
||||
module.metal_results = module.process_and_animate_all(
|
||||
module.metal_file_paths,
|
||||
ball_type='Metal',
|
||||
min_distance=module.min_distance,
|
||||
min_time_diff=module.min_time_diff,
|
||||
relative_prominence=module.metal_relative_prominence,
|
||||
low_height_threshold=module.metal_low_threshold,
|
||||
low_height_adjustment=module.metal_low_adjustment,
|
||||
high_height_threshold=module.metal_high_threshold,
|
||||
high_height_adjustment=module.metal_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=module.fs,
|
||||
save_animations=save
|
||||
)
|
||||
module.plot_cor_table(module.metal_results, 'Metal')
|
||||
else:
|
||||
print(f"Unknown ball type: {ball_type}")
|
||||
print("Available ball types: golf, lacrosse, metal")
|
||||
else:
|
||||
# Run the animations for all ball types one by one
|
||||
print("Running bouncing ball animations for all ball types...")
|
||||
|
||||
# Golf balls
|
||||
print("\nProcessing and animating Golf Ball trials...")
|
||||
module.golf_results = module.process_and_animate_all(
|
||||
module.golf_file_paths,
|
||||
ball_type='Golf',
|
||||
min_distance=module.min_distance,
|
||||
min_time_diff=module.min_time_diff,
|
||||
relative_prominence=module.golf_relative_prominence,
|
||||
low_height_threshold=module.golf_low_threshold,
|
||||
low_height_adjustment=module.golf_low_adjustment,
|
||||
high_height_threshold=module.golf_high_threshold,
|
||||
high_height_adjustment=module.golf_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=module.fs,
|
||||
save_animations=save
|
||||
)
|
||||
|
||||
# Lacrosse balls
|
||||
print("\nProcessing and animating Lacrosse Ball trials...")
|
||||
module.lacrosse_results = module.process_and_animate_all(
|
||||
module.lacrosse_file_paths,
|
||||
ball_type='Lacrosse',
|
||||
min_distance=module.min_distance,
|
||||
min_time_diff=module.min_time_diff,
|
||||
relative_prominence=module.lacrosse_relative_prominence,
|
||||
low_height_threshold=module.lacrosse_low_threshold,
|
||||
low_height_adjustment=module.lacrosse_low_adjustment,
|
||||
high_height_threshold=module.lacrosse_high_threshold,
|
||||
high_height_adjustment=module.lacrosse_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=module.fs,
|
||||
save_animations=save
|
||||
)
|
||||
|
||||
# Metal balls
|
||||
print("\nProcessing and animating Metal Ball trials...")
|
||||
module.metal_results = module.process_and_animate_all(
|
||||
module.metal_file_paths,
|
||||
ball_type='Metal',
|
||||
min_distance=module.min_distance,
|
||||
min_time_diff=module.min_time_diff,
|
||||
relative_prominence=module.metal_relative_prominence,
|
||||
low_height_threshold=module.metal_low_threshold,
|
||||
low_height_adjustment=module.metal_low_adjustment,
|
||||
high_height_threshold=module.metal_high_threshold,
|
||||
high_height_adjustment=module.metal_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=module.fs,
|
||||
save_animations=save
|
||||
)
|
||||
|
||||
# Print summary tables
|
||||
print("\nGolf Ball COR Table:")
|
||||
print(module.golf_results[['Trial', 'Initial Height', 'Average COR']])
|
||||
print("\nLacrosse Ball COR Table:")
|
||||
print(module.lacrosse_results[['Trial', 'Initial Height', 'Average COR']])
|
||||
print("\nMetal Ball COR Table:")
|
||||
print(module.metal_results[['Trial', 'Initial Height', 'Average COR']])
|
||||
|
||||
# Plot COR vs. initial height for each ball type
|
||||
module.plot_cor_table(module.golf_results, 'Golf')
|
||||
module.plot_cor_table(module.lacrosse_results, 'Lacrosse')
|
||||
module.plot_cor_table(module.metal_results, 'Metal')
|
||||
|
||||
else:
|
||||
print(f"Unknown animation type: {animation_type}")
|
||||
print("Available animation types: position, bounce")
|
||||
|
||||
def main():
|
||||
"""Main function to parse arguments and run the selected animation."""
|
||||
parser = argparse.ArgumentParser(description='Run bouncing ball animations.')
|
||||
parser.add_argument('animation_type', choices=['position', 'bounce'],
|
||||
help='Type of animation to run: position (for position vs. time) or bounce (for bouncing ball physics)')
|
||||
parser.add_argument('--ball', choices=['golf', 'lacrosse', 'metal'],
|
||||
help='Type of ball to animate (default: all)')
|
||||
parser.add_argument('--save', action='store_true',
|
||||
help='Save animations as GIF files')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check dependencies and files
|
||||
if not check_dependencies():
|
||||
print("Please install the required dependencies and try again.")
|
||||
sys.exit(1)
|
||||
|
||||
if not check_files():
|
||||
print("Please ensure the animation script files are in the current directory and try again.")
|
||||
sys.exit(1)
|
||||
|
||||
if not check_data_files(args.ball):
|
||||
print("Warning: Some data files are missing. The animations may not work correctly.")
|
||||
response = input("Do you want to continue anyway? (y/n): ")
|
||||
if response.lower() != 'y':
|
||||
sys.exit(1)
|
||||
|
||||
# Run the selected animation
|
||||
run_animation(args.animation_type, args.ball, args.save)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
287
src/visualization/animation.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
Animation module for visualizing bouncing ball data.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
from matplotlib.patches import Circle
|
||||
import pandas as pd
|
||||
from IPython.display import HTML
|
||||
|
||||
|
||||
def create_ball_animation(df, ball_radius=0.5, fps=30, duration=None, title="Bouncing Ball Animation"):
|
||||
"""
|
||||
Create an animation of a bouncing ball from position data.
|
||||
|
||||
Parameters:
|
||||
df (pandas.DataFrame): DataFrame with 'Time' and 'Position' columns
|
||||
ball_radius (float): Radius of the ball in inches
|
||||
fps (int): Frames per second for the animation
|
||||
duration (float): Duration of the animation in seconds (if None, uses the full data)
|
||||
title (str): Title for the animation
|
||||
|
||||
Returns:
|
||||
matplotlib.animation.Animation: The animation object
|
||||
"""
|
||||
# Extract time and position data
|
||||
time_data = df['Time'].values
|
||||
position_data = df['Position'].values
|
||||
|
||||
# Determine animation duration
|
||||
if duration is None:
|
||||
duration = time_data[-1]
|
||||
|
||||
# Calculate the number of frames based on fps and duration
|
||||
num_frames = int(fps * duration)
|
||||
|
||||
# Create figure and axis
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
# Set axis limits
|
||||
max_height = np.max(position_data) + 2 * ball_radius
|
||||
ax.set_xlim(-5, 5)
|
||||
ax.set_ylim(0, max_height)
|
||||
|
||||
# Set labels and title
|
||||
ax.set_xlabel('X Position (inches)')
|
||||
ax.set_ylabel('Y Position (inches)')
|
||||
ax.set_title(title)
|
||||
|
||||
# Add a horizontal line at y=0 to represent the ground
|
||||
ax.axhline(y=0, color='black', linestyle='-', linewidth=2)
|
||||
|
||||
# Create the ball (circle patch)
|
||||
ball = Circle((0, position_data[0]), ball_radius, color='red', zorder=2)
|
||||
ax.add_patch(ball)
|
||||
|
||||
# Create a line to show the ball's path
|
||||
line, = ax.plot([], [], 'b-', alpha=0.3, linewidth=1)
|
||||
|
||||
# Initialize empty lists for the path
|
||||
path_x = []
|
||||
path_y = []
|
||||
|
||||
def init():
|
||||
"""Initialize the animation"""
|
||||
ball.center = (0, position_data[0])
|
||||
line.set_data([], [])
|
||||
return ball, line
|
||||
|
||||
def animate(i):
|
||||
"""Update the animation for frame i"""
|
||||
# Calculate the current time based on the frame number
|
||||
current_time = i * duration / num_frames
|
||||
|
||||
# Find the closest time index in our data
|
||||
idx = np.abs(time_data - current_time).argmin()
|
||||
|
||||
# Update ball position
|
||||
y_pos = position_data[idx]
|
||||
ball.center = (0, y_pos)
|
||||
|
||||
# Update path
|
||||
path_x.append(0)
|
||||
path_y.append(y_pos)
|
||||
line.set_data(path_x, path_y)
|
||||
|
||||
return ball, line
|
||||
|
||||
# Create the animation
|
||||
anim = animation.FuncAnimation(
|
||||
fig, animate, init_func=init, frames=num_frames,
|
||||
interval=1000/fps, blit=True
|
||||
)
|
||||
|
||||
plt.close() # Prevent the static plot from displaying
|
||||
|
||||
return anim
|
||||
|
||||
|
||||
def create_ball_animation_with_model(df, peak_indices, cor, initial_height,
|
||||
ball_radius=0.5, fps=30, duration=None,
|
||||
title="Bouncing Ball Animation with Model",
|
||||
g=386.09):
|
||||
"""
|
||||
Create an animation of a bouncing ball from position data with an ideal model comparison.
|
||||
|
||||
Parameters:
|
||||
df (pandas.DataFrame): DataFrame with 'Time' and 'Position' columns
|
||||
peak_indices (numpy.ndarray): Indices of detected bounces
|
||||
cor (float): Coefficient of Restitution
|
||||
initial_height (float): Initial height of the ball in inches
|
||||
ball_radius (float): Radius of the ball in inches
|
||||
fps (int): Frames per second for the animation
|
||||
duration (float): Duration of the animation in seconds (if None, uses the full data)
|
||||
title (str): Title for the animation
|
||||
g (float): Acceleration due to gravity (in inches/s²)
|
||||
|
||||
Returns:
|
||||
matplotlib.animation.Animation: The animation object
|
||||
"""
|
||||
# Extract time and position data
|
||||
time_data = df['Time'].values
|
||||
position_data = df['Position'].values
|
||||
|
||||
# Determine animation duration
|
||||
if duration is None:
|
||||
duration = time_data[-1]
|
||||
|
||||
# Calculate the number of frames based on fps and duration
|
||||
num_frames = int(fps * duration)
|
||||
|
||||
# Create figure and axis
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
# Set axis limits
|
||||
max_height = np.max(position_data) + 2 * ball_radius
|
||||
ax.set_xlim(-5, 5)
|
||||
ax.set_ylim(0, max_height)
|
||||
|
||||
# Set labels and title
|
||||
ax.set_xlabel('X Position (inches)')
|
||||
ax.set_ylabel('Y Position (inches)')
|
||||
ax.set_title(title)
|
||||
|
||||
# Add a horizontal line at y=0 to represent the ground
|
||||
ax.axhline(y=0, color='black', linestyle='-', linewidth=2)
|
||||
|
||||
# Create the real data ball (circle patch)
|
||||
real_ball = Circle((0, position_data[0]), ball_radius, color='red', zorder=2, label='Actual')
|
||||
ax.add_patch(real_ball)
|
||||
|
||||
# Create the model ball (circle patch)
|
||||
model_ball = Circle((2, initial_height), ball_radius, color='blue', zorder=2, alpha=0.7, label='Model')
|
||||
ax.add_patch(model_ball)
|
||||
|
||||
# Create lines to show the balls' paths
|
||||
real_line, = ax.plot([], [], 'r-', alpha=0.3, linewidth=1)
|
||||
model_line, = ax.plot([], [], 'b-', alpha=0.3, linewidth=1)
|
||||
|
||||
# Add legend
|
||||
ax.legend(handles=[
|
||||
plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='red', markersize=10, label='Actual'),
|
||||
plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='blue', markersize=10, label='Model')
|
||||
])
|
||||
|
||||
# Initialize empty lists for the paths
|
||||
real_path_x = []
|
||||
real_path_y = []
|
||||
model_path_x = []
|
||||
model_path_y = []
|
||||
|
||||
# Generate model data
|
||||
dt = 0.001 # Time step for simulation
|
||||
model_time = []
|
||||
model_position = []
|
||||
|
||||
# Initialize simulation parameters
|
||||
y = initial_height
|
||||
v_y = 0.0 # Starting from rest
|
||||
t = 0.0
|
||||
|
||||
# Run simulation for the duration
|
||||
while t <= duration:
|
||||
model_time.append(t)
|
||||
model_position.append(y)
|
||||
|
||||
# Kinematic update
|
||||
v_y -= g * dt
|
||||
y += v_y * dt
|
||||
t += dt
|
||||
|
||||
# Bounce condition
|
||||
if y <= 0:
|
||||
y = 0 # Ground impact
|
||||
v_y = -v_y * cor # Apply COR
|
||||
|
||||
# Convert to numpy arrays for faster indexing
|
||||
model_time = np.array(model_time)
|
||||
model_position = np.array(model_position)
|
||||
|
||||
def init():
|
||||
"""Initialize the animation"""
|
||||
real_ball.center = (0, position_data[0])
|
||||
model_ball.center = (2, initial_height)
|
||||
real_line.set_data([], [])
|
||||
model_line.set_data([], [])
|
||||
return real_ball, model_ball, real_line, model_line
|
||||
|
||||
def animate(i):
|
||||
"""Update the animation for frame i"""
|
||||
# Calculate the current time based on the frame number
|
||||
current_time = i * duration / num_frames
|
||||
|
||||
# Find the closest time index in our data
|
||||
real_idx = np.abs(time_data - current_time).argmin()
|
||||
model_idx = np.abs(model_time - current_time).argmin()
|
||||
|
||||
# Update ball positions
|
||||
real_y_pos = position_data[real_idx]
|
||||
model_y_pos = model_position[model_idx]
|
||||
|
||||
real_ball.center = (0, real_y_pos)
|
||||
model_ball.center = (2, model_y_pos)
|
||||
|
||||
# Update paths
|
||||
real_path_x.append(0)
|
||||
real_path_y.append(real_y_pos)
|
||||
model_path_x.append(2)
|
||||
model_path_y.append(model_y_pos)
|
||||
|
||||
real_line.set_data(real_path_x, real_path_y)
|
||||
model_line.set_data(model_path_x, model_path_y)
|
||||
|
||||
return real_ball, model_ball, real_line, model_line
|
||||
|
||||
# Create the animation
|
||||
anim = animation.FuncAnimation(
|
||||
fig, animate, init_func=init, frames=num_frames,
|
||||
interval=1000/fps, blit=True
|
||||
)
|
||||
|
||||
plt.close() # Prevent the static plot from displaying
|
||||
|
||||
return anim
|
||||
|
||||
|
||||
def display_animation(anim, html_video=True):
|
||||
"""
|
||||
Display an animation in a Jupyter notebook.
|
||||
|
||||
Parameters:
|
||||
anim (matplotlib.animation.Animation): The animation to display
|
||||
html_video (bool): If True, convert to HTML5 video, otherwise use JavaScript
|
||||
|
||||
Returns:
|
||||
IPython.display.HTML: The HTML object containing the animation
|
||||
"""
|
||||
if html_video:
|
||||
# Convert to HTML5 video
|
||||
html = anim.to_html5_video()
|
||||
return HTML(html)
|
||||
else:
|
||||
# Use JavaScript animation
|
||||
return HTML(anim.to_jshtml())
|
||||
|
||||
|
||||
def save_animation(anim, filename, fps=30, dpi=100):
|
||||
"""
|
||||
Save an animation to a file.
|
||||
|
||||
Parameters:
|
||||
anim (matplotlib.animation.Animation): The animation to save
|
||||
filename (str): The filename to save to (should end with .mp4, .gif, etc.)
|
||||
fps (int): Frames per second
|
||||
dpi (int): Resolution in dots per inch
|
||||
"""
|
||||
# Determine the writer based on the file extension
|
||||
if filename.endswith('.mp4'):
|
||||
writer = animation.FFMpegWriter(fps=fps)
|
||||
elif filename.endswith('.gif'):
|
||||
writer = animation.PillowWriter(fps=fps)
|
||||
else:
|
||||
raise ValueError("Unsupported file format. Use .mp4 or .gif")
|
||||
|
||||
# Save the animation
|
||||
anim.save(filename, writer=writer, dpi=dpi)
|
||||
122
src/visualization/lab2_part1_animated.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
import seaborn as sns
|
||||
from matplotlib.animation import PillowWriter
|
||||
|
||||
sns.set_theme(style="whitegrid", palette="muted")
|
||||
|
||||
# File paths for golf ball data
|
||||
golf_file_paths = {
|
||||
"11 inches": "golf_11.csv",
|
||||
"12 inches": "golf_12.csv",
|
||||
"13 inches": "golf_13.csv",
|
||||
"14 inches": "golf_14.csv",
|
||||
"15 inches": "golf_15.csv"
|
||||
}
|
||||
|
||||
# File paths for lacrosse ball data
|
||||
lacrosse_file_paths = {
|
||||
"18 inches": "l_18.csv",
|
||||
"19 inches": "l_19.csv",
|
||||
"20 inches": "l_20.csv",
|
||||
"21 inches": "l_21.csv",
|
||||
"22 inches": "l_22.csv"
|
||||
}
|
||||
|
||||
# File paths for metal ball data
|
||||
metal_file_paths = {
|
||||
"2 inches": "metal_2.csv",
|
||||
"4 inches": "metal_4.csv",
|
||||
"6 inches": "metal_6.csv",
|
||||
"8 inches": "metal_8.csv",
|
||||
"10 inches": "metal_10.csv"
|
||||
}
|
||||
|
||||
def animate_position_vs_time(file_paths, ball_type, save_animation=False):
|
||||
"""
|
||||
Creates an animated plot showing the position vs time data for each trial.
|
||||
The animation shows the data being drawn progressively over time.
|
||||
"""
|
||||
for label, path in file_paths.items():
|
||||
# Read CSV with no header, assuming columns: Time, Position
|
||||
df = pd.read_csv(path, header=None, names=["Time", "Position"])
|
||||
|
||||
# Convert to numpy arrays for better performance and to avoid indexing issues
|
||||
time_data = df["Time"].to_numpy()
|
||||
position_data = df["Position"].to_numpy()
|
||||
|
||||
# Create figure and axis
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
# Set up the plot
|
||||
ax.set_xlabel("Time (seconds)", fontsize=14)
|
||||
ax.set_ylabel("Position (inches)", fontsize=14)
|
||||
ax.set_title(f"{ball_type} - Position vs. Time\nInitial Height: {label}", fontsize=16, pad=15)
|
||||
ax.grid(True, which='both', linestyle='--', linewidth=0.5, alpha=0.7)
|
||||
|
||||
# Set axis limits
|
||||
ax.set_xlim(time_data.min(), time_data.max())
|
||||
ax.set_ylim(0, position_data.max() * 1.1)
|
||||
|
||||
# Create a line object with no data
|
||||
line, = ax.plot([], [], 'o-', markersize=3, linewidth=2, label=label)
|
||||
|
||||
# Create a point that will move along the line
|
||||
point, = ax.plot([], [], 'ro', markersize=8)
|
||||
|
||||
# Add legend
|
||||
ax.legend(fontsize=12)
|
||||
|
||||
# Animation initialization function
|
||||
def init():
|
||||
line.set_data([], [])
|
||||
point.set_data([], [])
|
||||
return line, point
|
||||
|
||||
# Animation update function
|
||||
def update(frame):
|
||||
# For smooth animation, use a percentage of the data
|
||||
idx = int(len(time_data) * frame / 100) if frame < 100 else len(time_data) - 1
|
||||
|
||||
# Update line data
|
||||
line.set_data(time_data[:idx+1], position_data[:idx+1])
|
||||
|
||||
# Update point position - ensure we're passing arrays/lists, not single values
|
||||
if idx > 0:
|
||||
point.set_data([time_data[idx]], [position_data[idx]])
|
||||
|
||||
return line, point
|
||||
|
||||
# Create animation
|
||||
ani = animation.FuncAnimation(
|
||||
fig, update, frames=120, init_func=init,
|
||||
blit=True, interval=50, repeat=True
|
||||
)
|
||||
|
||||
# Save animation if requested
|
||||
if save_animation:
|
||||
writer = PillowWriter(fps=30)
|
||||
filename = f"{ball_type.lower().replace(' ', '_')}_{label.replace(' ', '_')}.gif"
|
||||
ani.save(filename, writer=writer)
|
||||
print(f"Animation saved as {filename}")
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# Function to animate all ball types
|
||||
def animate_all_ball_types(save_animations=False):
|
||||
print("Animating Golf Ball data...")
|
||||
animate_position_vs_time(golf_file_paths, "Golf Ball", save_animations)
|
||||
|
||||
print("Animating Lacrosse Ball data...")
|
||||
animate_position_vs_time(lacrosse_file_paths, "Lacrosse Ball", save_animations)
|
||||
|
||||
print("Animating Metal Ball data...")
|
||||
animate_position_vs_time(metal_file_paths, "Metal Ball", save_animations)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Set to True if you want to save the animations as GIF files
|
||||
save_animations = False
|
||||
animate_all_ball_types(save_animations)
|
||||
484
src/visualization/lab2_part2_animated.py
Normal file
@@ -0,0 +1,484 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
import seaborn as sns
|
||||
from scipy.signal import find_peaks
|
||||
from matplotlib.animation import PillowWriter
|
||||
|
||||
sns.set_theme(style="whitegrid", palette="muted")
|
||||
|
||||
# File paths for golf ball data
|
||||
golf_file_paths = {
|
||||
"11 inches": "golf_11.csv",
|
||||
"12 inches": "golf_12.csv",
|
||||
"13 inches": "golf_13.csv",
|
||||
"14 inches": "golf_14.csv",
|
||||
"15 inches": "golf_15.csv"
|
||||
}
|
||||
|
||||
# File paths for lacrosse ball data
|
||||
lacrosse_file_paths = {
|
||||
"18 inches": "l_18.csv",
|
||||
"19 inches": "l_19.csv",
|
||||
"20 inches": "l_20.csv",
|
||||
"21 inches": "l_21.csv",
|
||||
"22 inches": "l_22.csv"
|
||||
}
|
||||
|
||||
# File paths for metal ball data
|
||||
metal_file_paths = {
|
||||
"2 inches": "metal_2.csv",
|
||||
"4 inches": "metal_4.csv",
|
||||
"6 inches": "metal_6.csv",
|
||||
"8 inches": "metal_8.csv",
|
||||
"10 inches": "metal_10.csv"
|
||||
}
|
||||
|
||||
# General detection/plotting parameters
|
||||
min_distance = 5
|
||||
min_time_diff = 0.05
|
||||
fs = 50
|
||||
save_animations = False # Set to True to save animations as GIFs
|
||||
|
||||
# Prominence adjustments
|
||||
golf_relative_prominence = 0.15
|
||||
golf_low_threshold = 12.5
|
||||
golf_low_adjustment = 1.1
|
||||
golf_high_threshold = 14.5
|
||||
golf_high_adjustment = 1.3
|
||||
|
||||
lacrosse_relative_prominence = 0.05
|
||||
lacrosse_low_threshold = 18.5
|
||||
lacrosse_low_adjustment = 1.1
|
||||
lacrosse_high_threshold = 21.5
|
||||
lacrosse_high_adjustment = 1.3
|
||||
|
||||
metal_relative_prominence = 0.05
|
||||
metal_low_threshold = 5
|
||||
metal_low_adjustment = 0.8
|
||||
metal_high_threshold = 8
|
||||
metal_high_adjustment = 1.2
|
||||
|
||||
def read_trial(path):
|
||||
"""
|
||||
Reads a CSV file containing time and position data (with no header)
|
||||
and returns a DataFrame with columns 'Time' and 'Position'.
|
||||
"""
|
||||
return pd.read_csv(path, header=None, names=['Time', 'Position'])
|
||||
|
||||
|
||||
def detect_bounces(df,
|
||||
prominence=0,
|
||||
min_distance=10,
|
||||
min_time_diff=0.2,
|
||||
max_bounces=7,
|
||||
fs=None):
|
||||
"""
|
||||
Detects bounce events by locating peaks in the signal.
|
||||
|
||||
Parameters:
|
||||
- df: DataFrame with 'Time' and 'Position'
|
||||
- prominence: required prominence for peaks (in the signal)
|
||||
- min_distance: minimum number of data points between peaks
|
||||
- min_time_diff: minimum time difference (seconds) between bounces
|
||||
- max_bounces: maximum bounces to report
|
||||
- fs: override sampling frequency (Hz). If None, computed from 'Time'.
|
||||
|
||||
Returns:
|
||||
- peak_indices (array of indices),
|
||||
- bounce_heights (array of position values at these bounces),
|
||||
- bounce_times (array of times of these bounces),
|
||||
- signal_data (the data used for detection, here the raw Position values)
|
||||
"""
|
||||
if fs is None:
|
||||
dt = np.median(np.diff(df['Time']))
|
||||
if dt <= 0:
|
||||
raise ValueError(
|
||||
"Time differences are non-positive. "
|
||||
"Ensure your 'Time' column is in seconds."
|
||||
)
|
||||
fs = 1 / dt
|
||||
|
||||
signal_data = df['Position'].values
|
||||
|
||||
peaks, _ = find_peaks(signal_data, prominence=prominence, distance=min_distance)
|
||||
|
||||
filtered_peaks = []
|
||||
filtered_times = []
|
||||
group_start_time = None
|
||||
group_best_idx = None
|
||||
group_best_value = None
|
||||
|
||||
for idx in peaks:
|
||||
current_time = df['Time'].iloc[idx]
|
||||
current_value = signal_data[idx]
|
||||
|
||||
if group_start_time is None:
|
||||
group_start_time = current_time
|
||||
group_best_idx = idx
|
||||
group_best_value = current_value
|
||||
continue
|
||||
|
||||
if (current_time - group_start_time) < min_time_diff:
|
||||
if current_value > group_best_value:
|
||||
group_best_idx = idx
|
||||
group_best_value = current_value
|
||||
else:
|
||||
filtered_peaks.append(group_best_idx)
|
||||
filtered_times.append(df['Time'].iloc[group_best_idx])
|
||||
|
||||
group_start_time = current_time
|
||||
group_best_idx = idx
|
||||
group_best_value = current_value
|
||||
|
||||
if group_best_idx is not None:
|
||||
filtered_peaks.append(group_best_idx)
|
||||
filtered_times.append(df['Time'].iloc[group_best_idx])
|
||||
|
||||
filtered_peaks = filtered_peaks[:max_bounces]
|
||||
filtered_times = filtered_times[:max_bounces]
|
||||
|
||||
bounce_heights = df['Position'].iloc[filtered_peaks].values
|
||||
bounce_times = np.array(filtered_times)
|
||||
|
||||
return np.array(filtered_peaks), bounce_heights, bounce_times, signal_data
|
||||
|
||||
|
||||
def compute_cor(bounce_heights):
|
||||
"""
|
||||
Computes the Coefficient of Restitution (COR) for consecutive bounces:
|
||||
e = sqrt( h_{n+1} / h_n )
|
||||
|
||||
Returns an array of COR values.
|
||||
"""
|
||||
cor_values = []
|
||||
for i in range(len(bounce_heights) - 1):
|
||||
if bounce_heights[i] > 0:
|
||||
cor_values.append(np.sqrt(bounce_heights[i+1] / bounce_heights[i]))
|
||||
else:
|
||||
cor_values.append(np.nan)
|
||||
return np.array(cor_values)
|
||||
|
||||
|
||||
def process_trial(path,
|
||||
prominence,
|
||||
min_distance=10,
|
||||
min_time_diff=0.2,
|
||||
max_bounces=7,
|
||||
fs=None):
|
||||
"""
|
||||
Reads trial data, detects bounces, computes COR values, and returns:
|
||||
- df (the DataFrame)
|
||||
- peak_indices
|
||||
- bounce_heights
|
||||
- bounce_times
|
||||
- cor_values
|
||||
- avg_cor
|
||||
- signal_data
|
||||
"""
|
||||
df = read_trial(path)
|
||||
peak_indices, bounce_heights, bounce_times, signal_data = detect_bounces(
|
||||
df, prominence, min_distance, min_time_diff, max_bounces, fs
|
||||
)
|
||||
cor_values = compute_cor(bounce_heights)
|
||||
avg_cor = np.mean(cor_values) if cor_values.size > 0 else np.nan
|
||||
|
||||
return df, peak_indices, bounce_heights, bounce_times, cor_values, avg_cor, signal_data
|
||||
|
||||
|
||||
def animate_bouncing_ball(df, peak_indices, bounce_heights, bounce_times, cor,
|
||||
label, ball_type, initial_height=None, g=386.09,
|
||||
save_animation=False):
|
||||
"""
|
||||
Creates an animated visualization of a bouncing ball using the detected bounce data.
|
||||
|
||||
Parameters:
|
||||
df: DataFrame with Time and Position data
|
||||
peak_indices: Indices of detected bounces
|
||||
bounce_heights: Heights of detected bounces
|
||||
bounce_times: Times of detected bounces
|
||||
cor: Coefficient of Restitution
|
||||
label: Trial label (e.g., "11 inches")
|
||||
ball_type: Type of ball (e.g., "Golf")
|
||||
initial_height: Initial drop height
|
||||
g: Acceleration due to gravity (in inches/s²)
|
||||
save_animation: Whether to save the animation as a GIF
|
||||
"""
|
||||
# Convert DataFrame to numpy arrays for better performance
|
||||
time_data = df['Time'].to_numpy()
|
||||
position_data = df['Position'].to_numpy()
|
||||
|
||||
# Create figure and axis
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
# Set up the plot
|
||||
ax.set_xlabel("Time (s)", fontsize=14)
|
||||
ax.set_ylabel("Position (inches)", fontsize=14)
|
||||
ax.set_title(f"{ball_type} Ball Bounce - Initial Height: {label}\nAvg. COR: {cor:.3f}",
|
||||
fontsize=16, pad=15)
|
||||
|
||||
# Set axis limits
|
||||
time_range = time_data.max() - time_data.min()
|
||||
ax.set_xlim(time_data.min() - 0.05 * time_range, time_data.max() + 0.05 * time_range)
|
||||
ax.set_ylim(-0.5, max(position_data.max(), initial_height if initial_height else 0) * 1.1)
|
||||
|
||||
# Plot the raw data
|
||||
ax.plot(time_data, position_data, 'b-', alpha=0.3, linewidth=1, label='Raw Data')
|
||||
|
||||
# Mark the detected bounces
|
||||
if len(peak_indices) > 0:
|
||||
bounce_times_array = df['Time'].iloc[peak_indices].to_numpy()
|
||||
bounce_heights_array = df['Position'].iloc[peak_indices].to_numpy()
|
||||
ax.plot(
|
||||
bounce_times_array,
|
||||
bounce_heights_array,
|
||||
'gx',
|
||||
markersize=10,
|
||||
label='Detected Bounces'
|
||||
)
|
||||
|
||||
# Create a ball object (circle)
|
||||
ball_radius = initial_height/20 if initial_height else 0.5
|
||||
ball = plt.Circle((time_data[0], position_data[0]), radius=ball_radius,
|
||||
color='red', fill=True, alpha=0.7)
|
||||
ax.add_patch(ball)
|
||||
|
||||
# Create a ground line
|
||||
ax.axhline(y=0, color='k', linestyle='-', linewidth=2)
|
||||
|
||||
# Add legend
|
||||
ax.legend(fontsize=12, loc='upper right')
|
||||
|
||||
# Add grid
|
||||
ax.grid(True, which='both', linestyle='--', linewidth=0.5, alpha=0.7)
|
||||
|
||||
# Generate simulated trajectory data
|
||||
sim_line = None
|
||||
sim_time = None
|
||||
sim_pos = None
|
||||
|
||||
if initial_height is not None and cor is not None and not np.isnan(cor):
|
||||
# Create time points for simulation
|
||||
sim_dt = 0.001 # Simulation time step
|
||||
sim_time = np.arange(time_data.min(), time_data.max(), sim_dt)
|
||||
sim_pos = np.zeros_like(sim_time)
|
||||
|
||||
# Initialize simulation parameters
|
||||
y = initial_height
|
||||
v_y = 0.0 # Starting from rest
|
||||
bounce_count = 0
|
||||
|
||||
# Align first bounce with data
|
||||
time_offset = 0
|
||||
if len(bounce_times) > 0:
|
||||
# Calculate time to first bounce
|
||||
t_to_first_bounce = np.sqrt(2 * initial_height / g)
|
||||
time_offset = bounce_times[0] - t_to_first_bounce
|
||||
|
||||
# Simulate the bouncing motion
|
||||
for i in range(len(sim_time)):
|
||||
t = sim_time[i] - time_offset
|
||||
|
||||
# Reset if before simulation start
|
||||
if t < 0:
|
||||
sim_pos[i] = initial_height
|
||||
continue
|
||||
|
||||
# Apply physics
|
||||
y += v_y * sim_dt
|
||||
v_y -= g * sim_dt
|
||||
|
||||
# Check for bounce
|
||||
if y <= 0:
|
||||
y = 0
|
||||
v_y = -v_y * cor
|
||||
bounce_count += 1
|
||||
|
||||
sim_pos[i] = y
|
||||
|
||||
# Plot the simulated trajectory
|
||||
sim_line, = ax.plot([], [], 'r--', linewidth=1.5, label='Simulated Trajectory')
|
||||
|
||||
# Animation initialization function
|
||||
def init():
|
||||
ball.center = (time_data[0], position_data[0])
|
||||
if sim_line is not None:
|
||||
sim_line.set_data([], [])
|
||||
return [ball] if sim_line is None else [ball, sim_line]
|
||||
|
||||
# Animation update function
|
||||
def update(frame):
|
||||
# Scale frame to data length
|
||||
idx = min(int(frame * len(time_data) / 200), len(time_data) - 1)
|
||||
|
||||
# Update ball position
|
||||
t = time_data[idx]
|
||||
y = position_data[idx]
|
||||
ball.center = (t, max(0, y)) # Ensure ball doesn't go below ground
|
||||
|
||||
# Update simulated trajectory line if available
|
||||
if sim_line is not None and sim_time is not None:
|
||||
# Show trajectory up to current time
|
||||
mask = sim_time <= t
|
||||
if np.any(mask): # Make sure we have at least one point
|
||||
sim_line.set_data(sim_time[mask], sim_pos[mask])
|
||||
|
||||
return [ball] if sim_line is None else [ball, sim_line]
|
||||
|
||||
# Create animation
|
||||
ani = animation.FuncAnimation(
|
||||
fig, update, frames=200, init_func=init,
|
||||
blit=True, interval=30, repeat=True
|
||||
)
|
||||
|
||||
# Save animation if requested
|
||||
if save_animation:
|
||||
writer = PillowWriter(fps=30)
|
||||
filename = f"{ball_type.lower()}_{label.replace(' ', '_')}_bounce.gif"
|
||||
ani.save(filename, writer=writer)
|
||||
print(f"Animation saved as {filename}")
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
def process_and_animate_all(file_paths,
|
||||
ball_type,
|
||||
min_distance=10,
|
||||
min_time_diff=0.2,
|
||||
relative_prominence=None,
|
||||
low_height_threshold=8,
|
||||
low_height_adjustment=0.5,
|
||||
high_height_threshold=15,
|
||||
high_height_adjustment=1.2,
|
||||
max_bounces=7,
|
||||
fs=None,
|
||||
save_animations=False):
|
||||
"""
|
||||
Processes each trial and creates an animated visualization for each one.
|
||||
"""
|
||||
results = []
|
||||
for init_label, path in file_paths.items():
|
||||
try:
|
||||
initial_height = float(init_label.split()[0])
|
||||
except ValueError:
|
||||
initial_height = np.nan
|
||||
|
||||
# Compute effective prominence
|
||||
effective_prominence = relative_prominence * initial_height
|
||||
if initial_height < low_height_threshold:
|
||||
effective_prominence *= low_height_adjustment
|
||||
elif initial_height > high_height_threshold:
|
||||
effective_prominence *= high_height_adjustment
|
||||
|
||||
print(f"Processing trial '{init_label}' with effective prominence: {effective_prominence}")
|
||||
|
||||
# Process the trial
|
||||
df, peak_indices, bounce_heights, bounce_times, cor_values, avg_cor, signal_data = process_trial(
|
||||
path, effective_prominence,
|
||||
min_distance=min_distance,
|
||||
min_time_diff=min_time_diff,
|
||||
max_bounces=max_bounces,
|
||||
fs=fs
|
||||
)
|
||||
|
||||
results.append({
|
||||
'Trial': init_label,
|
||||
'Initial Height': initial_height,
|
||||
'Average COR': avg_cor,
|
||||
'Bounce Times': bounce_times,
|
||||
'Bounce Heights': bounce_heights,
|
||||
'COR Values': cor_values
|
||||
})
|
||||
|
||||
# Create animation
|
||||
animate_bouncing_ball(
|
||||
df, peak_indices, bounce_heights, bounce_times, avg_cor,
|
||||
label=init_label, ball_type=ball_type, initial_height=initial_height,
|
||||
g=386.09, save_animation=save_animations
|
||||
)
|
||||
|
||||
summary_df = pd.DataFrame(results)
|
||||
summary_df.sort_values(by='Initial Height', inplace=True)
|
||||
return summary_df
|
||||
|
||||
|
||||
def plot_cor_table(cor_df, ball_type):
|
||||
"""
|
||||
Plots a line (and markers) of Average COR vs. Initial Height from the summary DataFrame.
|
||||
"""
|
||||
plt.figure(figsize=(8, 5))
|
||||
sns.lineplot(x='Initial Height', y='Average COR', marker='o', data=cor_df)
|
||||
plt.title(f'Average COR vs. Initial Height for {ball_type} Ball')
|
||||
plt.xlabel('Initial Height (inches)')
|
||||
plt.ylabel('Average COR')
|
||||
plt.grid(True)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Process & animate Golf Ball trials
|
||||
print("\nProcessing and animating Golf Ball trials...")
|
||||
golf_results = process_and_animate_all(
|
||||
golf_file_paths,
|
||||
ball_type='Golf',
|
||||
min_distance=min_distance,
|
||||
min_time_diff=min_time_diff,
|
||||
relative_prominence=golf_relative_prominence,
|
||||
low_height_threshold=golf_low_threshold,
|
||||
low_height_adjustment=golf_low_adjustment,
|
||||
high_height_threshold=golf_high_threshold,
|
||||
high_height_adjustment=golf_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=fs,
|
||||
save_animations=save_animations
|
||||
)
|
||||
|
||||
# Process & animate Lacrosse Ball trials
|
||||
print("\nProcessing and animating Lacrosse Ball trials...")
|
||||
lacrosse_results = process_and_animate_all(
|
||||
lacrosse_file_paths,
|
||||
ball_type='Lacrosse',
|
||||
min_distance=min_distance,
|
||||
min_time_diff=min_time_diff,
|
||||
relative_prominence=lacrosse_relative_prominence,
|
||||
low_height_threshold=lacrosse_low_threshold,
|
||||
low_height_adjustment=lacrosse_low_adjustment,
|
||||
high_height_threshold=lacrosse_high_threshold,
|
||||
high_height_adjustment=lacrosse_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=fs,
|
||||
save_animations=save_animations
|
||||
)
|
||||
|
||||
# Process & animate Metal Ball trials
|
||||
print("\nProcessing and animating Metal Ball trials...")
|
||||
metal_results = process_and_animate_all(
|
||||
metal_file_paths,
|
||||
ball_type='Metal',
|
||||
min_distance=min_distance,
|
||||
min_time_diff=min_time_diff,
|
||||
relative_prominence=metal_relative_prominence,
|
||||
low_height_threshold=metal_low_threshold,
|
||||
low_height_adjustment=metal_low_adjustment,
|
||||
high_height_threshold=metal_high_threshold,
|
||||
high_height_adjustment=metal_high_adjustment,
|
||||
max_bounces=7,
|
||||
fs=fs,
|
||||
save_animations=save_animations
|
||||
)
|
||||
|
||||
# Print summary tables
|
||||
print("\nGolf Ball COR Table:")
|
||||
print(golf_results[['Trial', 'Initial Height', 'Average COR']])
|
||||
print("\nLacrosse Ball COR Table:")
|
||||
print(lacrosse_results[['Trial', 'Initial Height', 'Average COR']])
|
||||
print("\nMetal Ball COR Table:")
|
||||
print(metal_results[['Trial', 'Initial Height', 'Average COR']])
|
||||
|
||||
# Plot COR vs. initial height for each ball type
|
||||
plot_cor_table(golf_results, 'Golf')
|
||||
plot_cor_table(lacrosse_results, 'Lacrosse')
|
||||
plot_cor_table(metal_results, 'Metal')
|
||||
159
src/visualization/static_plots.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Static visualization module for the bouncing ball analysis project.
|
||||
"""
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
import numpy as np
|
||||
|
||||
# Set the default style
|
||||
sns.set_theme(style="whitegrid", palette="muted")
|
||||
|
||||
def plot_position_vs_time(df, label, ball_type):
|
||||
"""
|
||||
Plot position vs. time for a single trial.
|
||||
|
||||
Parameters:
|
||||
df (pandas.DataFrame): DataFrame with 'Time' and 'Position' columns
|
||||
label (str): Label for the trial (e.g., "11 inches")
|
||||
ball_type (str): Type of ball (e.g., "Golf")
|
||||
"""
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
ax.plot(df["Time"], df["Position"], label=label, marker='o', markersize=3, linewidth=2)
|
||||
|
||||
ax.set_xlabel("Time (seconds)", fontsize=14)
|
||||
ax.set_ylabel("Position (inches)", fontsize=14)
|
||||
ax.set_title(f"{ball_type} - Position vs. Time\nInitial Height: {label}", fontsize=16, pad=15)
|
||||
|
||||
ax.grid(True, which='both', linestyle='--', linewidth=0.5, alpha=0.7)
|
||||
|
||||
ax.legend(fontsize=12)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
def plot_trial_with_bounces(df, peak_indices, signal_data, label, ball_type,
|
||||
initial_height=None, cor=None, bounces=7, dt=0.001, g=386.09):
|
||||
"""
|
||||
Plots raw data (Position vs. Time) for a single trial and marks the detected bounces with red 'x'.
|
||||
Optionally overlays an ideal bounce trajectory.
|
||||
|
||||
Parameters:
|
||||
df (pandas.DataFrame): DataFrame with 'Time' and 'Position' columns
|
||||
peak_indices (numpy.ndarray): Indices of detected bounces
|
||||
signal_data (numpy.ndarray): Position data
|
||||
label (str): Label for the trial (e.g., "11 inches")
|
||||
ball_type (str): Type of ball (e.g., "Golf")
|
||||
initial_height (float): Initial height of the ball
|
||||
cor (float): Coefficient of Restitution
|
||||
bounces (int): Number of bounces to show in the ideal trajectory
|
||||
dt (float): Time step for the ideal trajectory simulation
|
||||
g (float): Acceleration due to gravity (in inches/s²)
|
||||
"""
|
||||
plt.figure(figsize=(10, 6))
|
||||
|
||||
# 1) Plot raw signal
|
||||
plt.plot(df['Time'], df['Position'], linewidth=1.5, color='blue', label='Raw Data')
|
||||
|
||||
# 2) Mark the detected bounces
|
||||
if len(peak_indices) > 0:
|
||||
plt.plot(
|
||||
df['Time'].iloc[peak_indices],
|
||||
df['Position'].iloc[peak_indices],
|
||||
'gx',
|
||||
markersize=10,
|
||||
label='Detected Bounces'
|
||||
)
|
||||
|
||||
# 3) Simulated Idealized Bounce Trajectory (Ensuring Exactly 7 Bounces)
|
||||
if initial_height is not None and cor is not None and not np.isnan(cor):
|
||||
time_points = []
|
||||
position_points = []
|
||||
|
||||
# Initialize simulation parameters
|
||||
y = initial_height
|
||||
v_y = 0.0 # Starting from rest
|
||||
t = 0.0
|
||||
bounce_count = 0
|
||||
model_first_bounce_time = None
|
||||
|
||||
while bounce_count < bounces: # Ensure exactly 7 bounces
|
||||
time_points.append(t)
|
||||
position_points.append(y)
|
||||
|
||||
# Kinematic update
|
||||
v_y -= g * dt
|
||||
y += v_y * dt
|
||||
t += dt
|
||||
|
||||
# Bounce condition
|
||||
if y <= 0:
|
||||
bounce_count += 1
|
||||
y = 0 # Ground impact
|
||||
v_y = -v_y * cor # Apply COR
|
||||
|
||||
if model_first_bounce_time is None:
|
||||
model_first_bounce_time = t # Store first bounce time
|
||||
|
||||
# Align first model bounce with data's first detected bounce
|
||||
if model_first_bounce_time is not None and len(peak_indices) > 0:
|
||||
data_first_bounce_time = df['Time'].iloc[peak_indices[0]]
|
||||
time_shift = data_first_bounce_time - model_first_bounce_time
|
||||
time_points = [t + time_shift for t in time_points]
|
||||
|
||||
# Plot idealized trajectory
|
||||
plt.plot(time_points, position_points, 'r--', label='Ideal Trajectory')
|
||||
|
||||
# 4) Formatting the plot
|
||||
plt.title(f'{ball_type} Trial: {label}')
|
||||
plt.xlabel('Time (s)')
|
||||
plt.ylabel('Position (inches)')
|
||||
plt.autoscale(True, 'x', True)
|
||||
plt.grid(True)
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
|
||||
def plot_cor_table(cor_df, ball_type):
|
||||
"""
|
||||
Plots a line (and markers) of Average COR vs. Initial Height from the summary DataFrame.
|
||||
|
||||
Parameters:
|
||||
cor_df (pandas.DataFrame): DataFrame with 'Initial Height' and 'Average COR' columns
|
||||
ball_type (str): Type of ball (e.g., "Golf")
|
||||
"""
|
||||
plt.figure(figsize=(8, 5))
|
||||
sns.lineplot(x='Initial Height', y='Average COR', marker='o', data=cor_df)
|
||||
plt.title(f'Average COR vs. Initial Height for {ball_type} Ball')
|
||||
plt.xlabel('Initial Height (inches)')
|
||||
plt.ylabel('Average COR')
|
||||
plt.grid(True)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
def plot_all_cors(golf_df, lacrosse_df, metal_df):
|
||||
"""
|
||||
Plot COR vs. initial height for all ball types on the same graph.
|
||||
|
||||
Parameters:
|
||||
golf_df (pandas.DataFrame): DataFrame with golf ball data
|
||||
lacrosse_df (pandas.DataFrame): DataFrame with lacrosse ball data
|
||||
metal_df (pandas.DataFrame): DataFrame with metal ball data
|
||||
"""
|
||||
plt.figure(figsize=(10, 6))
|
||||
|
||||
# Plot each ball type with a different color and marker
|
||||
sns.lineplot(x='Initial Height', y='Average COR', marker='o', data=golf_df, label='Golf Ball')
|
||||
sns.lineplot(x='Initial Height', y='Average COR', marker='s', data=lacrosse_df, label='Lacrosse Ball')
|
||||
sns.lineplot(x='Initial Height', y='Average COR', marker='^', data=metal_df, label='Metal Ball')
|
||||
|
||||
plt.title('Coefficient of Restitution vs. Initial Height')
|
||||
plt.xlabel('Initial Height (inches)')
|
||||
plt.ylabel('Average COR')
|
||||
plt.grid(True)
|
||||
plt.legend()
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||