def propagate_ages(self):
        """
        Mine ages table for any age data, and write it into
        specimens, samples, sites, locations tables.
        Do not overwrite existing age data.
        """
        # if there is no age table, skip
        if 'ages' not in self.tables:
            return
        # if age table has no data, skip
        if not len(self.tables['ages'].df):
            return
        # get levels in age table
        self.get_age_levels()
        # if age levels could not be determined, skip
        if not "level" in self.tables["ages"].df.columns:
            return
        if not any(self.tables["ages"].df["level"]):
            return
        # go through each level of age data
        for level in self.tables['ages'].df['level'].unique():
            table_name = level + 's'
            age_headers = self.data_model.get_group_headers(table_name, 'Age')
            # find age headers that are actually in table
            actual_age_headers = list(set(self.tables[table_name].df.columns).intersection(age_headers))
            # find site age headers that are available in ages table
            available_age_headers = list(set(self.tables['ages'].df.columns).intersection(age_headers))
            # fill in all available age info to all rows
            self.tables[table_name].front_and_backfill(actual_age_headers)
            # add any available headers to table
            add_headers = set(available_age_headers).difference(actual_age_headers)
            for header in add_headers:
                self.tables[table_name].df[header] = None
            # propagate values from ages into table
            def move_values(ser, level, available_headers):
                name = ser.name
                cond1 = self.tables['ages'].df[level] == name
                cond2 = self.tables['ages'].df['level'] == level
                mask = cond1 & cond2
                sli = self.tables['ages'].df[mask]
                if len(sli):
                    return list(sli[available_headers].values[0])
                return [None] * len(available_headers)

            res = self.tables[table_name].df.apply(move_values, axis=1,
                                                   args=[level, available_age_headers])
            # fill in table with values gleaned from ages
            new_df = pd.DataFrame(data=list(res.values), index=res.index,
                                  columns=available_age_headers)
            age_values = np.where(self.tables[table_name].df[available_age_headers],
                                  self.tables[table_name].df[available_age_headers],
                                  new_df)
            self.tables[table_name].df[available_age_headers] = age_values
        #
        # put age_high, age_low into locations table
        print("-I- Adding age_high and age_low to locations table based on minimum/maximum ages found in sites table")
        self.propagate_min_max_up(cols=['age'], target_df_name='locations',
                                  source_df_name='sites')
Mine ages table for any age data, and write it into
        specimens, samples, sites, locations tables.
        Do not overwrite existing age data.