Merge pull request 'Bounce+' (#1) from b3_animate into master

Reviewed-on: https://bee8333.ddns.net/bee8333/bounce/pulls/1
This commit was merged in pull request #1.
This commit is contained in:
2025-03-02 23:18:06 +00:00
75 changed files with 3198 additions and 2 deletions

170
README.md
View File

@@ -1,3 +1,169 @@
# bounce # Bouncing Ball Analysis
lab2 This project analyzes the bouncing behavior of different types of balls (golf, lacrosse, and metal) dropped from various heights. It includes both static analysis and animated visualizations to help understand the physics of bouncing balls and calculate the Coefficient of Restitution (COR).
## Project Structure
The project has been organized into a modular structure for maintainability:
```
bouncing-ball-analysis/
├── src/ # Source code directory
│ ├── data/ # Data files and loaders
│ │ ├── golf/ # Golf ball data files
│ │ ├── lacrosse/ # Lacrosse ball data files
│ │ ├── metal/ # Metal ball data files
│ │ ├── images/ # Image files
│ │ └── loader.py # Data loading utilities
│ ├── analysis/ # Analysis modules
│ │ └── bounce_detection.py # Bounce detection and COR calculation
│ ├── visualization/ # Visualization modules
│ │ ├── static_plots.py # Static visualization functions
│ │ ├── animation.py # Animation functions
│ │ └── lab2_part*_animated.py # Original animation scripts
│ ├── utils/ # Utility functions
│ │ ├── helpers.py # Helper functions
│ │ └── organize_files.py # File organization script
│ ├── main.py # Main entry point for analysis
│ └── lab2_part*.py # Original analysis scripts
├── output/ # Output directory for results
├── requirements.txt # Project dependencies
└── README.md # This file
```
## Original Files
The original scripts have been preserved for reference:
### Analysis Scripts
- `src/lab2_part1.py`: Static position vs. time plots for each ball type and height
- `src/lab2_part2.py`: Detailed analysis with bounce detection and COR calculation
### Animated Visualization Scripts
- `src/visualization/lab2_part1_animated.py`: Animated version of the position vs. time plots
- `src/visualization/lab2_part2_animated.py`: Animated visualization of bouncing balls with physics simulation
- `src/visualization/animate_bouncing_balls.py`: Combined script with command-line interface to run either animation type
## Setup Instructions
### Setting Up a Virtual Environment
It's recommended to use a virtual environment to run this project. Here's how to set it up:
#### For macOS/Linux:
```bash
# Create a virtual environment
python3 -m venv venv
# Activate the virtual environment
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
```
#### For Windows:
```bash
# Create a virtual environment
python -m venv venv
# Activate the virtual environment
venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
```
### Installing Dependencies Directly
If you prefer not to use a virtual environment, you can install the dependencies directly:
```bash
pip install -r requirements.txt
```
## Running the Analysis
### Using the New Structure
To run the complete analysis with the new modular structure:
```bash
# Organize files into the new structure (only needed once)
python src/utils/organize_files.py
# Run the main analysis script
python src/main.py
```
This will:
1. Load data for all ball types
2. Detect bounces and calculate COR values
3. Generate static plots and animations
4. Save results to the `output` directory
### Using the Original Scripts
You can still run the original scripts if preferred:
#### Static Analysis
To run the basic position vs. time plots:
```bash
python src/lab2_part1.py
```
To run the detailed analysis with bounce detection and COR calculation:
```bash
python src/lab2_part2.py
```
#### Animated Visualizations
The easiest way to run the animations is using the combined script:
```bash
# Run position vs. time animations
python src/visualization/animate_bouncing_balls.py position
# Run bouncing ball physics animations
python src/visualization/animate_bouncing_balls.py bounce
```
Additional options:
```bash
# Run animations for a specific ball type
python src/visualization/animate_bouncing_balls.py position --ball golf
python src/visualization/animate_bouncing_balls.py bounce --ball lacrosse
# Save animations as GIF files
python src/visualization/animate_bouncing_balls.py position --save
python src/visualization/animate_bouncing_balls.py bounce --ball metal --save
```
For help with command-line options:
```bash
python src/visualization/animate_bouncing_balls.py --help
```
## Physics Background
The scripts calculate the Coefficient of Restitution (COR) for each bounce, which is a measure of the "bounciness" of the ball. The COR is calculated as:
```
COR = sqrt(h_{n+1} / h_n)
```
Where:
- h_n is the height of the nth bounce
- h_{n+1} is the height of the next bounce
A COR of 1.0 would mean a perfectly elastic collision (no energy loss), while a COR of 0.0 would mean a completely inelastic collision (all energy lost).
## Customization
You can customize various parameters in the scripts:
- Animation speed: Modify the `fps` parameter in the animation functions
- Ball size: Change the `ball_radius` parameter in the animation functions
- Simulation parameters: Adjust the physics parameters like gravity (`g`) or time step (`dt`)
- Bounce detection: Adjust the parameters in the `BALL_PARAMS` dictionary in `src/analysis/bounce_detection.py`

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

6
output/golf_summary.csv Normal file
View File

@@ -0,0 +1,6 @@
peak_indices,bounce_heights,bounce_times,cor_values,Average COR,signal_data,Initial Height,Num Bounces,Ball Type,Path
[ 9104 11129 12858 14329 15579 17579 18364],[15.663 10.541 10.537 10.593 8.172 7.809 5.567],[1.821 2.226 2.572 2.866 3.116 3.516 3.673],[0.82035803 0.99981025 1.00265378 0.87832388 0.97753774 0.84433132],0.9205024998169424,[ 0. -0.015 -0.015 ... -0.011 -0.015 -0.015],11.0,7,Golf,src/data/golf/golf_11.csv
[ 8153 10235 12022 13560 14882 16008 17815],[11.556 12.342 11.634 9.555 7.728 5.693 3.243],[1.631 2.047 2.404 2.712 2.976 3.202 3.563],[1.03344889 0.97089387 0.90625584 0.8993282 0.85829589 0.75474958],0.9038287124950632,[0. 0. 0. ... 0.004 0.004 0. ],12.0,7,Golf,src/data/golf/golf_12.csv
[ 5831 7977 9825 11407 12765 13929 15765],[13.068 11.686 11.664 7.909 9.21 7.387 4.229],[1.166 1.595 1.965 2.281 2.553 2.786 3.153],[0.94564554 0.99905826 0.82344962 1.07911823 0.89557969 0.75663215],0.9165805802090672,[0. 0.004 0. ... 0.007 0.004 0.007],13.0,7,Golf,src/data/golf/golf_13.csv
[12333 14657 16642 18342 19801 21043 22093],[14.836 10.337 8.28 7.427 5.915 5.485 4.285],[2.467 2.931 3.328 3.668 3.96 4.209 4.419],[0.83471621 0.89498944 0.94709064 0.89242281 0.96296597 0.88386736],0.9026754047920241,[ 0. -0.007 -0.007 ... -0.011 -0.015 -0.004],14.0,7,Golf,src/data/golf/golf_14.csv
[ 9115 11353 13269 14917 16327 17531 18562],[13.031 10.982 9.607 8.128 6.827 6.712 5.13 ],[1.823 2.271 2.654 2.983 3.265 3.506 3.712],[0.91801938 0.93530483 0.91980963 0.91648024 0.99154179 0.8742441 ],0.925899992441524,[ 0. -0.004 -0.007 ... -0.007 -0.004 -0.007],15.0,7,Golf,src/data/golf/golf_15.csv
1 peak_indices bounce_heights bounce_times cor_values Average COR signal_data Initial Height Num Bounces Ball Type Path
2 [ 9104 11129 12858 14329 15579 17579 18364] [15.663 10.541 10.537 10.593 8.172 7.809 5.567] [1.821 2.226 2.572 2.866 3.116 3.516 3.673] [0.82035803 0.99981025 1.00265378 0.87832388 0.97753774 0.84433132] 0.9205024998169424 [ 0. -0.015 -0.015 ... -0.011 -0.015 -0.015] 11.0 7 Golf src/data/golf/golf_11.csv
3 [ 8153 10235 12022 13560 14882 16008 17815] [11.556 12.342 11.634 9.555 7.728 5.693 3.243] [1.631 2.047 2.404 2.712 2.976 3.202 3.563] [1.03344889 0.97089387 0.90625584 0.8993282 0.85829589 0.75474958] 0.9038287124950632 [0. 0. 0. ... 0.004 0.004 0. ] 12.0 7 Golf src/data/golf/golf_12.csv
4 [ 5831 7977 9825 11407 12765 13929 15765] [13.068 11.686 11.664 7.909 9.21 7.387 4.229] [1.166 1.595 1.965 2.281 2.553 2.786 3.153] [0.94564554 0.99905826 0.82344962 1.07911823 0.89557969 0.75663215] 0.9165805802090672 [0. 0.004 0. ... 0.007 0.004 0.007] 13.0 7 Golf src/data/golf/golf_13.csv
5 [12333 14657 16642 18342 19801 21043 22093] [14.836 10.337 8.28 7.427 5.915 5.485 4.285] [2.467 2.931 3.328 3.668 3.96 4.209 4.419] [0.83471621 0.89498944 0.94709064 0.89242281 0.96296597 0.88386736] 0.9026754047920241 [ 0. -0.007 -0.007 ... -0.011 -0.015 -0.004] 14.0 7 Golf src/data/golf/golf_14.csv
6 [ 9115 11353 13269 14917 16327 17531 18562] [13.031 10.982 9.607 8.128 6.827 6.712 5.13 ] [1.823 2.271 2.654 2.983 3.265 3.506 3.712] [0.91801938 0.93530483 0.91980963 0.91648024 0.99154179 0.8742441 ] 0.925899992441524 [ 0. -0.004 -0.007 ... -0.007 -0.004 -0.007] 15.0 7 Golf src/data/golf/golf_15.csv

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,6 @@
peak_indices,bounce_heights,bounce_times,cor_values,Average COR,signal_data,Initial Height,Num Bounces,Ball Type,Path
[10919 13508 15762 17683 19390 20869 22156],[8.836 6.401 4.262 2.646 1.82 1.301 0.949],[2.184 2.702 3.152 3.537 3.878 4.174 4.431],[0.85113032 0.81598619 0.78793102 0.82935559 0.84547925 0.85407195],0.8306590518166229,[ 0. -0.011 -0.015 ... -0.011 -0.011 -0.007],18.0,7,Lacrosse,src/data/lacrosse/l_18.csv
[10062 12709 15000 17011 18748 20269 21612],[11.189 7.476 5.4 4.277 3.347 2.639 1.983],[2.012 2.542 3. 3.402 3.75 4.054 4.322],[0.81740824 0.84988905 0.88996463 0.88462301 0.8879568 0.86684543],0.8661145251160992,[ 0. -0.004 -0.007 ... 0. 0.004 -0.004],19.0,7,Lacrosse,src/data/lacrosse/l_19.csv
[ 6822 9528 11865 13890 15648 17187 18540],[12.023 8.758 6.879 4.959 3.158 1.816 1.216],[1.364 1.906 2.373 2.778 3.13 3.437 3.708],[0.8534853 0.88625803 0.84905222 0.79801124 0.75831886 0.81829306],0.8272364515550595,[ 0. -0.015 -0.004 ... -0.011 -0.011 -0.007],20.0,7,Lacrosse,src/data/lacrosse/l_20.csv
[ 9209 11994 14400 16487 18330 19925 21329],[12.742 8.424 6.46 4.362 3.236 2.435 1.857],[1.842 2.399 2.88 3.297 3.666 3.985 4.266],[0.81309329 0.87570349 0.82172514 0.86131384 0.86745155 0.87328594],0.8520955412394214,[0. 0. 0. ... 0. 0. 0.],21.0,7,Lacrosse,src/data/lacrosse/l_21.csv
[ 5299 8140 10613 12737 14589 16203 17615],[14.088 7.546 4.737 3.543 2.802 2.183 1.52 ],[1.06 1.628 2.123 2.547 2.918 3.241 3.523],[0.73186964 0.79230663 0.86483625 0.8893004 0.88265869 0.83443964],0.8325685416695627,[ 0. 0. 0.004 ... -0.007 -0.004 0. ],22.0,7,Lacrosse,src/data/lacrosse/l_22.csv
1 peak_indices bounce_heights bounce_times cor_values Average COR signal_data Initial Height Num Bounces Ball Type Path
2 [10919 13508 15762 17683 19390 20869 22156] [8.836 6.401 4.262 2.646 1.82 1.301 0.949] [2.184 2.702 3.152 3.537 3.878 4.174 4.431] [0.85113032 0.81598619 0.78793102 0.82935559 0.84547925 0.85407195] 0.8306590518166229 [ 0. -0.011 -0.015 ... -0.011 -0.011 -0.007] 18.0 7 Lacrosse src/data/lacrosse/l_18.csv
3 [10062 12709 15000 17011 18748 20269 21612] [11.189 7.476 5.4 4.277 3.347 2.639 1.983] [2.012 2.542 3. 3.402 3.75 4.054 4.322] [0.81740824 0.84988905 0.88996463 0.88462301 0.8879568 0.86684543] 0.8661145251160992 [ 0. -0.004 -0.007 ... 0. 0.004 -0.004] 19.0 7 Lacrosse src/data/lacrosse/l_19.csv
4 [ 6822 9528 11865 13890 15648 17187 18540] [12.023 8.758 6.879 4.959 3.158 1.816 1.216] [1.364 1.906 2.373 2.778 3.13 3.437 3.708] [0.8534853 0.88625803 0.84905222 0.79801124 0.75831886 0.81829306] 0.8272364515550595 [ 0. -0.015 -0.004 ... -0.011 -0.011 -0.007] 20.0 7 Lacrosse src/data/lacrosse/l_20.csv
5 [ 9209 11994 14400 16487 18330 19925 21329] [12.742 8.424 6.46 4.362 3.236 2.435 1.857] [1.842 2.399 2.88 3.297 3.666 3.985 4.266] [0.81309329 0.87570349 0.82172514 0.86131384 0.86745155 0.87328594] 0.8520955412394214 [0. 0. 0. ... 0. 0. 0.] 21.0 7 Lacrosse src/data/lacrosse/l_21.csv
6 [ 5299 8140 10613 12737 14589 16203 17615] [14.088 7.546 4.737 3.543 2.802 2.183 1.52 ] [1.06 1.628 2.123 2.547 2.918 3.241 3.523] [0.73186964 0.79230663 0.86483625 0.8893004 0.88265869 0.83443964] 0.8325685416695627 [ 0. 0. 0.004 ... -0.007 -0.004 0. ] 22.0 7 Lacrosse src/data/lacrosse/l_22.csv

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

6
output/metal_summary.csv Normal file
View File

@@ -0,0 +1,6 @@
peak_indices,bounce_heights,bounce_times,cor_values,Average COR,signal_data,Initial Height,Num Bounces,Ball Type,Path
[3670 4515 5892 6453 8008 9034],[0.982 0.952 0.826 0.526 0.333 0.196],[0.734 0.903 1.178 1.291 1.602 1.807],[0.98460657 0.93147574 0.79799992 0.79566315 0.76719527],0.85538813191176,[ 0. -0.004 0. ... -0.004 -0.007 0. ],2.0,6,Metal,src/data/metal/metal_2.csv
[ 3738 4983 6054 8496 9121 10165 11639],[0.878 0.97 0.941 0.6 0.456 0.37 0.259],[0.748 0.997 1.211 1.699 1.824 2.033 2.328],[1.05108687 0.98493812 0.79851084 0.87177979 0.90077939 0.83666003],0.9072925036242109,[ 0. -0.004 0.004 ... 0.011 0.007 0.015],4.0,7,Metal,src/data/metal/metal_4.csv
[ 5380 6860 8096 9174 11618 12241 13691],[1.319 1.067 0.815 0.707 0.682 0.37 0.467],[1.076 1.372 1.619 1.835 2.324 2.448 2.738],[0.89941435 0.87397014 0.93138857 0.98216054 0.73656092 1.12345991],0.9244924038949129,[ 0. -0.004 -0.004 ... 0. -0.004 -0.004],6.0,7,Metal,src/data/metal/metal_6.csv
[1485 2918 4150 5184 7598 8214 9668],[1.419 1.278 1.096 0.752 0.667 1.074 0.415],[0.297 0.584 0.83 1.037 1.52 1.643 1.934],[0.94901752 0.92606154 0.82833048 0.94178983 1.26893455 0.6216156 ],0.9226249221490423,[ 0. 0. -0.015 ... 0.019 0.019 0.022],8.0,7,Metal,src/data/metal/metal_8.csv
[ 1900 3786 5374 6738 7917 9797 10566],[1.882 2.045 1.648 1.022 0.789 0.882 0.419],[0.38 0.757 1.075 1.348 1.583 1.959 2.113],[1.04240587 0.89770149 0.78749326 0.87864421 1.05729406 0.68924356],0.8921304085260445,[ 0. -0.004 0.015 ... -0.007 0. 0.004],10.0,7,Metal,src/data/metal/metal_10.csv
1 peak_indices bounce_heights bounce_times cor_values Average COR signal_data Initial Height Num Bounces Ball Type Path
2 [3670 4515 5892 6453 8008 9034] [0.982 0.952 0.826 0.526 0.333 0.196] [0.734 0.903 1.178 1.291 1.602 1.807] [0.98460657 0.93147574 0.79799992 0.79566315 0.76719527] 0.85538813191176 [ 0. -0.004 0. ... -0.004 -0.007 0. ] 2.0 6 Metal src/data/metal/metal_2.csv
3 [ 3738 4983 6054 8496 9121 10165 11639] [0.878 0.97 0.941 0.6 0.456 0.37 0.259] [0.748 0.997 1.211 1.699 1.824 2.033 2.328] [1.05108687 0.98493812 0.79851084 0.87177979 0.90077939 0.83666003] 0.9072925036242109 [ 0. -0.004 0.004 ... 0.011 0.007 0.015] 4.0 7 Metal src/data/metal/metal_4.csv
4 [ 5380 6860 8096 9174 11618 12241 13691] [1.319 1.067 0.815 0.707 0.682 0.37 0.467] [1.076 1.372 1.619 1.835 2.324 2.448 2.738] [0.89941435 0.87397014 0.93138857 0.98216054 0.73656092 1.12345991] 0.9244924038949129 [ 0. -0.004 -0.004 ... 0. -0.004 -0.004] 6.0 7 Metal src/data/metal/metal_6.csv
5 [1485 2918 4150 5184 7598 8214 9668] [1.419 1.278 1.096 0.752 0.667 1.074 0.415] [0.297 0.584 0.83 1.037 1.52 1.643 1.934] [0.94901752 0.92606154 0.82833048 0.94178983 1.26893455 0.6216156 ] 0.9226249221490423 [ 0. 0. -0.015 ... 0.019 0.019 0.022] 8.0 7 Metal src/data/metal/metal_8.csv
6 [ 1900 3786 5374 6738 7917 9797 10566] [1.882 2.045 1.648 1.022 0.789 0.882 0.419] [0.38 0.757 1.075 1.348 1.583 1.959 2.113] [1.04240587 0.89770149 0.78749326 0.87864421 1.05729406 0.68924356] 0.8921304085260445 [ 0. -0.004 0.015 ... -0.007 0. 0.004] 10.0 7 Metal src/data/metal/metal_10.csv

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
pandas>=1.3.0
numpy>=1.20.0
matplotlib>=3.4.0
seaborn>=0.11.0
scipy>=1.7.0
pillow>=8.0.0 # For saving animations as GIF files

1
src/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Main package initialization

1
src/analysis/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Analysis package initialization

View 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
View File

@@ -0,0 +1 @@
# Data package initialization

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

82
src/data/loader.py Normal file
View 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)

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View File

Can't render this file because it is too large.

View 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
}

View 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()

View 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()

136
src/main.py Normal file
View 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
View File

@@ -0,0 +1 @@
# Utils package initialization

80
src/utils/cleanup.py Normal file
View 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
View 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

View 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()

View File

@@ -0,0 +1 @@
# Visualization package initialization

View 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()

View 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)

View 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)

View 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')

View 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()