PHYS 216 Lab 3 Modules

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.

friction_experiment.py

In [2]:
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()

friction_trial.py

In [ ]:
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]