As this experiment was repeated 9 times for each of 4 configurations, the class FrictionExperiment
was created to handle the calculations and plotting needed to find the coefficient of friction for each trial. Dependent on the number (n
) of trials and type of friction calculation (static or kinetic), the FrictionExperiment
class created a tuple of size n
containing instances of type StaticFrictionTrial
or KineticFrictionTrial
. The blocks of code present the code existing in the friction_experiment.py and friction_trial.py files.
from typing import Tuple
from scipy import stats
import numpy as np
import matplotlib.pyplot as plt
import friction_trial
class FrictionExperiment:
'''An instance of this class is created for each configuration of the friction experiments done in this lab.'''
type: str
surface: str
trials: Tuple[friction_trial.FrictionTrial]
def __init__(self, friction_type: str, surface: str, trials: int):
'''
:param friction_type: either 'static' or 'kinetic'
:param surface: either 'big' or 'small'
:param trials: number of trials in this experiment
'''
self.type = friction_type
self.surface = surface
if friction_type == 'static':
self.trials = tuple([friction_trial.StaticFrictionTrial(surface=surface, trial=i) for i in range(1, trials+1)])
elif friction_type == 'kinetic':
self.trials = tuple([friction_trial.KineticFrictionTrial(surface=surface, trial=i) for i in range(1, trials+1)])
else:
raise ValueError("The argument assigned to 'type' must be either 'static' or 'kinetic'")
@property
def __str__(self):
return f"{self.type.title()} Friction of a {self.surface.title()} Surface"
@property
def mu(self) -> np.array:
'''Returns an array containing the calculated friction coefficient of each trial in this experiment.'''
return np.array([trial.mu for trial in self.trials])
@property
def mu_avg(self) -> float:
'''Returns the mean friction coefficient from the array of trials.'''
return self.mu.mean()
@property
def mu_error(self) -> float:
'''Returns the standard error of the calculated friction coefficients.'''
return self.mu.std(ddof=0) / np.sqrt(self.mu.shape[0])
def mu_confidence_interval(self, confidence: float = .95) -> Tuple[float]:
'''Returns the confidence interval for the calculated friction coefficients.'''
x = self.mu.mean()
sigma = self.mu.std(ddof=0)
n = self.mu.shape[0]
z_lower = stats.norm.ppf(0.5 - confidence/2)
z_upper = stats.norm.ppf(0.5 + confidence/2)
lower_bound = x + z_lower * sigma / np.sqrt(n)
upper_bound = x + z_upper * sigma / np.sqrt(n)
return lower_bound, upper_bound
def plot_raw_data(self, x="rx-yellowneon", y="ry-yellowneon", legend: bool=False, index: Tuple[int]=None):
'''
Creates am aggregated plot from the `raw_data` dataframe of each trial.
:param x: column name to be used for the x-axis
:param y: column name to be used for the y-axis
:param legend: if true, show the legend
:param index: if not none, only include the trials whos index is in this tuple
:return: None
'''
if not index:
index = range(len(self.trials))
for i in index:
plt.plot(x, y, data=self.trials[i].raw_data, label=str(self.trials[i]), marker='.', linestyle='')
plt.xlabel(x)
plt.ylabel(y)
plt.title(str(self))
if legend:
plt.legend()
plt.show()
def plot_calculations(self, x="timestamp", y="Theta", legend=False, index=None):
'''
Creates am aggregated plot from the `calculations` dataframe of each trial.
:param x: column name to be used for the x-axis
:param y: column name to be used for the y-axis
:param legend: if true, show the legend
:param index: if not none, only include the trials whos index is in this tuple
:return: None
'''
if not index:
index = range(len(self.trials))
for i in index:
plt.plot(x, y, data=self.trials[i].calculations, label=str(self.trials[i]), marker='.', linestyle='')
plt.xlabel(x)
plt.ylabel(y)
plt.title(str(self))
if legend:
plt.legend()
plt.show()
import pandas as pd
import numpy as np
from abc import ABC, abstractmethod
class FrictionTrial(ABC):
'''An instance of the subclasses of this class is created for each trial.'''
type: str
surface: str
trial: int
raw_data: pd.DataFrame
calculations: pd.DataFrame
def __init__(self, friction_type: str, surface: str, trial: int):
'''
:param friction_type: either 'static' or 'kinetic'
:param surface: either 'big' or 'small'
:param trial: trial number
'''
self.type = friction_type
self.surface = surface
self.trial = trial
self.raw_data = pd.read_csv(f'../Data/{self.type}_{self.surface}_{trial}.csv')
self.calculations = pd.DataFrame()
def __str__(self):
return f"Trial {self.trial}"
def convert_cm_to_m(self):
'''Converts each position, velocity, and acceleration column from [cm] to [m].'''
for col in self.raw_data:
if col == 'frame_no' or col == 'timestamp':
pass
else:
# x[cm] * 1[m] / 100[cm]
self.raw_data[col] = self.raw_data[col] / 100
def trim_data(self):
'''Removes unneeded columns and missing data from the `raw_data` dataframe.'''
mask = ['lightorange' not in x for x in self.raw_data]
self.raw_data = self.raw_data[self.raw_data.columns[mask]]
self.raw_data = self.raw_data.dropna()
def calculate_vectors(self):
'''Populates the `calculations` dataframe with position vectors for the block, base, and ramp.'''
self.calculations['timestamp'] = self.raw_data['timestamp']
self.calculations['R-Green'] = [np.array([x,y]) for x,y in zip(self.raw_data['rx-green'], self.raw_data['ry-green'])]
self.calculations['R-Orange'] = [np.array([x,y]) for x,y in zip(self.raw_data['rx-darkorange'], self.raw_data['ry-darkorange'])]
self.calculations['R-Pink'] = [np.array([x,y]) for x,y in zip(self.raw_data['rx-hotpink'], self.raw_data['ry-hotpink'])]
self.calculations['R-Block'] = [np.array([x,y]) for x,y in zip(self.raw_data['rx-yellowneon'], self.raw_data['ry-yellowneon'])]
self.calculations['Base'] = self.calculations['R-Green'] - self.calculations['R-Orange']
self.calculations['Ramp'] = self.calculations['R-Pink'] - self.calculations['R-Orange']
def calculate_theta(self):
'''Calculates the angle between the base and the ramp.'''
dot_product = (self.calculations['Base'] * self.calculations['Ramp']).apply(sum)
base_norm = self.calculations['Base'].apply(np.linalg.norm)
ramp_norm = self.calculations['Ramp'].apply(np.linalg.norm)
self.calculations['Theta'] = np.arccos(dot_product / (base_norm*ramp_norm))
@property
@abstractmethod
def mu(self) -> float:
'''Returns the friction coefficient, which requires a different calculation for static and kinetic trials.'''
return
#---------------------Sub-Classes------------------------#
class StaticFrictionTrial(FrictionTrial):
'''An instance of this class is created for each static friction trial.'''
def __init__(self, surface: str, trial: int):
'''
:param surface: either 'big' or 'small'
:param trial: trial number
'''
super().__init__(friction_type='static', surface=surface, trial = trial)
self.convert_cm_to_m()
self.calculate_vectors()
self.calculate_theta()
@property
def mu(self) -> float:
'''Returns the coefficient of static friction for this trial.'''
idx_max_height = self.raw_data['ry-yellowneon'].idxmax()
mu_s = np.tan(self.calculations['Theta'][idx_max_height])
return mu_s # [no unit]
class KineticFrictionTrial(FrictionTrial):
'''An instance of this class is created for each kinetic friction trial.'''
def __init__(self, surface: str, trial: int):
'''
:param surface: either 'big' or 'small'
:param trial: trial number
'''
super().__init__(friction_type='kinetic', surface=surface, trial = trial)
self.raw_data = self.raw_data[(self.raw_data['rx-yellowneon'] > 25) & (self.raw_data['rx-yellowneon'] < 60)]
self.convert_cm_to_m()
self.calculate_vectors()
self.calculate_theta()
self.calculations['Acceleration-Block'] = np.linalg.norm([self.raw_data['ax-yellowneon'], self.raw_data['ay-yellowneon']], axis=0)
@property
def mu(self) -> float:
'''Returns the coefficient of kinetic friction for this trial.'''
g = 9.80665 # [m/s^2]
a = self.calculations['Acceleration-Block'] # [m/s^2]
theta = self.calculations['Theta'] # [rad]
mu_k = 1 / np.cos(theta) * (np.sin(theta) - a / g)
return mu_k.mean() # [no unit]