From a6b7ed2c6cb892e1a357fec13b6594b410e90f49 Mon Sep 17 00:00:00 2001 From: weygoldt <88969563+weygoldt@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:19:19 +0100 Subject: [PATCH] merging diverged --- code/CTCPTC.py | 86 ++++++++++++- code/eventchirpsplots.py | 188 +++++++++++++++++------------ code/extract_chirps.py | 29 +++-- code/modules/behaviour_handling.py | 98 ++++++++------- poster/main.pdf | Bin 379323 -> 403471 bytes poster/main.tex | 8 +- 6 files changed, 271 insertions(+), 138 deletions(-) diff --git a/code/CTCPTC.py b/code/CTCPTC.py index 1935ba2..82fb2c2 100644 --- a/code/CTCPTC.py +++ b/code/CTCPTC.py @@ -1,8 +1,8 @@ -import os +import os import numpy as np import pandas as pd -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt from tqdm import tqdm from IPython import embed @@ -11,8 +11,88 @@ from modules.logger import makeLogger from modules.plotstyle import PlotStyle from modules.datahandling import flatten from modules.behaviour_handling import Behavior, correct_chasing_events, event_triggered_chirps +from extract_chirps import get_valid_datasets logger = makeLogger(__name__) ps = PlotStyle() -#### Goal: CTC & PTC for each winner and loser and for all winners and loser #### + +def get_chirp_winner_loser(folder_name, Behavior, order_meta_df): + + foldername = folder_name.split('/')[-2] + winner_row = order_meta_df[order_meta_df['recording'] == foldername] + winner = winner_row['winner'].values[0].astype(int) + winner_fish1 = winner_row['fish1'].values[0].astype(int) + winner_fish2 = winner_row['fish2'].values[0].astype(int) + + if winner > 0: + if winner == winner_fish1: + winner_fish_id = winner_row['rec_id1'].values[0] + loser_fish_id = winner_row['rec_id2'].values[0] + + elif winner == winner_fish2: + winner_fish_id = winner_row['rec_id2'].values[0] + loser_fish_id = winner_row['rec_id1'].values[0] + + chirp_winner = Behavior.chirps[Behavior.chirps_ids == winner_fish_id] + chirp_loser = Behavior.chirps[Behavior.chirps_ids == loser_fish_id] + + return chirp_winner, chirp_loser + else: + return None, None + + +def main(dataroot): + + foldernames, _ = get_valid_datasets(dataroot) + + meta_path = ( + '/').join(foldernames[0].split('/')[:-2]) + '/order_meta.csv' + meta = pd.read_csv(meta_path) + meta['recording'] = meta['recording'].str[1:-1] + + winner_chirps = [] + loser_chirps = [] + onsets = [] + offsets = [] + physicals = [] + + # Iterate over all recordings and save chirp- and event-timestamps + for folder in foldernames: + + logger.info('Loading data from folder: {}'.format(folder)) + + time_before = 30 + time_after = 60 + dt = 0.1 + kernel_width = 2 + kde_time = np.arange(-time_before, time_after, dt) + + broken_folders = ['../data/mount_data/2020-05-12-10_00/'] + if folder in broken_folders: + continue + + bh = Behavior(folder) + winner, loser = get_chirp_winner_loser(folder, bh, meta) + + if winner is None: + continue + + # Chirps are already sorted + winner_chirps.append(bh.chirps) + loser_chirps.append(bh.chirps) + + # Correct for doubles in chasing on- and offsets to get the right on-/offset pairs + # Get rid of tracking faults (two onsets or two offsets after another) + category, timestamps = correct_chasing_events(bh.behavior, bh.start_s) + + # Split categories + onsets.append(timestamps[category == 0]) + offsets.append(timestamps[category == 1]) + physicals.append(timestamps[category == 2]) + + # center chirps around events + + +if __name__ == '__main__': + main('../data/mount_data/') diff --git a/code/eventchirpsplots.py b/code/eventchirpsplots.py index 9117743..cd5c364 100644 --- a/code/eventchirpsplots.py +++ b/code/eventchirpsplots.py @@ -1,8 +1,8 @@ -import os +import os import numpy as np import pandas as pd -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt from tqdm import tqdm from IPython import embed @@ -14,42 +14,49 @@ from modules.datahandling import causal_kde1d, acausal_kde1d, flatten logger = makeLogger(__name__) ps = PlotStyle() + class Behavior: """Load behavior data from csv file as class attributes Attributes ---------- behavior: 0: chasing onset, 1: chasing offset, 2: physical contact - behavior_type: - behavioral_category: - comment_start: - comment_stop: - dataframe: pandas dataframe with all the data - duration_s: - media_file: - observation_date: - observation_id: - start_s: start time of the event in seconds - stop_s: stop time of the event in seconds - total_length: + behavior_type: + behavioral_category: + comment_start: + comment_stop: + dataframe: pandas dataframe with all the data + duration_s: + media_file: + observation_date: + observation_id: + start_s: start time of the event in seconds + stop_s: stop time of the event in seconds + total_length: """ def __init__(self, folder_path: str) -> None: print(f'{folder_path}') - LED_on_time_BORIS = np.load(os.path.join(folder_path, 'LED_on_time.npy'), allow_pickle=True) - self.time = np.load(os.path.join(folder_path, "times.npy"), allow_pickle=True) - csv_filename = [f for f in os.listdir(folder_path) if f.endswith('.csv')][0] # check if there are more than one csv file + LED_on_time_BORIS = np.load(os.path.join( + folder_path, 'LED_on_time.npy'), allow_pickle=True) + self.time = np.load(os.path.join( + folder_path, "times.npy"), allow_pickle=True) + csv_filename = [f for f in os.listdir(folder_path) if f.endswith( + '.csv')][0] # check if there are more than one csv file self.dataframe = read_csv(os.path.join(folder_path, csv_filename)) - self.chirps = np.load(os.path.join(folder_path, 'chirps.npy'), allow_pickle=True) - self.chirps_ids = np.load(os.path.join(folder_path, 'chirp_ids.npy'), allow_pickle=True) + self.chirps = np.load(os.path.join( + folder_path, 'chirps.npy'), allow_pickle=True) + self.chirps_ids = np.load(os.path.join( + folder_path, 'chirp_ids.npy'), allow_pickle=True) for k, key in enumerate(self.dataframe.keys()): - key = key.lower() + key = key.lower() if ' ' in key: key = key.replace(' ', '_') if '(' in key: key = key.replace('(', '') key = key.replace(')', '') - setattr(self, key, np.array(self.dataframe[self.dataframe.keys()[k]])) + setattr(self, key, np.array( + self.dataframe[self.dataframe.keys()[k]])) last_LED_t_BORIS = LED_on_time_BORIS[-1] real_time_range = self.time[-1] - self.time[0] @@ -58,6 +65,7 @@ class Behavior: self.start_s = (self.start_s - shift) / factor self.stop_s = (self.stop_s - shift) / factor + """ 1 - chasing onset 2 - chasing offset @@ -87,16 +95,17 @@ temporal encpding needs to be corrected ... not exactly 25FPS. def correct_chasing_events( - category: np.ndarray, + category: np.ndarray, timestamps: np.ndarray - ) -> tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: onset_ids = np.arange( len(category))[category == 0] offset_ids = np.arange( len(category))[category == 1] - wrong_bh = np.arange(len(category))[category!=2][:-1][np.diff(category[category!=2])==0] + wrong_bh = np.arange(len(category))[ + category != 2][:-1][np.diff(category[category != 2]) == 0] if onset_ids[0] > offset_ids[0]: offset_ids = np.delete(offset_ids, 0) help_index = offset_ids[0] @@ -105,7 +114,6 @@ def correct_chasing_events( category = np.delete(category, wrong_bh) timestamps = np.delete(timestamps, wrong_bh) - # Check whether on- or offset is longer and calculate length difference if len(onset_ids) > len(offset_ids): len_diff = len(onset_ids) - len(offset_ids) @@ -115,21 +123,22 @@ def correct_chasing_events( logger.info(f'Offsets are greater than onsets by {len_diff}') elif len(onset_ids) == len(offset_ids): logger.info('Chasing events are equal') - + return category, timestamps def event_triggered_chirps( - event: np.ndarray, - chirps:np.ndarray, + event: np.ndarray, + chirps: np.ndarray, time_before_event: int, time_after_event: int, dt: float, width: float, - )-> tuple[np.ndarray, np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: event_chirps = [] # chirps that are in specified window around event - centered_chirps = [] # timestamps of chirps around event centered on the event timepoint + # timestamps of chirps around event centered on the event timepoint + centered_chirps = [] for event_timestamp in event: start = event_timestamp - time_before_event @@ -138,25 +147,28 @@ def event_triggered_chirps( event_chirps.append(chirps_around_event) if len(chirps_around_event) == 0: continue - else: + else: centered_chirps.append(chirps_around_event - event_timestamp) - + time = np.arange(-time_before_event, time_after_event, dt) - + # Kernel density estimation with some if's if len(centered_chirps) == 0: centered_chirps = np.array([]) centered_chirps_convolved = np.zeros(len(time)) else: - centered_chirps = np.concatenate(centered_chirps, axis=0) # convert list of arrays to one array for plotting - centered_chirps_convolved = (acausal_kde1d(centered_chirps, time, width)) / len(event) + # convert list of arrays to one array for plotting + centered_chirps = np.concatenate(centered_chirps, axis=0) + centered_chirps_convolved = (acausal_kde1d( + centered_chirps, time, width)) / len(event) return event_chirps, centered_chirps, centered_chirps_convolved def main(datapath: str): - foldernames = [datapath + x + '/' for x in os.listdir(datapath) if os.path.isdir(datapath + x)] + foldernames = [ + datapath + x + '/' for x in os.listdir(datapath) if os.path.isdir(datapath + x)] nrecording_chirps = [] nrecording_chirps_fish_ids = [] @@ -171,7 +183,7 @@ def main(datapath: str): continue bh = Behavior(folder) - + # Chirps are already sorted category = bh.behavior timestamps = bh.start_s @@ -193,14 +205,12 @@ def main(datapath: str): physical_contacts = timestamps[category == 2] nrecording_physicals.append(physical_contacts) - - # Define time window for chirps around event analysis time_before_event = 30 time_after_event = 60 dt = 0.01 width = 1.5 # width of kernel for all recordings, currently gaussian kernel - recording_width = 2 # width of kernel for each recording + recording_width = 2 # width of kernel for each recording time = np.arange(-time_before_event, time_after_event, dt) ##### Chirps around events, all fish, all recordings ##### @@ -222,14 +232,18 @@ def main(datapath: str): physical_contacts = nrecording_physicals[i] # Chirps around chasing onsets - _, centered_chasing_onset_chirps, cc_chasing_onset_chirps = event_triggered_chirps(chasing_onsets, chirps, time_before_event, time_after_event, dt, recording_width) + _, centered_chasing_onset_chirps, cc_chasing_onset_chirps = event_triggered_chirps( + chasing_onsets, chirps, time_before_event, time_after_event, dt, recording_width) # Chirps around chasing offsets - _, centered_chasing_offset_chirps, cc_chasing_offset_chirps = event_triggered_chirps(chasing_offsets, chirps, time_before_event, time_after_event, dt, recording_width) + _, centered_chasing_offset_chirps, cc_chasing_offset_chirps = event_triggered_chirps( + chasing_offsets, chirps, time_before_event, time_after_event, dt, recording_width) # Chirps around physical contacts - _, centered_physical_chirps, cc_physical_chirps = event_triggered_chirps(physical_contacts, chirps, time_before_event, time_after_event, dt, recording_width) + _, centered_physical_chirps, cc_physical_chirps = event_triggered_chirps( + physical_contacts, chirps, time_before_event, time_after_event, dt, recording_width) nrecording_centered_onset_chirps.append(centered_chasing_onset_chirps) - nrecording_centered_offset_chirps.append(centered_chasing_offset_chirps) + nrecording_centered_offset_chirps.append( + centered_chasing_offset_chirps) nrecording_centered_physical_chirps.append(centered_physical_chirps) ## Shuffled chirps ## @@ -252,7 +266,6 @@ def main(datapath: str): # _, _, cc_shuffled_physical_chirps = event_triggered_chirps(physical_contacts, shuffled_chirps, time_before_event, time_after_event, dt, recording_width) # nshuffled_physical_chirps.append(cc_shuffled_physical_chirps) - # rec_shuffled_q5_onset, rec_shuffled_median_onset, rec_shuffled_q95_onset = np.percentile( # nshuffled_onset_chirps, (5, 50, 95), axis=0) # rec_shuffled_q5_offset, rec_shuffled_median_offset, rec_shuffled_q95_offset = np.percentile( @@ -260,7 +273,6 @@ def main(datapath: str): # rec_shuffled_q5_physical, rec_shuffled_median_physical, rec_shuffled_q95_physical = np.percentile( # nshuffled_physical_chirps, (5, 50, 95), axis=0) - # #### Recording plots #### # fig, ax = plt.subplots(1, 3, figsize=(28*ps.cm, 16*ps.cm, ), constrained_layout=True, sharey='all') # ax[0].set_xlabel('Time[s]') @@ -307,7 +319,7 @@ def main(datapath: str): # fig.suptitle(f'Recording: {i}') # # plt.show() # plt.close() - + # nrecording_shuffled_convolved_onset_chirps.append(nshuffled_onset_chirps) # nrecording_shuffled_convolved_offset_chirps.append(nshuffled_offset_chirps) # nrecording_shuffled_convolved_physical_chirps.append(nshuffled_physical_chirps) @@ -319,9 +331,12 @@ def main(datapath: str): # New bootstrapping approach for n in range(nbootstrapping): - diff_onset = np.diff(np.sort(flatten(nrecording_centered_onset_chirps))) - diff_offset = np.diff(np.sort(flatten(nrecording_centered_offset_chirps))) - diff_physical = np.diff(np.sort(flatten(nrecording_centered_physical_chirps))) + diff_onset = np.diff( + np.sort(flatten(nrecording_centered_onset_chirps))) + diff_offset = np.diff( + np.sort(flatten(nrecording_centered_offset_chirps))) + diff_physical = np.diff( + np.sort(flatten(nrecording_centered_physical_chirps))) np.random.shuffle(diff_onset) shuffled_onset = np.cumsum(diff_onset) @@ -339,16 +354,18 @@ def main(datapath: str): bootstrap_physical.append(kde_physical) # New shuffle approach q5, q50, q95 - onset_q5, onset_median, onset_q95 = np.percentile(bootstrap_onset, [5, 50, 95], axis=0) - offset_q5, offset_median, offset_q95 = np.percentile(bootstrap_offset, [5, 50, 95], axis=0) - physical_q5, physical_median, physical_q95 = np.percentile(bootstrap_physical, [5, 50, 95], axis=0) - + onset_q5, onset_median, onset_q95 = np.percentile( + bootstrap_onset, [5, 50, 95], axis=0) + offset_q5, offset_median, offset_q95 = np.percentile( + bootstrap_offset, [5, 50, 95], axis=0) + physical_q5, physical_median, physical_q95 = np.percentile( + bootstrap_physical, [5, 50, 95], axis=0) # vstack um 1. Dim zu cutten # nrecording_shuffled_convolved_onset_chirps = np.vstack(nrecording_shuffled_convolved_onset_chirps) # nrecording_shuffled_convolved_offset_chirps = np.vstack(nrecording_shuffled_convolved_offset_chirps) # nrecording_shuffled_convolved_physical_chirps = np.vstack(nrecording_shuffled_convolved_physical_chirps) - + # shuffled_q5_onset, shuffled_median_onset, shuffled_q95_onset = np.percentile( # nrecording_shuffled_convolved_onset_chirps, (5, 50, 95), axis=0) # shuffled_q5_offset, shuffled_median_offset, shuffled_q95_offset = np.percentile( @@ -356,27 +373,37 @@ def main(datapath: str): # shuffled_q5_physical, shuffled_median_physical, shuffled_q95_physical = np.percentile( # nrecording_shuffled_convolved_physical_chirps, (5, 50, 95), axis=0) - # Flatten all chirps + # Flatten all chirps all_chirps = np.concatenate(nrecording_chirps).ravel() # not centered # Flatten event timestamps - all_onsets = np.concatenate(nrecording_chasing_onsets).ravel() # not centered - all_offsets = np.concatenate(nrecording_chasing_offsets).ravel() # not centered - all_physicals = np.concatenate(nrecording_physicals).ravel() # not centered + all_onsets = np.concatenate( + nrecording_chasing_onsets).ravel() # not centered + all_offsets = np.concatenate( + nrecording_chasing_offsets).ravel() # not centered + all_physicals = np.concatenate( + nrecording_physicals).ravel() # not centered # Flatten all chirps around events - all_onset_chirps = np.concatenate(nrecording_centered_onset_chirps).ravel() # centered - all_offset_chirps = np.concatenate(nrecording_centered_offset_chirps).ravel() # centered - all_physical_chirps = np.concatenate(nrecording_centered_physical_chirps).ravel() # centered + all_onset_chirps = np.concatenate( + nrecording_centered_onset_chirps).ravel() # centered + all_offset_chirps = np.concatenate( + nrecording_centered_offset_chirps).ravel() # centered + all_physical_chirps = np.concatenate( + nrecording_centered_physical_chirps).ravel() # centered # Convolute all chirps # Divide by total number of each event over all recordings - all_onset_chirps_convolved = (acausal_kde1d(all_onset_chirps, time, width)) / len(all_onsets) - all_offset_chirps_convolved = (acausal_kde1d(all_offset_chirps, time, width)) / len(all_offsets) - all_physical_chirps_convolved = (acausal_kde1d(all_physical_chirps, time, width)) / len(all_physicals) + all_onset_chirps_convolved = (acausal_kde1d( + all_onset_chirps, time, width)) / len(all_onsets) + all_offset_chirps_convolved = (acausal_kde1d( + all_offset_chirps, time, width)) / len(all_offsets) + all_physical_chirps_convolved = (acausal_kde1d( + all_physical_chirps, time, width)) / len(all_physicals) # Plot all events with all shuffled - fig, ax = plt.subplots(1, 3, figsize=(28*ps.cm, 16*ps.cm, ), constrained_layout=True, sharey='all') + fig, ax = plt.subplots(1, 3, figsize=( + 28*ps.cm, 16*ps.cm, ), constrained_layout=True, sharey='all') # offsets = np.arange(1,28,1) ax[0].set_xlabel('Time[s]') @@ -384,8 +411,10 @@ def main(datapath: str): ax[0].set_ylabel('Chirp rate [Hz]') ax[0].plot(time, all_onset_chirps_convolved, color=ps.yellow, zorder=2) ax0 = ax[0].twinx() - nrecording_centered_onset_chirps = np.asarray(nrecording_centered_onset_chirps, dtype=object) - ax0.eventplot(np.array(nrecording_centered_onset_chirps), linelengths=0.5, colors=ps.gray, alpha=0.25, zorder=1) + nrecording_centered_onset_chirps = np.asarray( + nrecording_centered_onset_chirps, dtype=object) + ax0.eventplot(np.array(nrecording_centered_onset_chirps), + linelengths=0.5, colors=ps.gray, alpha=0.25, zorder=1) ax0.vlines(0, 0, 1.5, ps.white, 'dashed') ax[0].set_zorder(ax0.get_zorder()+1) ax[0].patch.set_visible(False) @@ -400,8 +429,10 @@ def main(datapath: str): ax[1].set_xlabel('Time[s]') ax[1].plot(time, all_offset_chirps_convolved, color=ps.orange, zorder=2) ax1 = ax[1].twinx() - nrecording_centered_offset_chirps = np.asarray(nrecording_centered_offset_chirps, dtype=object) - ax1.eventplot(np.array(nrecording_centered_offset_chirps), linelengths=0.5, colors=ps.gray, alpha=0.25, zorder=1) + nrecording_centered_offset_chirps = np.asarray( + nrecording_centered_offset_chirps, dtype=object) + ax1.eventplot(np.array(nrecording_centered_offset_chirps), + linelengths=0.5, colors=ps.gray, alpha=0.25, zorder=1) ax1.vlines(0, 0, 1.5, ps.white, 'dashed') ax[1].set_zorder(ax1.get_zorder()+1) ax[1].patch.set_visible(False) @@ -416,8 +447,10 @@ def main(datapath: str): ax[2].set_xlabel('Time[s]') ax[2].plot(time, all_physical_chirps_convolved, color=ps.maroon, zorder=2) ax2 = ax[2].twinx() - nrecording_centered_physical_chirps = np.asarray(nrecording_centered_physical_chirps, dtype=object) - ax2.eventplot(np.array(nrecording_centered_physical_chirps), linelengths=0.5, colors=ps.gray, alpha=0.25, zorder=1) + nrecording_centered_physical_chirps = np.asarray( + nrecording_centered_physical_chirps, dtype=object) + ax2.eventplot(np.array(nrecording_centered_physical_chirps), + linelengths=0.5, colors=ps.gray, alpha=0.25, zorder=1) ax2.vlines(0, 0, 1.5, ps.white, 'dashed') ax[2].set_zorder(ax2.get_zorder()+1) ax[2].patch.set_visible(False) @@ -425,14 +458,15 @@ def main(datapath: str): ax2.set_yticks([]) # ax[2].fill_between(time, shuffled_q5_physical, shuffled_q95_physical, color=ps.gray, alpha=0.5) # ax[2].plot(time, shuffled_median_physical, ps.black) - ax[2].fill_between(time, physical_q5, physical_q95, color=ps.gray, alpha=0.5) + ax[2].fill_between(time, physical_q5, physical_q95, + color=ps.gray, alpha=0.5) ax[2].plot(time, physical_median, ps.black) fig.suptitle('All recordings') plt.show() plt.close() embed() - + # chasing_durations = [] # # Calculate chasing duration to evaluate a nice time window for kernel density estimation # for onset, offset in zip(chasing_onsets, chasing_offsets): @@ -444,7 +478,6 @@ def main(datapath: str): # plt.show() # plt.close() - # # Associate chirps to individual fish # fish1 = chirps[chirps_fish_ids == fish_ids[0]] # fish2 = chirps[chirps_fish_ids == fish_ids[1]] @@ -453,7 +486,6 @@ def main(datapath: str): # Convolution over all recordings # Rasterplot for each recording - # #### Chirps around events, winner VS loser, one recording #### # # Load file with fish ids and winner/loser info # meta = pd.read_csv('../data/mount_data/order_meta.csv') @@ -462,7 +494,7 @@ def main(datapath: str): # fish2 = current_recording['rec_id2'].values # # Implement check if fish_ids from meta and chirp detection are the same??? # winner = current_recording['winner'].values - + # if winner == fish1: # loser = fish2 # elif winner == fish2: @@ -546,7 +578,6 @@ def main(datapath: str): # ax5.set_yticks([]) # plt.show() # plt.close() - # for i in range(len(fish_ids)): # fish = fish_ids[i] @@ -556,7 +587,6 @@ def main(datapath: str): #### Chirps around events, only losers, one recording #### - if __name__ == '__main__': # Path to the data datapath = '../data/mount_data/' diff --git a/code/extract_chirps.py b/code/extract_chirps.py index 5d5580e..77e3e8d 100644 --- a/code/extract_chirps.py +++ b/code/extract_chirps.py @@ -7,21 +7,12 @@ from IPython import embed # check rec ../data/mount_data/2020-03-25-10_00/ starting at 3175 -def main(datapaths): - - for path in datapaths: - chirpdetection(path, plot='show') - - -if __name__ == '__main__': - - dataroot = '../data/mount_data/' +def get_valid_datasets(dataroot): datasets = sorted([name for name in os.listdir(dataroot) if os.path.isdir( os.path.join(dataroot, name))]) valid_datasets = [] - for dataset in datasets: path = os.path.join(dataroot, dataset) @@ -43,9 +34,25 @@ if __name__ == '__main__': datapaths = [os.path.join(dataroot, dataset) + '/' for dataset in valid_datasets] + return datapaths, valid_datasets + + +def main(datapaths): + + for path in datapaths: + chirpdetection(path, plot='show') + + +if __name__ == '__main__': + + dataroot = '../data/mount_data/' + + + datapaths, valid_datasets= get_valid_datasets(dataroot) + recs = pd.DataFrame(columns=['recording'], data=valid_datasets) recs.to_csv('../recs.csv', index=False) - datapaths = ['../data/mount_data/2020-03-25-10_00/'] + # datapaths = ['../data/mount_data/2020-03-25-10_00/'] main(datapaths) # window 1524 + 244 in dataset index 4 is nice example diff --git a/code/modules/behaviour_handling.py b/code/modules/behaviour_handling.py index 6641309..f846d45 100644 --- a/code/modules/behaviour_handling.py +++ b/code/modules/behaviour_handling.py @@ -1,8 +1,7 @@ import numpy as np -import os +import os -import numpy as np from IPython import embed @@ -19,46 +18,60 @@ class Behavior: Attributes ---------- behavior: 0: chasing onset, 1: chasing offset, 2: physical contact - behavior_type: - behavioral_category: - comment_start: - comment_stop: - dataframe: pandas dataframe with all the data - duration_s: - media_file: - observation_date: - observation_id: - start_s: start time of the event in seconds - stop_s: stop time of the event in seconds - total_length: + behavior_type: + behavioral_category: + comment_start: + comment_stop: + dataframe: pandas dataframe with all the data + duration_s: + media_file: + observation_date: + observation_id: + start_s: start time of the event in seconds + stop_s: stop time of the event in seconds + total_length: """ def __init__(self, folder_path: str) -> None: - LED_on_time_BORIS = np.load(os.path.join(folder_path, 'LED_on_time.npy'), allow_pickle=True) + LED_on_time_BORIS = np.load(os.path.join( + folder_path, 'LED_on_time.npy'), allow_pickle=True) + + csv_filename = os.path.split(folder_path[:-1])[-1] + csv_filename = '-'.join(csv_filename.split('-')[:-1]) + '.csv' + # embed() - csv_filename = [f for f in os.listdir(folder_path) if f.endswith('.csv')][0] + # csv_filename = [f for f in os.listdir( + # folder_path) if f.endswith('.csv')][0] logger.info(f'CSV file: {csv_filename}') self.dataframe = read_csv(os.path.join(folder_path, csv_filename)) - self.chirps = np.load(os.path.join(folder_path, 'chirps.npy'), allow_pickle=True) - self.chirps_ids = np.load(os.path.join(folder_path, 'chirp_ids.npy'), allow_pickle=True) - - self.ident = np.load(os.path.join(folder_path, 'ident_v.npy'), allow_pickle=True) - self.idx = np.load(os.path.join(folder_path, 'idx_v.npy'), allow_pickle=True) - self.freq = np.load(os.path.join(folder_path, 'fund_v.npy'), allow_pickle=True) - self.time = np.load(os.path.join(folder_path, "times.npy"), allow_pickle=True) - self.spec = np.load(os.path.join(folder_path, "spec.npy"), allow_pickle=True) + self.chirps = np.load(os.path.join( + folder_path, 'chirps.npy'), allow_pickle=True) + self.chirps_ids = np.load(os.path.join( + folder_path, 'chirp_ids.npy'), allow_pickle=True) + + self.ident = np.load(os.path.join( + folder_path, 'ident_v.npy'), allow_pickle=True) + self.idx = np.load(os.path.join( + folder_path, 'idx_v.npy'), allow_pickle=True) + self.freq = np.load(os.path.join( + folder_path, 'fund_v.npy'), allow_pickle=True) + self.time = np.load(os.path.join( + folder_path, "times.npy"), allow_pickle=True) + self.spec = np.load(os.path.join( + folder_path, "spec.npy"), allow_pickle=True) for k, key in enumerate(self.dataframe.keys()): - key = key.lower() + key = key.lower() if ' ' in key: key = key.replace(' ', '_') if '(' in key: key = key.replace('(', '') key = key.replace(')', '') - setattr(self, key, np.array(self.dataframe[self.dataframe.keys()[k]])) - + setattr(self, key, np.array( + self.dataframe[self.dataframe.keys()[k]])) + last_LED_t_BORIS = LED_on_time_BORIS[-1] real_time_range = self.time[-1] - self.time[0] factor = 1.034141 @@ -68,16 +81,17 @@ class Behavior: def correct_chasing_events( - category: np.ndarray, + category: np.ndarray, timestamps: np.ndarray - ) -> tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: onset_ids = np.arange( len(category))[category == 0] offset_ids = np.arange( len(category))[category == 1] - wrong_bh = np.arange(len(category))[category!=2][:-1][np.diff(category[category!=2])==0] + wrong_bh = np.arange(len(category))[ + category != 2][:-1][np.diff(category[category != 2]) == 0] if onset_ids[0] > offset_ids[0]: offset_ids = np.delete(offset_ids, 0) help_index = offset_ids[0] @@ -95,21 +109,22 @@ def correct_chasing_events( logger.info(f'Offsets are greater than onsets by {len_diff}') elif len(onset_ids) == len(offset_ids): logger.info('Chasing events are equal') - + return category, timestamps def event_triggered_chirps( - event: np.ndarray, - chirps:np.ndarray, + event: np.ndarray, + chirps: np.ndarray, time_before_event: int, time_after_event: int, dt: float, width: float, - )-> tuple[np.ndarray, np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: event_chirps = [] # chirps that are in specified window around event - centered_chirps = [] # timestamps of chirps around event centered on the event timepoint + # timestamps of chirps around event centered on the event timepoint + centered_chirps = [] for event_timestamp in event: start = event_timestamp - time_before_event @@ -118,18 +133,19 @@ def event_triggered_chirps( event_chirps.append(chirps_around_event) if len(chirps_around_event) == 0: continue - else: + else: centered_chirps.append(chirps_around_event - event_timestamp) - + time = np.arange(-time_before_event, time_after_event, dt) - + # Kernel density estimation with some if's if len(centered_chirps) == 0: centered_chirps = np.array([]) centered_chirps_convolved = np.zeros(len(time)) else: - centered_chirps = np.concatenate(centered_chirps, axis=0) # convert list of arrays to one array for plotting - centered_chirps_convolved = (acausal_kde1d(centered_chirps, time, width)) / len(event) + # convert list of arrays to one array for plotting + centered_chirps = np.concatenate(centered_chirps, axis=0) + centered_chirps_convolved = (acausal_kde1d( + centered_chirps, time, width)) / len(event) return event_chirps, centered_chirps, centered_chirps_convolved - diff --git a/poster/main.pdf b/poster/main.pdf index 8e5c90ad4548fb1a5cd4ed8d02f9cbf2898c9fd6..a1d28bebf08ab172246e4f2aae5322485e84004d 100644 GIT binary patch delta 25023 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 TZMY-aolyJ8O&N&W+Au~=!TD>_=D z==<1ruhwFqJ?NPgG;ZDDM%qM!Tp#)+B-D#1=#pB#b`&%r;xbv6{!hVbiDIGBa zHV7yM6)FP6xGXvXaRhbI<;RS}@MWd{n}8+M<=y(*hlIu9kdnHlrlx25u&aTotane? z?|8-hN3hdjEJMKeq%?gbj)2Qy{rLusQa*><`gWg2?WbZcr~T_a4x7bv4v+1h>hv0m zsUXAY3_-uuMVe$Pg&b!8_2qN}U*k#FVyQT6PPegwK*W!?hl_e%c5@%nTnWYiZB78Vu=4vmD%ZgsugFBXP?1Bc6Lw|u=bC>0=CBv~jKeJWB_cD-0c zc!6b8ndWSu{3yD$?fGtr@}-?7xScQZoGP>p_)i|IkcA`T@p{enfsPqogMTRwZEahE z!+cHCd;tbyy^Np2LdN-~2GODfTMVQVSltFTVFr^jU)&zmzo4Im4X@z=PLO!eZ=ZNO zc(0}c{vKbfpJg%hUyOZ1;f5zZ&#*qAM2YF|zg8B{_oAJvZ#2+yC=WSE=APkJG&9*@ zSK$5jl`x6%%CASQBm94xh1A{WgTG??LLu8>&jMReLN8sdoXzGL7P=dJX@8e()UK-+l|PzgW#i=$(cGQH*8?k$ z4`qRKoxp}bb`Z*$E#7;Q!zY?0dma@b_rm=U&lBA!p_=v~(Y&O(c6 zc^uF|PYM4q#7NXM^(u`TgKqDsEDoZiKaw|0+f26Gpx4a4ZVh1=gq=vug!iZ?rgLG$ zABZ0~L@@D3SY=p;=^r2r!5W~^^oC(0TEDO->rrBuvE=*y&52}6Fh3awz%HjB0=m6; za8#Iyv2I(p^^>by9pybhk{G@Z+iO>5RA`q-KCkYYWDLy*fuql0uJ^IS3Ho#lKN8$& zfWV5(#;g@Hk)#?CkJDkj$@=baO1H&s1#a~)v^S`?Z;Q)*waJ-mmL6<9min1P5HEk4 zDWOO7RduJCGs%3G)C!3i2+P?0cp8H<3MrNAhcF6kckKhI>xy}&Qpy3T5SS+-sW=Fz zZ;D!>NQxJn+1EvT9?cnp;D#FmG5<;x0GdaE|Z9pZT5_ z@&wF~n#6bf^xCC>{d}|1pGIU6h_eupkbNM(f%aSIFTnnBf~`zR?S1=QOMUiU@|_S< zJw@v`IbB1ZiDK3EYht(iR|`A%HG&>$zyORgc)}MNaR1qUhe;2yCH1=h3_-LUJ<&Mo zs2JF@I9ihyKmr(^>nYR%Zd`;xNyGNr%6Bt_(LlCVg?Akuo#FW z2=2D~J+R&DcJgZkBPRdEF~Z+*#WV)_p5oM3B`IF`TM+f9T@7;(?3V_g-uV`#T8B6x z_||OrcCXouZnV*@UveLEwsvY8VRKv0ikdF~FG%s%P1LYKie>z+e<2C!Zuh+e8-U3P`8RPH;+v!fYw zch9pe_|_VK(ad7*R3K+05R4{w%v{t;c0OA_vGeS|B+w>5ficsor&c|(50)c`%8 z_v!a$bR5_=b1xP}w4-|-r57l~NI_KF849cl6^-5sVHE@l;q;{idZCm|%bOy({EL~99Q_uX)r^&vGWwlSplwJt(4c^p z(LObdC_4l-)DAu~K%z@LM4SiCzO|JEyY?b%ociq4AHDIaGdl0yRPC-=YO8N>&N%#Q zP#3~q5R1TQSnk|;m@lm{59Qg%Ek5z+2FY5Tq`u5reYb<*0{Yj`pYm*VuNW<11>t0t zFcMsFX2h6$s}Yb%FsmT*359Q!RU1J3`tRlz&hLxJg0vc%w(2QNZ%$L;w1TmE7jvq} ztd_^Z1hxWo-<~tseh!*=ev@zSBPiX^FRK*cR z@&3#koS;!W`mm{NrJ>kEl0|vvt}it11F76H2Cd`D3bZ2zFSL!2=!arPD1tRYH{$#~ z0ZK3V%$uOGi7U3@VqR^~lYI+RRfyKFemmi-F4?9pwWwu7bCwhgx0z7Y&I@%{2%8Q) zaIS5`c~tg#W1eh7zgcrKCZRbp;vm6vh3Tm?LrT05U8Nxk{0hhqS4DJ3$s$3&E6)ml z&qpMsOF*oK^2Tpri%$my>kw1Vz~8z4CLPgUx7229h_#6P4$P& zU3(Nuj%)-6dOU%=koo5%y9cFprThfv=$}nUjJpfvp_k592xz~PzLV2Y)`XV`Jx^xV z-N<9{(d%fZz|-w2#6SY5uO%Qvtq zF_PAQJIs5Ko&$iaB+U$*8AP0TcdJM6i{<8fEJDxD?=6Bqy91|ezFHJK^m8W{zZ z%$f$B8rg&-XY$t{EVk&1;q}465jq|zq=AKUTrHAg@vBz<8Z=9iiQR#-~X@xq?^si-lUB5yfVYz z;XTY?lPzyh83f5jaXs#6^?ute@>OD0St^Z}nvy=*Mf`LDW3C^$gSjxh`PKY0HC`s; ztL)&jJ{3CvI^g=M@14)|{fg$s5Rus>MF5$*1ONT)@x=UW)mf;_d+(Em0NKSOlucc* zQ@RYxd^{1T<;$uoidH$w84zDee30k2c_|6jH`$Q&lqNLaCK-As69%YJ@d|!_1p-|A zb)eMApU?ZF1lq9ojXqB`YZ;*)^&xb7&enrV><-$YJT3QL%>yU6l^jW1O^CXrF}g-n zWwWeTat{8Vw+bMx&v^J%2Md`Pf|Uc$8c30=c69?ltOs( z(_S59m3T2;IyzD}j_di9A}!(1$F%zdmE~aHQQmpp8lHJRs6I2uM=Dg1X(reuZR`34 zuWs(wSbQQwIgb!C>l~=0e41L!9o61yi(4MPH`%2TqEeueJM|E`6P;a|j~gDV1Oh<@ zY(Tz-mhWt?9)rLTgi2cPPG2=SLQ*70ir-e>lXm)x zx8C2P4uKu%?n(ytT)x=Zfr!M{roYAfzcTnni0>OyiKe8!2eZR++IAnms_qd#N)r}F zL#OT;f5VY${7#UXpQ5<~x|B%f-FnRUCl`?PEZB{Iw)7?MhR*~+6n<9dvb!rDGOx_uVR7!S1p>ds;yYm(;}7s z*iog`QUXXyC&NpCjOg7x(rcU)bSNBEo7AH}8-LO~M&QZY)_h1_l4j!Jwk-A!_yVZ= zpj)LLBl60_=d!)h-0O7J^NwSon z`AO{?g(`DH>Wrc`fsvza;OEmiUA@<^8=%KjIuVf3TDmo8`AKrqYHc}@@O<6>QNQa+ zt1AyIHHH78E?FGp5i;Yv@*Q(Mj)=Fn4rBujR%h1xhmU5c_m}e)XtXxfJ)Gtxv ztU_bmJ*$B=%InT$1HDvzz40H5-=WI|w=w?yc!a0e8uQIFnhIx)W+VXJ3CJd-n=IGU zXR$~!ETv`}}XNImicn)S+2$kzQlRgieY{HbML=cTLDQtYmUU;TVR ze3)yvBOnRyN~H{e6IL6v@3hE2OZ!hNTkHBQWa&mx{#;MWpJS3ucylE8Y(8_7MZ0(ZO|`dHR?H^eNas@=@9r-+m8(&GKp+-80NaG>z;L%ik8{cS4s!ur^z3< zVt@5jrErKQN?N5~^Xp*&tXbI_KORa~-)#kxG|>n(g0`Agy9uO6uruqlzUU58mPIC& z$-$|Aw7&`itn#dq(>}QxNV_ft)2jAHMQ2APdFn)f3qaoAg@Tj~a8q0Xi z$G9a$MKFH*H@01R+n5|C!AW~a_GgjA(zM^w>D^Z{uU2NV-OHPiYU5c-y3jg$9932@ z55aG;LyLVDEG{77;7eRdlcpkOq{?UoEa5%~(q6;+xQ*LeY37 zb^e{apz@u%H$l}VPN}7mYq+v2QT?g@#>D)kQ?I{8oxiZ81u~K6%>@PP-N+@P54)PS z@-T{P2M>Nx16PHEV(@QPM;0&X#aSKPUBNuyLm9C$#q2rwh4@k-``sg04jY$N{e?!A zA_i{j8_UI6<^_(9M(wawPjopSb6xoMs5yos@fa0+I`VT08^O~Mcs>R$&f3q8&0O_- z)(@~xy0{+oRI9mc%)`xz$@|S{PEL7UtUs!c1L5|t+;r@^eiB#L8FKt0s{=C+rl~zZ zO0rmE&y7tPE*61&`dEp;en~YYm(0tNhTxWGV2oQ->-_YOo!Z<^R7}QDb*nfPs|T)` zO{E20%jhO!f-R!z4sPmJuoM5Pb(%b>bcUDO()G9O@mkdr@)2ZtHR*PX(hmV`Vmc8mH z3>L03KT@r&`mnmU5~Cjqv-8*eO4kBgj!H{$U?nmCm{(UVuSNR`&2fY`F_50wIDdB$ zNIhXr7xjT(?g8_C*m&T%*KF$E)NFr@@O3uW3A^+^YggGA#cNZ~JyEx89)5HIit=b} zeYxKty0SF4b8Hvc>l0#A)sp+ePmeel+HTJz~osZvOk(8 zSZlRk>oC8gtcSo3nnI}w_z>6x4nc;U)tDB38rMlKs!cl=tU8}|(Q=xV&$aZ&TC8ch zbi2jQQbtA;Mjp zhw#II5W?rK2fljzmVcey%{*SNAcleP*=_u?Cc_3D0Yiec1<#~>?s*;nygGSQTQ^h& zpAGZ+^-p&yE;0Nj9uMg04jO501TV3m*#m|*XQF1~tUc|jj#hOkHh|lYUnVnc%ZX$6 zV#x+p@_5ojyl&59iN#X!1bqJQLy>hVTUt9ziaFf==f`to;k~?GMqfQly~K3q>w%oh z*DSH%jzJU+TtToaaDpa4{N&wGq-R9ZWc^}tqonS13DHy>K?4B=AJMij_S++PxL7zc zmZ7XAt91fJO_oWO?3Vj{Q|Lmi@A}KQTizRMy~SV3^+c|I!5^oKCkvL-c>+%ttw%G4 zZB9FUvrqKTe?#GqW`MVg_C`y^d|uczZjbxx!eHn}zn2$FACD_wwZR@Q14If`oHPSj zgMB28D_ZOcA{P|go3hu$e7@47dBk`@M7>xnCaq;5$yVz4zG8_mwqow!?VKahg>esXi>dyE7<@&~n3t@YKJ&VN6 zc&7X~p|GZN;GnlLypK&GZ#$^;p*j8Zm+paYol20JF#>69T`5-*MtFO-RCJF)QjayX zIn?9A_16`}BC-zC(`=@xI_u{akbGHRybt zSjw+^cRL|n-d=OHRP?)haBcyK53I@NGC_wE3n)%FLAZR+8&I$^nAoT5ps3kBIajd^xt=07p zd-J)>1Y!ZVNFPg8y?ULGbOB!7^`MC1k9%dVky$~tKqNtNwxL-;AV_dQ4Yy&I9T563w-xo8%u-6a=U)8m2=wB8-MqOH zGszhw?A^rOzOPh?<&nR~eW^S3ftB|M#|@g!p_oLIy}E?UvR;{Pk(sfKZ4Q*Zq!xFF z06tNC$L|W;U+^QuxFFa;Mj-g>b*>YF$_h{r26Ic?!^PZyn`{fww^uv3bAzu~UBl=GW zi4(B~Xl*lb2EhtP2tgHaLvDkhfE&@$0&;uE>u~zIP0pQN4`9wFx%;@xWUQ$pfFZFo~a-)=ye&J0|>C->&U5 z(Ab3Agqen8F+k}Gipb@u;a$Q9ayfa6dGW5LJ|jV?p=3~gtsKRwf_Z1M&E6el2q}QH z!EFySV}_Nl33zVl+g7TanKogKZc)&E&iJ0}2}fyvOR%6Z4Q9t4-o6m^6lntMT9{hQ z6t|qgegADprpyXP4iyPm8-AXD9sI=h6^-pbozLg`T5^2^a(_aF9oS+F7(0-CT|qU8 z+0he{-+PjM%m`UNv-ri`v*Lso>$%!3MoDZUI1n;S!#1SsD;%F|$8X&|fp4=SgK&Uo z3rjo)zo|Vxy&FS?fslh6C4YVqE`yzkolqXV#~5P8M%AQ9NV0w3V~WrA#vH>b<}QP3 zlYq}@wt)kS^wW8XBXueY+VvA{#XyxxKD$0eQFY2SI?abC5-&7S$-_Zy37;Uhkw zrHlVKrHk!?aiwGNfg%3C@~Ye{|K(LlAy_$xsnebLz$nw7_`s@KQ3b%BAi$U<(?LbS zFabSgRyI*RRyMYOz%KF+*#7~C@IT=A2b_ZcH?T1a`~yxsW;PbSf57z*Sb6^saB%6d zvNQj4*tq`>aB}|EuXyQ~j@? zq4-y@(31UAW;)XU#-JzpryLB#|CEj4zh3_V%YVSB$I8mZ_z(V#$I9{l0Na1S{?B3l zKfuEIAN>0imj5;X5B}HuA217W{?`%fKVtuXfQ9})2l$`*{--S7bpNU6|J0rKKX&;~ znVtUs)6}g0pEUJF4s?F51jfjyBvya9bVoPWP!uWVd3P~PL#;UKu+^ypCu3)%G&~6+ z7(`M8tV}&QN*0_+@v6@U<)-KFOxNig>np!2{h${h@Z;hxCzI!>{VGOhHOTx&nq0LJ z5MwgJPbaX~M?2aIMv0HbO&LftV>5$0(29fg+qWzsnpS!1INkA3i@qTQ%~Ok$`3(OT zQYIB1HFM&AP_42d#)}Td(S&bq15ENgt5f8v=tcpf(*s)3=zdO#{CIV(S!~3sYTmyn zS^6Eu5( ziOMN?l-N^Vc&~ruG~YTU5rjb$18YTCx>CkQ3?N9tYaH zc<@C%OI#Awu>!Rzg%VveNp<17aLdK~_yG3enhZJV|H+ zdq;XLtSm_By7GE#qfnj>G#{2=Gg;=u{0No0PH&g=BSkWHCATy)RYb;;GE+xq4E?O^ z)%;=_znjO`b`@s>BN9ih2bca5{#5p{mT;PbY=`c`#6AuEVUV&voRi^M`0tR|F4V7q zwQc=tLT2a$Py_!W0d+xTeT_~hsR|;V#XGbvVufj%akN877O*uCfbw~Gn}HZ90Mr1` z`H=Rb7eEvIF7VUitY2YLC|^@HQ>q9r5OE07ERZYURB3xC_$5F<7!w6N;#1L|yr;%j^GW&#Ro6mpL>&5)i&|-Wn*m+U zYG|Y}uU0HY+v$on&?Z)-l!Q|RKL+LqVgRD(N7u8oiP^*CL)OLcgQ*jB!{!pb)h2kk zfbAQ0l`KUt0>MZ1-$S{pbQ@E9jf zVzd}Y%pJ6R9oqolX~vE&c^-Pp^H77_QV_QZ8Khs8fSy16wYT6zidKulgo zRo_)k4i@Pa^RIHW-#;q*B11CswBuI7c+%Rleq_BMZn`mJt~UviqGg79fb+rf^W)?a zoF7Mc)|Qn@0yg=MB`y`jnY0KjOQs#$4K##QPS^)dlc;%Qs8XO(;G^7xS3HCC_AWA!?E`F)?5>!% zSZ7%NvOs5CWQJzS@TiFLg3@4&R!Bw)1%F5R@S}KU2@`*z-j$jYdZ*kTu*8z=bX%n? zWbA9f$%XH<4>-EzPrnk`JyH`ULfoBA7>-iK%%uLSA%W-rwJ?mu9T_h(dyYu<6YqE< z#}Qf;4}e@Y>yxjcWMh5-{d~m(wjBA;Rf)S`k}iwE3d{q>zUj%_-7 zZ!T-KJEL!Z9&y+=oTR&hZ!|4*v^uQXZMblCdoF}W>a=&hGpe-|Lu6f^?GAz z`DnuOtFWDbq3vHmGVn=n7+yAR;~@qPXI>|cfrqH~kMswX+61lew)MSQ*G{_4W0SUa z;mvibzpWG5*@sg#cZZ)ZSsb=mbSG+RYRSpTQ}+9jk+6QFKb9)h_xASaG~;4pW8>m# zwHkb1Zp|ms81;XdjqeUeE68Byoe@&7bePvKvZ6)uy#dtcN6JLT=@id_TQD8VyV>tzVcE4{wb=p*p)5hF6(va@0X~(ooV&guA6R#2LhId9+~aGotunMUMzsQG>2?xQgB>zVE-*x0bcM@3Z$l``rCL``PR4bKWhe&N#!1ujxHAJp1_Fl=%-a z@o~T1mhN9>g)yE7%+HJqDuX|*N@^D@sZLhW3r{)rnVgPCx(KV8&bqwh&-4AUoAZh` zVl}`Qr(DNX$5Hvkrg-?Pd&Q3oUJIq0!!ItJ6TnbI{Z_M03 zG&sfA@LJ)TRqf+Fy3pWZiE>9LK9rSJN>mLWhZ}+4E}e8*$=BWdee8}!?akAyrkdU6 zlL^!0KfwmQYFqr!kNBYsImd$LQ&LSSncB7V?pGBuxZbv+n}0{;RB?+uNkJdi8I9aE z$l#M(IrVL#e$+E_;$lZ=lr`t=+e713Pw)Nw65xER)|aCo&Y+8bihiSuqafJe3H>mu zrQkapd(fZn`8kQ3HO8orhYT15O%zsY?X<9O#MtXvp1oA6~mQc z#TImd;Xvt$$l~P2^8r$BT3haIh2sOjh=(~7d(7q?%c|4+8dH{%~7Ki5q9)Aj|#V;u)(vg-~;P>5eNMp>Gl#oH(jSu zU!q>^THh5F`5nXk=DWUqlzpe7H}?+d#HAIaWwi@llOu`UB9ATe*}G&#jv(zpnQ&zB zqc{A;jn}knvag?sDA%c^8G1L&Qe$d=GHb_}Usp5!na=WZth$FU`M1+Ukqr0Z?d$Es zBXOA$OU@Hf)J+y`5fh4%wycz|PZCZx$qmA0pKuqqiq_-H>|rLqveA5}Z%-bY+ovk^ zj6_NbwA`YlDfDh&6rGm|e$_WV$xG&Xm^TC$_+M~>&H9of53kSja*Rq@SCx}&EwcR< z?~z8`ZQD_q;O*@VY2wjDg_&Zr$ihwgjB(*Xc`@g(ZSu^Y3{uO+FKmoZymU~D@bCK> zGIO^lI4zKN$Zk5rk75_{#qc%%vGjeS@L9p~Npj@F%b}&iqLGb+6yKg>&nXHg6EA2S z54^PTZ0t@x`De*5?$^AYWXDjMs`aN!tt=&}(($ICS6++lG_*3QxVK-qpA#@0^h%@z zXIJaoP++*R72T7@qA+}?gZA>!pFnO={s#|cOqFP&Kb(#Cp-X-&FxD%0O}A}DN_6;; zFw@Y&TzQ6>Ml*(^4RHO5ll)1?ROwgIEQ zOe=!z0{UBj%zyoI#K-YzVc5M4(lcq6C2l;vf zBY6<8=_rx~)FEaqU650JW+G!Eqh!&iATNJGMpo8DMqbwRGo%j*v9EuZ-2?F^#rEIq(a4p!^VZg;}O&K|K*Ngk0K;9;pQWx6awCxg;)qr3y`e{ zIC&0YfiqU17WmjaxIN7g0a)+LkU_BfBE(7<8$~K2;F=`>O8ta} z;HVW~otQx0gDqAeHo}W-NEiX9uL1DQH1ad7xemJdz5_)d;J$Tmal=_;0L=LlfT_=r zJ_wj>6V&9ZL+ZiFTfnm0hZ&ytC3ja`9jEW-u7`Gsgiqc<14=Z`f$<^VakGaEb zFUeajw|%@YD)7q}$RFVbJogNE5Cj7rsFVlZ`vZb*GYg>!VBg^;IVn~~fe@(!_2oh; z^2D=zuyE@%G@tsK^r)8bkq28N{@aCAj!ylNBaF>J9teaA%-D-8hb7XHEHK?16b{>G zAX$ll5rVttAPe|hCU60@?!xn#pvsdxWFSnP4dM;R#fN|oi6#iW3pY=4_iJ07U&`v&MW}MqoYV=1ngb}I+FegeSx(f zBUuTYn~)y@jwl8opg}Y&Q3Bd%Z9~`L?ov=3JB1WOzzt;p1cVrb4a>oW03pH=a9cTO z0|*fa(^UWqLuA2z1*mw$0^X;f^C(MmHk=#rUX5 z9@bg_oYqMv1_j*%%7Pama^h4If-^FZ&mnmr44KG>;Do2PZy-3}_XxuRS;$#PUJ0hj zMsC0iy~IXENOG7d2k8gu@y~(KX6GPX5C{cW`wM79qyeyrM??{c5CAv#f)P~9M{*u?U!c5P;xzzrtEy zfOW)<@F8H?mLspiY{Q@;;E4(Xo*xFiA8Ca62$GdRUx{Quz@J7y%_E*1319D1q|jgC z`%9TOqe#}jJYgLN7y8Q+#CHG!o&>=n6X2Hu;qXPk?GxZaKsfZ_{YhXQQO0))^z)Z8 zwbQ`tK7at-vu8kaiAH1ytT+p50^#U@JAw6&D!Z#lUHoM(a(Jy7sRk*jz)~-e3YV3M zS*K?I8(Eb9rddb>nZPRl-OG|Dl12GHN&?8D{7*{$H?_bjD-ZyaENS8Rh5s<)Ob@aL zQkMNoC!zy)Fa38Jd~^A~T}T;lWELt#VE+oARyvZOC=kThgP9-*i+x012L4g}iCRI5 zRgs0)=fH24|AdS|Kq_*uHc^eaz)A$K9|AcgvIJ65AZRZDt%a~EN8}?23=-F+kPR^E zH;B?fye3#eV0Z|LgVpFz%y2qbHXx8{3%XV&B&|Y}2-sf?#R6*(nji9Wua80;

;Iv}Yk)+&%bIVYy1V9x* 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