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]