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