Exemplo n.º 1
0
 def __init__(self, subjects, hand_name, rotation=None):
     """
     Class to hold all the data pertaining to a specific hand.
     Combines data from all subjects
     :param subjects: subjects to include in hand data object
     :param hand_name: name of hand for this object
     """
     self.hand = HandObj(hand_name)
     self.subjects_containing = subjects
     self.data = self._gather_hand_data(subjects, rotation)
     self.filtered = False
     self.window_size = None
     self.averages = []
Exemplo n.º 2
0
 def add_hand(self, hand_name):
     """
     If you didn't make the object with a file_name, a function to set hand in painless manner
     :param hand_name: name of hand to make
     """
     self.hand = HandObj(hand_name)
Exemplo n.º 3
0
    def __init__(self, file_name=None, do_metrics=True, norm_data=True):
        """
        Class to represent a single asterisk test trial.
        :param file_name: - name of the file that you want to import data from

        Class contains:
        :attribute hand: - hand object with info for hand involved in the trial (see above)
        :attribute subject_num: - integer value for subject number
        :attribute direction: - single lettered descriptor for which direction the object travels in for this trial
        :attribute trial_type: - indicates one-step or two-step trial as a string (None, Plus15, Minus15)
        :attribute trial_num: - integer number of the trial

        :attribute poses: - pandas dataframe containing the object's trajectory (as floats)
        :attribute filtered: - boolean that indicates whether trial has been filtered or not
        :attribute ideal_poses: - pandas dataframe containing the 'perfect trial' line that we will compare our trial to using Frechet Distance.
        This 'perfect trial' line is a line that travels in the trial direction (with no deviations) to the max travel distance the 
        trial got to in the respective direction. This is denoted as the projection of the object trajectory on the direction

        :attribute total_distance: - float value
        :attribute frechet_distance: - float value
        :attribute dist_along_translation: - float
        :attribute dist_along_twist: - float
        """
        if file_name:
            s, h, t, r, e = file_name.split("_")
            n, _ = e.split(".")
            self.hand = HandObj(h)

            # Data will not be filtered in this step
            data = self._read_file(file_name, norm_data=norm_data)
            self.poses = data[["x", "y", "rmag"]]

        else:
            s, t, r, n = None, None, None, None
            self.hand = None

        self.subject = s
        self.trial_translation = t
        self.trial_rotation = r
        self.trial_num = n

        if file_name:
            print(self.generate_name())

        self.filtered = False
        self.window_size = 0

        self.target_line = None  # the straight path in the direction that this trial is
        self.target_rotation = None

        # metrics - predefining them
        self.total_distance = None
        self.max_error = None
        self.translation_fd = None
        self.rotation_fd = None
        self.fd = None
        self.mvt_efficiency = None
        self.arc_len = None
        self.area_btwn = None
        self.max_area_region = None
        self.max_area_loc = None
        self.metrics = None  # pd series that contains all metrics in it... TODO: to replace the rest later

        if file_name:
            self.target_line, self.total_distance = self.generate_target_line(
                100)  # 100 samples
            self.target_rotation = self.generate_target_rot(
            )  # TODO: doesn't work for true cw and ccw yet

            if do_metrics and self.poses is not None:
                self.update_all_metrics()
Exemplo n.º 4
0
class AsteriskTrialData:
    def __init__(self, file_name=None, do_metrics=True, norm_data=True):
        """
        Class to represent a single asterisk test trial.
        :param file_name: - name of the file that you want to import data from

        Class contains:
        :attribute hand: - hand object with info for hand involved in the trial (see above)
        :attribute subject_num: - integer value for subject number
        :attribute direction: - single lettered descriptor for which direction the object travels in for this trial
        :attribute trial_type: - indicates one-step or two-step trial as a string (None, Plus15, Minus15)
        :attribute trial_num: - integer number of the trial

        :attribute poses: - pandas dataframe containing the object's trajectory (as floats)
        :attribute filtered: - boolean that indicates whether trial has been filtered or not
        :attribute ideal_poses: - pandas dataframe containing the 'perfect trial' line that we will compare our trial to using Frechet Distance.
        This 'perfect trial' line is a line that travels in the trial direction (with no deviations) to the max travel distance the 
        trial got to in the respective direction. This is denoted as the projection of the object trajectory on the direction

        :attribute total_distance: - float value
        :attribute frechet_distance: - float value
        :attribute dist_along_translation: - float
        :attribute dist_along_twist: - float
        """
        if file_name:
            s, h, t, r, e = file_name.split("_")
            n, _ = e.split(".")
            self.hand = HandObj(h)

            # Data will not be filtered in this step
            data = self._read_file(file_name, norm_data=norm_data)
            self.poses = data[["x", "y", "rmag"]]

        else:
            s, t, r, n = None, None, None, None
            self.hand = None

        self.subject = s
        self.trial_translation = t
        self.trial_rotation = r
        self.trial_num = n

        if file_name:
            print(self.generate_name())

        self.filtered = False
        self.window_size = 0

        self.target_line = None  # the straight path in the direction that this trial is
        self.target_rotation = None

        # metrics - predefining them
        self.total_distance = None
        self.max_error = None
        self.translation_fd = None
        self.rotation_fd = None
        self.fd = None
        self.mvt_efficiency = None
        self.arc_len = None
        self.area_btwn = None
        self.max_area_region = None
        self.max_area_loc = None
        self.metrics = None  # pd series that contains all metrics in it... TODO: to replace the rest later

        if file_name:
            self.target_line, self.total_distance = self.generate_target_line(
                100)  # 100 samples
            self.target_rotation = self.generate_target_rot(
            )  # TODO: doesn't work for true cw and ccw yet

            if do_metrics and self.poses is not None:
                self.update_all_metrics()

    def add_hand(self, hand_name):
        """
        If you didn't make the object with a file_name, a function to set hand in painless manner
        :param hand_name: name of hand to make
        """
        self.hand = HandObj(hand_name)

    def _read_file(self, file_name, folder="aruco_data/", norm_data=True):
        """
        Function to read file and save relevant data in the object
        :param file_name: name of file to read in
        :param folder: name of folder to read file from. Defaults csv folder
        """
        total_path = f"{folder}{file_name}"
        try:
            df_temp = pd.read_csv(total_path, skip_blank_lines=True)
            df = self._condition_df(df_temp, norm_data=norm_data)

        except Exception as e:  # TODO: add more specific except clauses
            # print(e)
            df = None
            print(f"{total_path} has failed to read csv")
        return df

    def _condition_df(self, df, norm_data=True):
        """
        Data conditioning procedure used to:
        0) Make columns of the dataframe numeric (they aren't by default), makes dataframe header after the fact to avoid errors with apply function
        1) convert translational data from meters to mm
        2) normalize translational data by hand span/depth
        3) remove extreme outlier values in data
        """
        # TODO: move to aruco pose detection object?
        # df_numeric = df.apply(pd.to_numeric)
        df = df.set_index("frame")

        # df_numeric.columns = ["pitch", "rmag", "roll", "tmag", "x", "y", "yaw", "z"]
        # TODO: is there a way I can make this directly hit each column without worrying about the order?
        # convert m to mm in translational data
        df = df * [1., 1., 1., 1000., 1000., 1000., 1., 1000.]
        df.round(4)

        if norm_data:
            # normalize translational data by hand span
            df = df / [
                1.,
                1.,
                1.,  # orientation data
                1.,  # translational magnitude, don't use
                self.hand.span,  # x
                self.hand.depth,  # y
                1.,  # yaw
                1.
            ]  # z - doesn't matter
            df = df.round(4)

        # occasionally get an outlier value (probably from vision algorithm), I filter them out here
        inlier_df = self._remove_outliers(df, ["x", "y", "rmag"])
        return inlier_df.round(4)

    def is_ast_trial(self):
        return isinstance(self, AsteriskTrialData)

    def is_trial(self,
                 subject_name,
                 hand_name,
                 translation_name,
                 rotation_name,
                 trial_num=None):
        """  TODO: not tested yet
        a function that returns whether this trial is equivalent to the parameters listed
        :param subject_name: name of subject
        :param hand_name: name of hand
        :param translation_name: name of translation trial
        :param rotation_name: name of rotation trial
        :param trial_num: trial number, default parameter
        """
        # TODO: make with *args instead, that way we can specify as much as we want to
        if subject_name == self.subject and hand_name == self.hand.get_name() \
                and translation_name == self.trial_translation \
                and rotation_name == self.trial_rotation:
            if trial_num and trial_num == self.trial_num:
                return True
            elif trial_num:
                return False
            else:
                return True
        else:
            return False

    def generate_name(self):
        """
        Generates the codified name of the trial
        :return: string name of trial
        """
        return f"{self.subject}_{self.hand.get_name()}_{self.trial_translation}_" \
               f"{self.trial_rotation}_{self.trial_num}"

    def save_data(self, file_name_overwrite=None):
        """
        Saves pose data as a new csv file
        :param file_name_overwrite: optional parameter, will save as generate_name unless a different name is specified
        """
        folder = "trial_paths/"
        if file_name_overwrite is None:
            new_file_name = f"{self.generate_name()}.csv"

        else:
            new_file_name = f"{file_name_overwrite}.csv"

        # if data has been filtered, we also want to include that in csv generation,
        # otherwise the filtered columns won't exist
        if self.filtered:
            filtered_file_name = f"{folder}f{self.window_size}_{new_file_name}"

            self.poses.to_csv(
                filtered_file_name,
                index=True,
                columns=["x", "y", "rmag", "f_x", "f_y", "f_rmag"])
        else:
            self.poses.to_csv(f"{folder}{new_file_name}",
                              index=True,
                              columns=["x", "y", "rmag"])

        # print(f"CSV File generated with name: {new_file_name}")

    def _remove_outliers(self, df_to_fix, columns):
        """
        Removes extreme outliers from data, in 99% quartile.
        Occasionally this happens in the aruco analyzed data and is a necessary function to run.
        These values completely mess up the moving average filter unless they are dealt with earlier.
        :param df_to_fix: the dataframe to fix
        :param columns: dataframe columns to remove outliers from
        """
        for col in columns:
            # see: https://stackoverflow.com/questions/23199796/detect-and-exclude-outliers-in-pandas-data-frame
            # q_low = df_to_fix[col].quantile(0.01)
            q_hi = df_to_fix[col].quantile(0.99)

            df_to_fix = df_to_fix[(
                df_to_fix[col] < q_hi)]  # this has got to be the problem line

            # print(col)
            # print(f"q_low: {q_low}")
            # print(f"q_hi: {q_hi}")
            # print(" ")

        return df_to_fix

    def moving_average(self, window_size=15):
        """
        Runs a moving average on the pose data. Saves moving average data into new columns with f_ prefix.
        Overwrites previous moving average calculations.
        :param window_size: size of moving average. Defaults to 15.
        """
        # TODO: makes a bunch of nan values at end of data
        self.poses["f_x"] = self.poses["x"].rolling(window=window_size,
                                                    min_periods=1).mean()
        self.poses["f_y"] = self.poses["y"].rolling(window=window_size,
                                                    min_periods=1).mean()
        self.poses["f_rmag"] = self.poses["rmag"].rolling(
            window=window_size, min_periods=1).mean()

        self.poses = self.poses.round(4)
        self.filtered = True
        self.window_size = window_size

    def get_poses(self, use_filtered=True):
        """
        Separates poses into x, y, theta for easy plotting.
        :param: use_filtered: Gives option to return filtered or unfiltered data
        """
        if self.filtered and use_filtered:
            x = self.poses["f_x"]
            y = self.poses["f_y"]
            twist = self.poses["f_rmag"]
        else:
            x = self.poses["x"]
            y = self.poses["y"]
            twist = self.poses["rmag"]

        return_x = pd.Series.to_list(x.dropna())
        return_y = pd.Series.to_list(y.dropna())
        return_twist = pd.Series.to_list(twist.dropna())

        return return_x, return_y, return_twist

    def plot_trial(self, use_filtered=True, show_plot=True, save_plot=False):
        """
        Plot the poses in the trial, using marker size to denote the error in twist from the desired twist
        :param use_filtered: Gives option to return filtered or unfiltered data
        :param show_plot: flag to show plot. Default is true
        :param save_plot: flat to save plot as a file. Default is False
        """
        data_x, data_y, theta = self.get_poses(use_filtered)

        # experimenting...
        # junk = 70
        # data_x = data_x[0:(len(data_x)-junk)]
        # data_y = data_y[0:(len(data_y)-junk)]

        plt.plot(data_x, data_y, color="xkcd:dark blue", label='trajectory')

        # plot data points separately to show angle error with marker size
        for n in range(len(data_x)):
            # TODO: rn having difficulty doing marker size in a batch, so plotting each point separately
            plt.plot(data_x[n],
                     data_y[n],
                     'r.',
                     alpha=0.5,
                     markersize=5 * theta[n])

        target_x, target_y = [], []
        for t in self.target_line:
            target_x.append(t[0])
            target_y.append(t[1])

        #target_x, target_y = aplt.get_direction(self.trial_translation)
        plt.plot(target_x,
                 target_y,
                 color="xkcd:pastel blue",
                 label="target_line",
                 linestyle="-")

        max_x = max(data_x)
        max_y = max(data_y)
        min_x = min(data_x)
        min_y = min(data_y)

        plt.xlabel('X')
        plt.ylabel('Y')
        plt.title('Path of Object')

        # gives a realistic view of what the path looks like
        plt.xticks(np.linspace(aplt.round_half_down(min_x, decimals=2),
                               aplt.round_half_up(max_x, decimals=2), 10),
                   rotation=30)
        if self.trial_translation in ["a", "b", "c", "g", "h"]:
            plt.yticks(
                np.linspace(0, aplt.round_half_up(max_y, decimals=2), 10))
        else:
            plt.yticks(
                np.linspace(aplt.round_half_down(min_y, decimals=2), 0, 10))

        # experimenting...
        # plt.xticks(np.linspace(-10,
        #                        80, 10), rotation=30)
        # plt.yticks(np.linspace(-10, 80, 10))

        # plt.gca().set_aspect('equal', adjustable='box')

        plt.title(f"Plot: {self.generate_name()}")

        if save_plot:
            plt.savefig(f"pics/plot_{self.generate_name()}.jpg", format='jpg')
            # name -> tuple: subj, hand  names
            print("Figure saved.")
            print(" ")

        if show_plot:
            plt.legend()
            plt.show()

    def get_last_pose(self):
        """
        Returns last pose as an array. Returns both filtered and unfiltered data if obj is filtered
        """
        return self.poses.dropna().tail(1).to_numpy()[0]

    def generate_target_line(self, n_samples=100):
        """
        Using object trajectory (self.poses), build a line to compare to for frechet distance.
        Updates this attribute on object.
        :param n_samples: number of samples for target line. Defaults to 100
        """
        x_vals, y_vals = aplt.get_direction(self.trial_translation, n_samples)

        target_line = np.column_stack((x_vals, y_vals))

        # get last object pose and use it for determining how far target line should go
        last_obj_pose = self.poses.tail(1).to_numpy()[0]

        target_line_length = acalc.narrow_target(last_obj_pose, target_line)

        if target_line_length:
            distance_travelled = acalc.t_distance(
                [0, 0], target_line[target_line_length + 1])
            final_target_ln = target_line[:target_line_length]
        else:
            distance_travelled = acalc.t_distance([0, 0], target_line[0])
            final_target_ln = target_line[:1]

        # TODO: distance travelled has error because it is built of target line... maybe use last_obj_pose instead?
        return final_target_ln, distance_travelled

    def generate_target_rot(self, n_samples=50):
        """
        get target rotation to compare to with fd
        :param n_samples: number of samples for target line. TODO: Currently not used
        """
        if self.trial_rotation in ["cw", "ccw"]:
            if self.filtered:
                last_rot = self.poses.tail(1)["f_rmag"]
            else:
                last_rot = self.poses.tail(1)["rmag"]

            target_rot = pd.Series.to_list(last_rot)

        # TODO: we compute rotation magnitude, so no neg values ever show up, revisit how rotation is calc'd?
        # elif self.trial_rotation == "ccw":
        #     last_rot = self.poses.tail["rmag"]
        #     target_rot = np.array([-last_rot])

        elif self.trial_rotation in ["p15", "m15"]:
            target_rot = np.array([15])

        # elif self.trial_rotation == "m15":
        #     target_rot = np.array([-15])

        else:
            target_rot = np.zeros(1)

        return target_rot

    def calc_rot_err(self, use_filtered=True):
        """
        calculate and return the error in rotation for every data point
        :param: use_filtered: Gives option to return filtered or unfiltered data
        """

        if self.filtered and use_filtered:
            rots = self.poses["f_rmag"]
        else:
            rots = self.poses["rmag"]

        # subtract desired rotation
        rots = rots - self.target_rotation

        return pd.Series.to_list(rots)

    def update_all_metrics(self, use_filtered=True):
        """
        Updates all metric values on the object.
        """ # TODO: make a pandas dataframe that contains the metrics? Easier to organize
        self.translation_fd, self.rotation_fd = acalc.calc_frechet_distance(
            self)
        # self.fd = am.calc_frechet_distance_all(self)
        self.max_error = acalc.calc_max_error(self)
        self.mvt_efficiency, self.arc_len = acalc.calc_mvt_efficiency(self)
        self.area_btwn = acalc.calc_area_btwn_curves(self)
        self.max_area_region, self.max_area_loc = acalc.calc_max_area_region(
            self)

        metric_dict = {
            "trial": self.generate_name(),
            "t_fd": self.translation_fd,
            "r_fd": self.rotation_fd,  # "fd": self.fd
            "max_err": self.max_error,
            "mvt_eff": self.mvt_efficiency,
            "arc_len": self.arc_len,
            "area_btwn": self.area_btwn,
            "max_a_reg": self.max_area_region,
            "max_a_loc": self.max_area_loc
        }

        self.metrics = pd.Series(metric_dict)
        return self.metrics

    def print_metrics(self):
        """
        Print out a report with all the metrics, useful for debugging
        """
        pass
Exemplo n.º 5
0
class AsteriskHandData:
    # TODO: add ability to add trials after the fact?
    def __init__(self, subjects, hand_name, rotation=None):
        """
        Class to hold all the data pertaining to a specific hand.
        Combines data from all subjects
        :param subjects: subjects to include in hand data object
        :param hand_name: name of hand for this object
        """
        self.hand = HandObj(hand_name)
        self.subjects_containing = subjects
        self.data = self._gather_hand_data(subjects, rotation)
        self.filtered = False
        self.window_size = None
        self.averages = []

    def _gather_hand_data(self, subjects, rotation=None):
        """
        Returns a dictionary with the data for the hand, sorted by task.
        Each key,value pair of dictionary is:
        key: name of task, string. Ex: "a_n"
        value: list of AsteriskTrial objects for the corresponding task, with all subjects specified
        :param subjects: list of subjects to get
        """
        data_dictionary = dict()
        if rotation is None:
            for t, r in datamanager.generate_t_r_pairs(self.hand.get_name()):
                key = f"{t}_{r}"
                data_dictionary[key] = self._make_asterisk_trials(subjects, t, r,
                                                                  datamanager.generate_options("numbers"))

        elif rotation in ["n", "m15", "p15"]:  # TODO: also add a check for just cw and ccw
            for t in ["a", "b", "c", "d", "e", "f", "g", "h"]:
                key = f"{t}_{rotation}"
                data_dictionary[key] = self._make_asterisk_trials(subjects, t, rotation,
                                                                  datamanager.generate_options("numbers"))

        elif rotation in ["cw", "ccw"]:
            key = f"n_{rotation}"
            data_dictionary[key] = self._make_asterisk_trials(subjects, "n", rotation,
                                                              datamanager.generate_options("numbers"))

        else:
            print("invalid key")
            data_dictionary = None

        return data_dictionary

    def _make_asterisk_trials(self, subjects, translation_label, rotation_label, trials):
        """
        Goes through data and compiles data with set attributes into an AsteriskTrial objects
        :param subjects: name of subject
        :param translation_label: name of translation trials
        :param rotation_label: name of rotation trials
        :param trial_num: trial numbers to include, default parameter
        """
        gathered_data = list()
        for s in subjects:  # TODO: subjects is a list, make a type recommendation?
            for n in trials:
                try:
                    asterisk_trial_file = f"{s}_{self.hand.get_name()}_{translation_label}_{rotation_label}_{n}.csv"

                    trial_data = trial.AsteriskTrialData(asterisk_trial_file)

                    gathered_data.append(trial_data)

                except Exception as e:
                    print(e)
                    print("Skipping.")
                    continue

        return gathered_data

    def add_trial(self, ast_trial):
        """
        add an ast_trial after the asteriskhanddata object was created
        :param ast_trial: asterisktrialdata to add
        """
        label = f"{ast_trial.trial_translation}_{ast_trial.trial_rotation}"
        self.data[label].append(ast_trial)

    def _get_ast_set(self, subjects, trial_number=None, rotation_type="n"):
        """
        Picks out an asterisk of data (all translational directions) with specific parameters
        :param subjects: specify the subject or subjects you want
        :param trial_number: specify the number trial you want, if None then it will
            return all trials for a specific subject
        :param rotation_type: rotation type of batch. Defaults to "n"
        """
        dfs = []
        translations = ["a", "b", "c", "d", "e", "f", "g", "h"]

        for direction in translations:
            dict_key = f"{direction}_{rotation_type}"
            trials = self.data[dict_key]
            # print(f"For {subject_to_run} and {trial_number}: {direction}")

            for t in trials:
                # print(t.generate_name())
                if trial_number:  # if we want a specific trial, look for it
                    if (t.subject == subjects) and (t.trial_num == trial_number):
                        dfs.append(t)
                    elif (t.subject in subjects) and (t.trial_num == trial_number):
                        dfs.append(t)

                else:  # otherwise, grab trial as long as it has the right subject
                    if t.subject == subjects or t.subject in subjects:
                        dfs.append(t)

        return dfs

    def _get_ast_dir(self, direction_label, subjects, rotation_label="n"):
        """
        Get all of the trials for a specific direction. You can specify subject too
        :param direction_label: translation direction
        :param subjects: subject or list of subjects to include
        :param rotation_label: rotation label, defaults to "n"
        """
        dict_key = f"{direction_label}_{rotation_label}"
        direction_trials = self.data[dict_key]
        dfs = []

        for t in direction_trials:
            if t.subject == subjects or t.subject in subjects:
                dfs.append(t)

        return dfs

    def _average_dir(self, translation, rotation, subject=None):
        """
        Averages a set of asterisk_trial paths. We run this on groups of paths of the same direction.
        :param translation: trial direction to average
        :param rotation: trial rotation to average
        :param subject: subject or list of subjects to average, optional. If not provided, defaults to all subjects
        :return returns averaged path
        """
        if subject is None:  # get batches of data by trial type, if no subjects given, defaults to all subjects
            trials = self._get_ast_dir(translation, self.subjects_containing, rotation)

        else:
            trials = self._get_ast_dir(translation, subject, rotation)

        average = AveragedTrial()
        # average.make_average_line(trials)
        average.calculate_avg_line(trials)
        return average

    def replace_trial_data(self, trial_obj):
        """
        Delete trial data obj from stored data and replace with new trial data obj
        Gets attributes of obj to delete from the obj passed in
        """
        # TODO: implement this
        pass

    def calc_avg_ast(self, subjects=None, rotation=None):
        """
        calculate and store all averages
        :param subjects: subject(s) to include in the average. Defaults to all subjects in object
        :param rotation: refers to the rotation type ("n", "m15", "p15"). Defaults to all
        """
        averages = []
        if subjects is None:  # if no subjects given, defaults to all subjects
            subjects = self.subjects_containing

        if rotation is None:
            # TODO: make this smarter, so that we base the list on what exists on object
            for t, r in datamanager.generate_t_r_pairs(self.hand.get_name()):
                avg = self._average_dir(t, r, subjects)
                averages.append(avg)
        else:
            for t in ["a", "b", "c", "d", "e", "f", "g", "h"]:
                avg = self._average_dir(t, rotation, subjects)
                averages.append(avg)

        self.averages = averages
        return averages

    def filter_data(self, window_size=15):
        """
        Runs moving average on data stored inside object
        :param window_size: size of moving average. default is 15
        """
        for key in self.data.keys():
            for t in self.data[key]:
                t.moving_average(window_size)

        self.filtered = True
        self.window_size = window_size

    def save_all_data(self):
        """
        Saves each AsteriskTrialObject as a csv file
        """
        for key in self.data.keys():
            for t in self.data[key]:
                t.save_data()
                # print(f"Saved: {t.generate_name()}")

    def _make_plot(self, trials, use_filtered=True, stds=False, linestyle="solid"):
        """
        Function to make our plots.
        :param trials: either a list of AsteriskTrialData or AsteriskAverage objs
        :param use_filtered: flag whether to use filtered data. Default is True
        :param stds: flag whether to plot standard deviations. Only for AsteriskAverage objects. Default is False
        """
        # TODO: plot orientation error
        colors = ["tab:blue", "tab:purple", "tab:red", "tab:olive",
                  "tab:cyan", "tab:green", "tab:pink", "tab:orange"]

        # plot data
        for i, t in enumerate(trials):
            data_x, data_y, theta = t.get_poses(use_filtered)

            plt.plot(data_x, data_y, color=colors[i], label='trajectory', linestyle=linestyle)

            if stds: # only for AsteriskAverage objs
                t.plot_sd(colors[i])

        # plot target lines as dotted lines
        self.plot_all_target_lines(colors)
        plt.title(f"{self.hand.get_name()} avg asterisk, rot: {trials[0].trial_rotation}")
        plt.xticks(np.linspace(-0.6, 0.6, 13), rotation=30)
        plt.yticks(np.linspace(-0.6, 0.6, 13))
        plt.gca().set_aspect('equal', adjustable='box')
        return plt

    def _make_fd_plot(self, trials):
        """
        make a bar plot with fd values for each direction
        :param trials: a set of asterisktrials or average objects, best if its an asterisk of data with no repeats
        """
        for i, t in enumerate(trials):  # TODO: make it work with plotting multiple values for one label?
            # plot the fd values for that direction
            trial_label = f"{t.trial_translation}_{t.trial_rotation}"
            t_fd = t.translation_fd
            r_fd = t.rotation_fd  # TODO: we don't do anything with r_fd but fd will only return max error anyway

            plt.bar(trial_label, t_fd)

        return plt

    def plot_data_subset(self, subjects, trial_number="1", show_plot=True, save_plot=False):
        """
        Plots a subset of the data, as specified in parameters
        :param subjects: subjects or list of subjects,
        :param trial_number: the number of trial to include
        :param show_plot: flag to show plot. Default is true
        :param save_plot: flat to save plot as a file. Default is False
        """
        dfs = self._get_ast_set(subjects, trial_number)  # TODO: make this work for the hand data object
        plt = self._make_plot(dfs)
        plt.title(f"Plot: {self.hand.get_name()}, {subjects}, set #{trial_number}")

        if save_plot:
            plt.savefig(f"pics/fullplot4_{self.hand.get_name()}_{subjects}_{trial_number}.jpg", format='jpg')
            # name -> tuple: subj, hand  names
            print("Figure saved.")
            print(" ")

        if show_plot:
            plt.legend()
            plt.show()

            # TODO: add ability to make comparison plot between n, m15, and p15
            # TODO: have an ability to plot a single average trial

    def plot_avg_data(self, rotation="n", subjects=None, show_plot=True, save_plot=False, linestyle="solid", plot_contributions=True):
        """
        Plots the data from one subject, averaging all of the data in each direction
        :param subjects: list of subjects. If none is provided, uses all of them
        :param rotation: the type of rotation type to plot, will collect an asterisk of this
        :param show_plot: flag to show plot. Default is true
        :param save_plot: flat to save plot as a file. Default is False
        """
        if subjects is None:
            subjects = self.subjects_containing

        # TODO: check that specifying subjects works ok when an average was already calculated
        if self.averages:
            avgs = self.averages

        else:
            avgs = self.calc_avg_ast(subjects, rotation)

        plt = self._make_plot(avgs, use_filtered=False, stds=True, linestyle=linestyle)

        if plot_contributions:
            for a in avgs:
                a.plot_line_contributions()

        plt.title(f"Avg {self.hand.get_name()}, {subjects}, {rotation}")

        if save_plot:
            plt.savefig(f"pics/avgd_{self.hand.get_name()}_{len(self.subjects_containing)}subs_{rotation}.jpg", format='jpg')

            # name -> tuple: subj, hand  names
            print("Figure saved.")
            print(" ")

        if show_plot:
            # plt.legend()  # TODO: showing up weird, need to fix
            plt.show()

    def plot_orientation_errors(self, translation, subject=None, rotation="n", show_plot=True, save_plot=False):
        """
        line plot of orientation error throughout a trial for a specific direction
        :param translation: the type of translation
        :param subject: list of subjects. If none is provided, uses all of them
        :param rotation: type of rotation. Defaults to "n"
        :param show_plot: flag to show plot. Default is true
        :param save_plot: flat to save plot as a file. Default is False
        """
        if subject:
            trials = self._get_ast_dir(translation, subject, rotation)
        else:
            trials = self._get_ast_dir(translation, self.subjects_containing, rotation)

        # if self.averages and incl_avg:  # TODO: have an option to include the average?
        #     for a in self.averages:
        #         if a.trial_translation==direction_label and a.trial_rotation==rotation_label:
        #             trials.append(a)

        for t in trials:
            rot_err = t.calc_rot_err()
            # currently using the get_c function to generate a normalized set of x values to use as x values
            x, _ = aplt.get_c(len(rot_err))  # will need to multiply by 2 to get it to go to 1.0 instead of 0.5
            plt.plot(2*x, rot_err, label=f"Orientation Err {t.subject}, trial {t.trial_num}")

        if save_plot:
            if subject:
                plt.savefig(f"pics/angerror_{self.hand.get_name()}_{subject}_{translation}_{rotation}.jpg", format='jpg')
            else:
                plt.savefig(f"pics/angerror_{self.hand.get_name()}_all_{translation}_{rotation}.jpg", format='jpg')
            # name -> tuple: subj, hand  names
            print("Figure saved.")
            print(" ")

        if show_plot:
            plt.legend()
            plt.show()

    def plot_fd_set(self, subjects, trial_number="1", rotation="n", show_plot=True, save_plot=False):
        """  # TODO: still need to test
        plots the frechet distance values of an asterisk of data specified in the parameters
        :param subject: list of subjects. If none is provided, uses all of them
        :param trial_number: the trial number to choose. Defaults to "1"
        :param rotation: type of rotation. Defaults to "n"
        :param show_plot: flag to show plot. Default is true
        :param save_plot: flat to save plot as a file. Default is False
        """
        trials = self._get_ast_set(subjects, trial_number, rotation)
        # dirs = ["a", "b", "c", "d", "e", "f", "g", "h"]  # TODO: add cw and ccw later, once index issue is fixed

        plt = self._make_fd_plot(trials)
        if subjects:
            plt.title(f"FD {self.hand.get_name()}, {subjects}, {rotation}")
        else:
            plt.title(f"Frechet Distance {self.hand.get_name()}, {rotation}")

        if save_plot:
            if subjects:
                plt.savefig(f"pics/fds_{self.hand.get_name()}_{subjects}_{rotation}.jpg", format='jpg')
            else:
                plt.savefig(f"pics/fds_{self.hand.get_name()}_all_{rotation}.jpg", format='jpg')
            # name -> tuple: subj, hand  names
            print("Figure saved.")
            print(" ")

        if show_plot:
            plt.legend()
            plt.show()

    def plot_avg_fd(self, subjects=None, rotation="n", show_plot=True, save_plot=False):
        """  # TODO: still need to test
        plots averaged fd values in a bar chart
        :param subject: list of subjects. If none is provided, uses all of them
        :param rotation: type of rotation. Defaults to "n"
        :param show_plot: flag to show plot. Default is true
        :param save_plot: flat to save plot as a file. Default is False
        """
        trials = self.averages
        # dirs = ["a", "b", "c", "d", "e", "f", "g", "h"]  # TODO: add cw and ccw later, once index issue is fixed

        plt = self._make_fd_plot(trials)
        if subjects:
            plt.title(f"Avg FD {self.hand.get_name()}, {subjects}, {rotation}")
        else:
            plt.title(f"Avg FD {self.hand.get_name()}, {rotation}")

        if save_plot:
            if subjects:
                plt.savefig(f"pics/fd_avg_{self.hand.get_name()}_{subjects}_{rotation}.jpg", format='jpg')
            else:
                plt.savefig(f"pics/fd_avg_{self.hand.get_name()}_all_{rotation}.jpg", format='jpg')
            # name -> tuple: subj, hand  names
            print("Figure saved.")
            print(" ")

        if show_plot:
            plt.legend()
            plt.show()

    def plot_all_target_lines(self, order_of_colors):
        """
        Plot all target lines on a plot for easy reference
        :param order_of_colors:
        """
        x_a, y_a = aplt.get_a()
        x_b, y_b = aplt.get_b()
        x_c, y_c = aplt.get_c()
        x_d, y_d = aplt.get_d()
        x_e, y_e = aplt.get_e()
        x_f, y_f = aplt.get_f()
        x_g, y_g = aplt.get_g()
        x_h, y_h = aplt.get_h()

        ideal_xs = [x_a, x_b, x_c, x_d, x_e, x_f, x_g, x_h]
        ideal_ys = [y_a, y_b, y_c, y_d, y_e, y_f, y_g, y_h]

        for i in range(8):
            plt.plot(ideal_xs[i], ideal_ys[i], color=order_of_colors[i], label='ideal', linestyle='--')