Module bvmpc.bv_session
This module is a representation of a single Operant box Session.
Written by Sean Martin and Gao Xiang Ham
Expand source code
"""
This module is a representation of a single Operant box Session.
Written by Sean Martin and Gao Xiang Ham
"""
from scipy.spatial.distance import pdist
from scipy.cluster.hierarchy import cophenet
import os
from datetime import datetime
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from bvmpc.bv_session_info import SessionInfo
from bvmpc.bv_axona import AxonaInput, AxonaSet
from bvmpc.bv_array_methods import check_error_during_tone
from bvmpc.bv_array_methods import split_into_blocks
from bvmpc.bv_array_methods import split_array
from bvmpc.bv_array_methods import split_array_with_another
from bvmpc.bv_array_methods import split_array_in_between_two
import bvmpc.bv_plot as bv_plot
import scipy.cluster.hierarchy as shc
class Session:
"""
The base class to hold MEDPC behaviour information.
An Operant Chamber Session holds data and methods related to this.
Provide ONE of h5_file, lines, neo_file, and axona_file to initialise.
Parameters
----------
h5_file: str - Default None
h5 file location to load the session from
lines: List of str - Default None
The lines in the MedPC file for this session.
neo_file: str - Default None
The location of a neo file to load from
axona_file: str - Default None
The location of a .inp file to load from
s_type: str - Default None
The type of session when using axona_file
For now, leaving as 6 will work for everything.
neo_backend: str - Default "nix
If using neo_file, what backend to use
verbose: bool - Default False
Whether to print information while loading.
file_origin: str - Default None
Where the file originated from,
mostly only useful if directly passing lines
"""
def __init__(
self, h5_file=None, lines=None, neo_file=None,
axona_file=None, s_type="6",
neo_backend="nix", verbose=False, file_origin=None):
"""See help(Session) for more info."""
self.session_info = SessionInfo()
self.fname = None
self.metadata = {}
self.info_arrays = {}
self.verbose = verbose
self.file_origin = file_origin
self.out_dir = None
self.trial_df = None
self.trial_df_norm = None
self.cluster_results = None
self.clust_reord_trial_df = None
self.insert = [] # Stores inserted reward ts due to post-hoc correction
self.title = None
a = 0 if lines is None else 1
b = 0 if h5_file is None else 1
c = 0 if neo_file is None else 1
d = 0 if axona_file is None else 1
if (a + b + c + d != 1):
print(
"Error: Session takes one of" +
"h5_file, lines, neo_file, and axona_file")
return
if lines is not None:
self.lines = lines
self._extract_metadata()
self._extract_session_arrays()
elif h5_file is not None:
self.file_origin = h5_file
self.h5_file = h5_file
self._extract_h5_info()
elif neo_file is not None:
self.file_origin = neo_file
self.neo_file = neo_file
self.neo_backend = neo_backend
self._extract_neo_info()
elif axona_file is not None:
if s_type is None:
print("Error: Please provide a session type (6 or 7)")
exit(-1)
self.s_type = s_type
self.file_origin = axona_file
self.axona_file = axona_file
self.fname = axona_file[:-3]
self._extract_axona_info()
else:
print("Error: Unknown situation in Session init")
exit(-1)
def init_trial_dataframe(self):
"""Initialise the trial based Pandas dataframe for this session."""
self._init_trial_df()
def get_title(self):
if self.title is None:
date = self.get_metadata('start_date').replace('/', '_')
sub = self.get_metadata('subject')
stage = self.get_stage()
self.title = '{} {} S{}'.format(sub, date, stage)
return self.title
def get_date(self):
"""Return date in datetime format"""
import datetime
date = self.get_metadata('start_date')
dd = datetime.datetime.strptime(date, '%m/%d/%y').date()
return dd
def get_insert(self):
"""
Return a list of inserted reward timestamps due to post-hoc correction.
"""
return self.insert
def add_insert(self, to_insert):
"""
Add to list of inserted reward timestamps due to post-hoc correction.
"""
insert_list = self.get_insert()
insert_list.append(to_insert)
self.insert = insert_list
def split_pell_ts(self):
"""
Return a tuple of arrays splitting up the rewards.
(Pellets without doubles, time of doubles)
"""
pell_ts = self.get_arrays("Reward")
dpell_bool = np.diff(pell_ts) < 0.8
# Provides index of double pell in pell_ts
dpell_idx = np.nonzero(dpell_bool)[0] + 1
dpell = pell_ts[dpell_idx]
# pell drop ts excluding double ts
pell_ts_exdouble = np.delete(pell_ts, dpell_idx)
return pell_ts_exdouble, dpell
def time_taken(self):
"""Calculate how long the Session took in mins."""
start_time = self.get_metadata("start_time")[-8:].replace(' ', '0')
end_time = self.get_metadata("end_time")[-8:].replace(' ', '0')
fmt = '%H:%M:%S'
tdelta = (
datetime.strptime(end_time, fmt) -
datetime.strptime(start_time, fmt))
tdelta_mins = int(tdelta.total_seconds() / 60)
return tdelta_mins
def split_sess(
self, norm=True, blocks=None, error_only=False, all_levers=False):
"""
Split a session up into multiple blocks.
blocks: defines timepoints to split.
returns 5 outputs:
1) timestamps split into rows depending on blocks input
-> norm_reward_ts, norm_lever_ts, norm_err_ts, norm_double_r_ts
2) print to include in title and file name. Mainly for stage 7.
"""
session_type = self.get_metadata('name')
stage = session_type[:2].replace('_', '')
reward_times = self.get_rw_ts()
timestamps = self.get_arrays()
lever_ts = self.get_lever_ts()
pell_ts = timestamps["Reward"]
pell_double = np.nonzero(np.diff(pell_ts) < 0.8)
# returns reward ts after d_pell
reward_double = reward_times[
np.searchsorted(
reward_times, pell_ts[pell_double], side='right')]
err_lever_ts = []
if blocks is not None:
pass
else:
# blocks = np.arange(5, 1830, 305) # Default split into schedules
blocks = self.get_tone_starts() + 5 # Default split into schedules
incl = "" # Initialize print for type of extracted lever_ts
if stage == '7' and error_only: # plots errors only
incl = '_Errors_Only'
lever_ts = self.get_err_lever_ts()
elif stage == '7' and all_levers: # plots all responses incl. errors
incl = '_All'
err_lever_ts = self.get_err_lever_ts()
lever_ts = np.sort(np.concatenate((
lever_ts, err_lever_ts), axis=None))
elif stage == '7': # plots all responses exclu. errors
incl = '_Correct Only'
split_lever_ts = np.split(lever_ts,
np.searchsorted(lever_ts, blocks))
split_reward_ts = np.split(reward_times,
np.searchsorted(reward_times, blocks))
split_double_r_ts = np.split(reward_double,
np.searchsorted(reward_double, blocks))
split_err_ts = np.split(err_lever_ts,
np.searchsorted(err_lever_ts, blocks))
norm_reward_ts = []
norm_lever_ts = []
norm_err_ts = []
norm_double_r_ts = []
if norm:
for i, l in enumerate(split_lever_ts[1:]):
norm_lever_ts.append(np.append([0], l - blocks[i], axis=0))
norm_reward_ts.append(split_reward_ts[i + 1] - blocks[i])
norm_double_r_ts.append(split_double_r_ts[i + 1] - blocks[i])
if stage == '7' and all_levers: # plots all responses incl. errors
norm_err_ts.append(split_err_ts[i + 1] - blocks[i])
else:
norm_lever_ts = split_lever_ts[1:]
norm_reward_ts = split_reward_ts[1:]
norm_err_ts = split_err_ts[1:]
norm_double_r_ts = split_double_r_ts[1:]
return (
norm_reward_ts, norm_lever_ts, norm_err_ts, norm_double_r_ts, incl)
def old_extract_features(self, should_scale=True):
"""
Extract per trial features
This is the previous version of this function.
Parameters
------
should_scale : bool, True
Optional. Whether to scale the data to unit variance
Returns
-------
feat_df : pd.df
pandas dataframe with
"""
df = self.get_trial_df_norm()
feat_names = ["Trial_Len", "Pell",
"Rw_lat", "Err", "1st_Resp", "Resp_n",
"Avg_Press_Rate", "Avg_Press_Gradient"]
# Number of features before lever histogram
n_feat_bh = len(feat_names)
# Set hist bin size for lever responses here
h_bin_final = 4
for i in np.arange(h_bin_final):
feat_names.append("L_Hist-{}".format(i))
features = np.zeros(
shape=(len(df.index), len(feat_names)))
for row in df.itertuples():
index = row.Index
# Trial duration
features[index, 0] = row.Reward_ts
# Completion Latency
features[index, 1] = row.Pellet_ts
# Reward Latency
features[index, 2] = row.Reward_ts - row.Pellet_ts
# Number of err responses
features[index, 3] = row.Err_ts.size
# Time to first response
features[index, 4] = row.First_response
# Number of lever responses
features[index, 5] = np.count_nonzero(
~np.isnan(row.Levers_ts))
x = row.Levers_ts
press_times = x[~np.isnan(x)]
# Average length between lever presses
if len(press_times) > 1:
features[index, 6] = np.mean(np.diff(press_times))
else:
features[index, 6] = row.Reward_ts
# Average rate of change of the rate of change.
if len(press_times) > 2:
features[index, 7] = np.mean(
np.gradient(np.gradient(press_times, 1), 1))
else:
features[index, 7] = 0
# Histogram values of lever responses
lever_hist = np.histogram(
x[~np.isnan(x)], bins=h_bin_final, density=True)[0]
features[index, n_feat_bh:(n_feat_bh + h_bin_final)] = lever_hist
# double_pellet_time = row.D_Pellet_ts
# if len(double_pellet_time) == 0:
# d_pell_feature = 0
# else:
# d_pell_feature = double_pellet_time
# features[index, 2] = d_pell_feature
# err_hist = np.histogram(
# row.Err_ts, bins=5, density=False)[0]
feat_df = pd.DataFrame(features, columns=feat_names)
# Drop features/columns with all zeros
feat_df = feat_df.loc[:, (feat_df != 0).any(axis=0)]
if should_scale: # Normalize features
scaler = StandardScaler()
data_scaled = scaler.fit_transform(feat_df)
feat_df = pd.DataFrame(data_scaled, columns=feat_df.columns)
return feat_df
def extract_features(self, should_scale=True):
"""
Extract per trial features
Parameters
------
should_scale : bool, True
Optional. Whether to scale the data to unit variance
Returns
-------
feat_df : pd.df
pandas dataframe with
"""
df = self.get_trial_df_norm()
feat_names = ["Trial_Len", "Resp_n",
"Avg_Press_Rate (s)", "Avg_Press_Gradient",
"Max_in_10s"]
# Number of features before lever histogram
n_feat_bh = len(feat_names)
# Set hist bin size for lever responses here
# h_bin_final = 3
# bins = np.arange(0, 60 + 0.00001, 60 / h_bin_final)
# for i in np.arange(h_bin_final):
# feat_names.append("L_Hist-{}".format(i))
features = np.zeros(
shape=(len(df.index), len(feat_names)))
for row in df.itertuples():
index = row.Index
# Trial duration
features[index, 0] = row.Reward_ts
x = row.Levers_ts
press_times = x[~np.isnan(x)]
# Number of lever responses
features[index, 1] = np.count_nonzero(
~np.isnan(row.Levers_ts))
# Average length between lever presses
if len(press_times) > 1:
features[index, 2] = np.mean(np.diff(press_times))
else:
features[index, 2] = row.Reward_ts
# Average rate of change of the rate of change.
if len(press_times) > 2:
features[index, 3] = np.mean(
np.gradient(np.gradient(press_times, 1), 1))
else:
features[index, 3] = 0
# Find the max number of response 10 seconds apart
time_len = 10
dt = time_len / 2
max_val = 0
for value in press_times:
in_range = np.logical_and(
press_times >= value - dt,
press_times <= value + dt)
num_in_range = np.count_nonzero(in_range)
if num_in_range > max_val:
max_val = num_in_range
features[index, 4] = max_val
# Histogram values of lever responses
# lever_hist = np.histogram(
# x[~np.isnan(x)], bins=bins, density=True)[0]
# features[index, n_feat_bh:(n_feat_bh + h_bin_final)] = lever_hist
feat_df = pd.DataFrame(features, columns=feat_names)
# Drop features/columns with all zeros
feat_df = feat_df.loc[:, (feat_df != 0).any(axis=0)]
if should_scale: # Normalize features
scaler = StandardScaler()
data_scaled = scaler.fit_transform(feat_df)
feat_df = pd.DataFrame(data_scaled, columns=feat_df.columns)
return feat_df
def perform_pca(self, n_components=5, should_scale=True):
"""
Perform PCA on per trial features
Parameters
------
n_components : int or float
the number of PCA components to compute
if float, uses enough components to reach that much variance
should_scale - whether to scale the data to unit variance
Returns
-------
tuple : (pd.df, pd.df, PCA)
(features 2d array, PCA of features, PCA object)
"""
data = self.extract_features(should_scale=should_scale)
pca = PCA(n_components=n_components)
after_pca = pca.fit_transform(data)
after_pca = pd.DataFrame(
after_pca, columns=['PC{}'.format(x) for x in np.arange(
after_pca.shape[1]) + 1])
print(
"\nPCA fraction of explained variance",
pca.explained_variance_ratio_)
print(
"Total explained variance: ", sum(pca.explained_variance_ratio_))
return data, after_pca, pca
def perform_HDBSCAN(self, should_scale=True):
""" Testing purposes only. Perform HDBSCAN on per trial features. """
fig, ax = plt.subplots(1, 2, figsize=(15, 8))
import hdbscan
_, data, _ = self.perform_pca(
n_components=None, should_scale=should_scale)
ax[0].set_xlabel('PCA 1')
ax[0].set_ylabel('PCA 2')
df = self.get_trial_df_norm()
markers = df['Schedule']
clusterer = hdbscan.HDBSCAN(
min_cluster_size=2)
clusterer.fit(data)
print(clusterer.labels_.max())
# HDBSCAN(algorithm='best', alpha=1.0, approx_min_span_tree=True,
# gen_min_span_tree=False, leaf_size=40, memory=Memory(cachedir=None),
# metric='euclidean', min_cluster_size=5, min_samples=None, p=None)
sns.scatterplot(data[:, 0], data[:, 1], s=50, linewidth=0,
style=markers, hue=clusterer.labels_, palette='deep', ax=ax[0])
clusterer.condensed_tree_.plot(select_clusters=True,
selection_palette=sns.color_palette('deep')[1:], axis=ax[1]) # Plots condensed dendogram
plt.suptitle('HDBSCAN', fontsize=24)
fig.text(0.5, 0.91, self.get_title(),
transform=fig.transFigure, ha='center')
plt.show()
exit(-1)
def perform_UMAP(self, should_scale=True):
""" Testing purposes only. Perform UMAP on per trial features. """
import umap
data = self.extract_features(should_scale=should_scale)
df = self.get_trial_df_norm()
color = [sns.color_palette()[x]
for x in df.Schedule.astype('category').cat.codes]
for n in (2, 5, 10, 20, 50, 100, 200):
self.draw_umap(data, color, n_neighbors=n,
title='n_neighbors = {}'.format(n))
plt.show()
exit(-1)
# reducer = umap.UMAP()
# embedding = reducer.fit_transform(data)
# plt.scatter(embedding[:, 0], embedding[:, 1], c=[
# sns.color_palette()[x] for x in df.Schedule.astype('category').cat.codes])
# plt.gca().set_aspect('equal', 'datalim')
# plt.title('UMAP projection of the Behav', fontsize=24)
# plt.show()
return data
def draw_umap(
self, data, c, n_neighbors=15, min_dist=0.1, n_components=2,
metric='euclidean', title=''):
""" For testing out UMAP parameters """
import umap
fit = umap.UMAP(
n_neighbors=n_neighbors,
min_dist=min_dist,
n_components=n_components,
metric=metric
)
u = fit.fit_transform(data)
fig = plt.figure()
if n_components == 1:
ax = fig.add_subplot(111)
ax.scatter(u[:, 0], range(len(u)), c=c)
if n_components == 2:
ax = fig.add_subplot(111)
ax.scatter(u[:, 0], u[:, 1], c=c)
if n_components == 3:
ax = fig.add_subplot(111, projection='3d')
ax.scatter(u[:, 0], u[:, 1], u[:, 2], c=c, s=100)
plt.title(title, fontsize=18)
def hier_cluster(
self, should_scale=True, should_pca=True, cutoff=None,
test_methods=False):
"""
Perform hierarchical clustering on per trial features
Parameters
------
should_scale : bool, True
whether to scale the data to unit variance
cutoff: int, None
Optional. Level at which to cutoff dendrogram
Returns
-------
dict: {'Z' : np.array, 'reindex' : list[int], 'clusters' : np.array}
{linkage matrix, index for sorting trials, cluster indices}
"""
if should_pca:
_, data, _ = self.perform_pca(
n_components=5) # Use PCA as features
else:
data = self.extract_features(
should_scale=should_scale) # Use raw features
from bvmpc.bv_utils import test_all_hier_clustering
if test_methods:
test_all_hier_clustering(data, verbose=True)
Z = shc.linkage(data, method='centroid') # linkage matrix
c, coph_dists = cophenet(Z, pdist(data))
print('Cophentic Correlation Coefficient: ', c)
dend = shc.dendrogram(Z, no_plot=True, color_threshold=cutoff)
reindex = list(map(int, dend['ivl'])) # index for sorting trials
if cutoff is None:
cutoff = 0.7 * max(Z[:, 2])
clusters = shc.fcluster(Z, cutoff, criterion='distance')
self.cluster_results = {
'Z': Z,
'reindex': reindex,
'clusters': clusters,
}
return self.cluster_results
def get_cluster_results(self, should_pca=True, cutoff=None):
"""
Returns cluster results from hier_cluster
Returns
-------
dict: {'Z' : np.array, 'reindex' : list[int], 'clusters' : np.array}
{linkage matrix, index for sorting trials, cluster indices}
"""
if self.cluster_results is None:
self.hier_cluster(should_pca=should_pca, cutoff=cutoff)
return self.cluster_results
def get_cluster_ordering(self):
""" Returns cluster ordering from hier_cluster """
if self.cluster_results is None:
self.hier_cluster()
return self.cluster_results['reindex']
def get_cluster_ordered_df(self):
""" Returns trial_df_norm in cluster order """
if self.clust_reord_trial_df is None:
reindex = self.cluster_results['reindex']
self.clust_reord_trial_df = self.get_trial_df_norm().reindex(reindex)
return self.clust_reord_trial_df
def save_to_h5(self, out_dir, name=None):
"""Save information to a h5 file."""
location = name
if name is None:
location = self._get_hdf5_name()
self.h5_file = os.path.join(out_dir, location)
self._save_h5_info()
def save_to_neo(
self, out_dir, name=None,
neo_backend="nix", remove_existing=False):
"""Save information to a neo file."""
location = name
self.neo_backend = neo_backend
if name is None:
ext = self._get_neo_io(get_ext=True)
location = self._get_hdf5_name(ext=ext)
self.neo_file = os.path.join(out_dir, location)
self._save_neo_info(remove_existing)
def get_trial_df(self):
"""Get dataframe split into trials without normalisation (time).
Returns
-------
mod: used to identify trials where reward timestamps modified post-hoc
trial_df = {
'Reward_ts': reward_times,
'Pellet_ts': pell_ts_exdouble,
'D_Pellet_ts': trial_dr_ts,
'Schedule': schedule_type,
'Levers_ts': trial_lever_ts,
'Err_ts': trial_err_ts,
'Tone_s': trial_tone_start,
'Trial_s': trial_norm[:-1],
'Mod': mod
}
"""
if self.trial_df is None:
self.init_trial_dataframe()
return self.trial_df
def get_trial_df_norm(self):
"""Get dataframe split into trials with normalisation (time)."""
if self.trial_df_norm is None:
self.init_trial_dataframe()
return self.trial_df_norm
def get_metadata(self, key=None):
"""
Get the metadata for the Session.
Parameters
----------
key : str - Default None
Possible Keys: "start_date", "end_date", "subject",
"experiment", "group", "box", "start_time", "end_time", "name"
Returns
-------
str : If key is a valid key.
Dict : If key is None, all the metadata.
"""
if key:
return self.metadata.get(key, None)
return self.metadata
def get_subject_type(self):
"""Return the subject and session type as a string."""
subject = self.get_metadata("subject")
name = self.get_metadata("name")
return 'Subject: {}, Trial Type {}'.format(subject, name)
def get_ratio(self):
"""Return the fixed ratio as an int."""
ratio = self.get_metadata("fixed_ratio")
if ratio is not None:
return int(ratio)
return None
def get_interval(self):
"""Return the fixed ratio as an int."""
interval = self.get_metadata("fixed_interval (secs)")
if interval is not None:
return int(interval)
return None
def get_stage(self):
"""Obtain the stage number (without _)."""
session_type = self.get_metadata('name')
stage = session_type[:2].replace('_', '')
return stage
def get_arrays(self, key=None):
"""
Return the info arrays in the session.
Parameters
----------
key : str - Default None
The name of the info array to get.
Returns
-------
np.ndarray: If key is a valid key.
[] : If key is not a valid key
Dict : If key is None, all the info arrays.
"""
if key:
return self.info_arrays.get(key, [])
return self.info_arrays
def get_lever_ts(self, include_un=True):
"""
Get the timestamps of the lever presses.
Parameters
----------
include_un : bool - Default True
Include lever presses that were unnecessary for the reward.
Returns
-------
np.ndarray : A numpy array of sorted timestamps.
"""
levers = [
self.get_arrays("R"),
self.get_arrays("L")]
if include_un:
levers.append(self.get_arrays("Un_R"))
levers.append(self.get_arrays("Un_L"))
return np.sort(np.concatenate(levers, axis=None))
def get_one_lever_ts(self, lever_type, include_un=True):
"""
Get the timestamps of an individual lever press side.
Lever type is expected to be "L" or "R".
"""
if (lever_type != "L") and (lever_type != "R"):
raise ValueError(
"lever_type is expected to be L or R, entered {}".format(
lever_type))
levers = [self.get_arrays(lever_type)]
if include_un:
levers.append(self.get_arrays("Un_" + lever_type))
return np.sort(np.concatenate(levers, axis=None))
def get_err_lever_ts(self, include_un=True):
"""
Get the timestamps of the error/opposite lever presses.
Parameters
----------
include_un : bool - Default True
Include lever presses that were unnecessary for the reward.
Returns
-------
np.ndarray : A numpy array of sorted timestamps.
"""
levers = [
self.get_arrays("FR_Err"),
self.get_arrays("FI_Err")]
if include_un:
levers.append(self.get_arrays("Un_FR_Err"))
levers.append(self.get_arrays("Un_FI_Err"))
return np.sort(np.concatenate(levers, axis=None))
def get_rw_ts(self):
"""
Get the timestamps of rewards.
Corrected for session switching/ending without reward collection.
Returns
-------
np.ndarray : A numpy array of sorted timestamps.
"""
session_type = self.get_metadata('name')
stage = session_type[:2].replace('_', '')
pell_ts = self.get_arrays("Reward")
reward_times = self.get_arrays("Nosepoke")
pell_ts_exdouble = self.split_pell_ts()[0]
reward_times = self.get_arrays("Nosepoke")
trial_len = self.get_metadata("trial_length (mins)") * 60
if stage == '7' or stage == '6':
# Check if trial switched before reward collection
# -> Adds collection as switch time
block_ends = self.get_block_ends()
split_pell_ts = split_into_blocks(
pell_ts_exdouble, blocks=block_ends)
split_reward_ts = split_into_blocks(
reward_times, blocks=block_ends)
for i, (pell, reward) in enumerate(
zip(split_pell_ts, split_reward_ts)):
if len(pell) > len(reward):
block_len = block_ends[i]
reward_times = np.insert(
reward_times, np.searchsorted(
reward_times, block_len), block_len)
# if last reward time < last pellet dispensed,
# assume animal picked reward at end of session.
# Checks if last block contains responses first
if reward_times[-1] < pell_ts[-1]:
if stage == '7' or stage == '6':
reward_times = np.append(reward_times, block_ends[-1])
else:
reward_times = np.append(reward_times, trial_len)
return np.sort(reward_times, axis=None)
def get_block_ends(self):
"""Return block end times - Tone end times + end of session"""
sound = self.info_arrays.get("sound", [])
if len(sound) != 0:
# Axona
block_ends = np.concatenate([sound[1:], [sound[-1] + 305]])
else:
# MED-PC
trial_len = self.get_metadata("trial_length (mins)") * 60
trial_len += 5
repeated_trial_len = (trial_len) * 6
block_ends = np.arange(trial_len, repeated_trial_len, trial_len)
return block_ends
def get_tone_starts(self):
"""Return tone start times"""
sound = self.info_arrays.get("sound", [])
if len(sound) != 0:
# Axona
block_starts = np.array(sound - 5)
else:
# MED-PC
trial_len = self.get_metadata("trial_length (mins)") * 60
repeated_trial_len = (trial_len) * 6
block_starts = np.arange(0, repeated_trial_len, trial_len)
return block_starts
def get_valid_tdf(self, excl_dr=False, norm=False):
"""
Returns trial_df of excluding:
First trial in FI blocks
Trials with post-hoc modification of reward timestamp
Trials more than 60s
Parameters
----------
excl_dr : bool, False
Optional. Excludes double reward trials
norm : bool, False
Optional. Return trial_df_norm with masks applied
"""
if norm:
df = self.get_trial_df_norm()
else:
df = self.get_trial_df()
# Masks used for filtering trials
mod_mask = df.Mod.notnull() # trials with post-hoc rw correction
FI_s_mask = (df.Tone_s.str.len() != 0) & (
df.Schedule == 'FI') # 1st trial of each FI blocks
tlen_mask = (df.Reward_ts - df.Trial_s) > 60 # trials longer then 60s
df = df[(~mod_mask) & (~FI_s_mask) & (~tlen_mask)]
if excl_dr:
dr_mask = df.D_Pellet_ts.str.len() != 0
df = df[(~dr_mask)]
return df
def _save_neo_info(self, remove_existing):
"""Private function to save info to neo file."""
if os.path.isfile(self.neo_file):
if remove_existing:
os.remove(self.neo_file)
else:
print("Skipping {} as it already exists".format(
self.neo_file) +
" - set remove_existing to True to remove")
return
from neo.core import Block, Segment, Event
from quantities import s
anots = self.get_metadata()
anots["nix_name"] = "Block_Main"
anots["protocol"] = anots.pop("name")
blk = Block(name="Block_Main", **anots)
seg = Segment(name="Segment_Main", nix_name="Segment_Main")
blk.segments.append(seg)
# Could consider splitting by trials using index in seg
for key, val in self.get_arrays().items():
e = Event(
times=val * s, labels=None,
name=key, nix_name="Event_" + key)
seg.events.append(e)
nio = self._get_neo_io()
nio.write_block(blk)
nio.close()
def _extract_neo_info(self):
"""Private function to extract info from neo file."""
nio = self._get_neo_io()
block = nio.read()[0]
nio.close()
annotations = block.annotations
for key, val in annotations.items():
if key == "protocol":
key = "name"
self.metadata[key] = val
for event in block.segments[0].events:
key = event.name
self.info_arrays[key] = (
event.times.rescale('s').magnitude)
def _get_neo_io(self, get_ext=False):
backend = self.neo_backend
if backend == "nix":
if get_ext:
return "nix"
from neo.io import NixIO
nio = NixIO(filename=self.neo_file)
elif backend == "nsdf":
if get_ext:
return "h5"
from neo.io import NSDFIO
nio = NSDFIO(filename=self.neo_file)
elif backend == "matlab":
if get_ext:
return "mat"
from neo.io import NeoMatlabIO
nio = NeoMatlabIO(filename=self.neo_file)
else:
print(
"Backend {} not recognised, defaulting to nix".format(
backend))
from neo.io import NixIO
if get_ext:
return "nix"
nio = NixIO(filename=self.neo_file)
return nio
def _save_h5_info(self):
"""Private function to save info to h5 file."""
import h5py
with h5py.File(self.h5_file, "w", libver="latest") as f:
for key, val in self.get_metadata().items():
f.attrs[key] = val
for key, val in self.get_arrays().items():
f.create_dataset(key, data=val, dtype=np.float32)
def _extract_h5_info(self):
"""Private function to pull info from h5 file."""
import h5py
with h5py.File(self.h5_file, "r", libver="latest") as f:
for key, val in f.attrs.items():
self.metadata[key] = val
for key in f.keys():
self.info_arrays[key] = f[key][()]
def _extract_axona_info(self):
"""Extract from .inp .set and .log files for session conversion."""
# Extract metadata from set file
self.axona_setfile = self.axona_file[:-3] + "set"
self.axona_set = AxonaSet(self.axona_setfile)
for k, v in self.session_info.get_axona_metadata_map().items():
self.metadata[k] = self.axona_set.get_val(v)
# Numerical metadata extract from log file
# **No log file in some cases. Static method for now**
self.metadata["fixed_ratio"] = 6
self.metadata["double_reward_window (secs)"] = 10
self.metadata["fixed_interval (secs)"] = 30
self.metadata["num_trials"] = 6
self.metadata["trial_length (mins)"] = 5
self.metadata['subject'] = os.path.basename(
self.axona_file).split("_")[0]
# Extract the timestamp arrays from the axona data .inp file
self.axona_info = AxonaInput(self.axona_file)
for k, v in self.session_info.get_input_channel(self.s_type).items():
self.info_arrays[k] = self.axona_info.get_times(
"I", v[0], v[1])
for k, v in self.session_info.get_output_channel(self.s_type).items():
self.info_arrays[k] = self.axona_info.get_times(
"O", v[0], v[1])
self._convert_axona_info()
def _convert_axona_info(self):
"""Perform postprocessing on the extracted axona timestamp arrays."""
left_presses = self.info_arrays.get("left_lever", [])
right_presses = self.info_arrays.get("right_lever", [])
nosepokes = self.info_arrays.get("all_nosepokes", [])
block_ends = self.get_block_ends()
block_s = self.get_tone_starts() + 5 # End of tone
# Check for errors in presses during Tone presentation
left_presses = check_error_during_tone(
left_presses, block_s, l_type=" left ")
right_presses = check_error_during_tone(
right_presses, block_s, l_type=" right ")
# Extract nosepokes as necessary and unecessary
pell_ts_exdouble, _ = self.split_pell_ts()
good_nosepokes, un_nosepokes = split_array_with_another(
nosepokes, pell_ts_exdouble)
split_nosepokes = split_into_blocks(
good_nosepokes, blocks=block_ends)
split_pellets = split_into_blocks(
pell_ts_exdouble, blocks=block_ends)
split_all_nosepokes = split_into_blocks(
nosepokes, blocks=block_ends)
for i in range(6):
b1, b2 = split_nosepokes[i], split_pellets[i]
if len(b2) > len(b1):
print("block: {}, End-time: {}".format(i, block_ends[i]))
# print("good nosepokes: {}".format(good_nosepokes))
# print("nosepokes: {}".format(b1))
# print("pellets: {}".format(b2))
last_nosepoke_idx = -1
for j in range(i, -1, -1):
bl = split_all_nosepokes[j]
if len(bl) == 0:
continue
# Finds last nosepoke in this block, or previous block
last_nosepoke_arr = np.nonzero(
np.abs(nosepokes - bl[-1]) <= 0.00001)
last_nosepoke_idx = last_nosepoke_arr[0][0]
break
# Replaces overflowed nosepoke w block end in main array
to_insert = block_ends[i] - 0.001
print(' Insert: ', to_insert)
self.add_insert(to_insert)
# if i == 5:
# to_insert = pell_ts_exdouble[-1] + 0.01
nosepokes = np.insert(
nosepokes, last_nosepoke_idx + 1, to_insert)
good_nosepokes, un_nosepokes = split_array_with_another(
nosepokes, pell_ts_exdouble)
# if i < 5:
# ignores first nosepoke in next block in split arrays
split_nosepokes = split_into_blocks(
good_nosepokes, blocks=block_ends)
split_all_nosepokes = split_into_blocks(
nosepokes, blocks=block_ends)
# print("Corrected:", good_nosepokes)
split_nosepokes = split_into_blocks(
good_nosepokes, blocks=block_ends)
split_pellets = split_into_blocks(
pell_ts_exdouble, blocks=block_ends)
for i, (b1, b2) in enumerate(zip(split_nosepokes, split_pellets)):
if b2.shape != b1.shape:
print("Error, nosepokes in blocks don't match pellets")
print("{} nosepokes, {} pellets, in block {}".format(
len(b1), len(b2), i + 1))
exit(-1)
self.info_arrays["Nosepoke"] = good_nosepokes
self.info_arrays["Un_Nosepoke"] = un_nosepokes
fi_starts = self.info_arrays["left_light"]
fr_starts = self.info_arrays["right_light"]
trial_types = np.zeros(6)
# set trial types - 1 is FR, 0 is FI
for i in range(3):
j = int(fi_starts[i] // 305)
trial_types[j] = 0
j = int(fr_starts[i] // 305)
trial_types[j] = 1
self.info_arrays["Trial Type"] = trial_types
# TODO parse other file to remove fixed value
self.metadata["fixed_interval (secs)"] = 30
fi = self.get_metadata("fixed_interval (secs)")
# Set left presses and unnecessary left presses
split_left_presses = split_into_blocks(
left_presses, blocks=block_ends)
left_presses_fi = np.concatenate(
split_left_presses[np.nonzero(trial_types == 0)])
left_presses_fr = np.concatenate(
split_left_presses[np.nonzero(trial_types == 1)])
good_nosepokes_fi = np.concatenate(
split_into_blocks(good_nosepokes, blocks=block_ends)[
np.nonzero(trial_types == 0)])
fi_allow_times = np.add(good_nosepokes_fi, fi)
good_left_presses, un_left_presses = split_array_with_another(
left_presses_fi, fi_allow_times)
self.info_arrays["L"] = good_left_presses
self.info_arrays["Un_L"] = un_left_presses
un_fr_err, fr_err = split_array_in_between_two(
left_presses_fr, pell_ts_exdouble, good_nosepokes)
self.info_arrays["Un_FR_Err"] = un_fr_err
self.info_arrays["FR_Err"] = fr_err
# set right presses and unnecessary right presses
split_right_presses = split_into_blocks(
right_presses, blocks=block_ends)
right_presses_fr = np.concatenate(
split_right_presses[np.nonzero(trial_types == 1)])
right_presses_fi = np.concatenate(
split_right_presses[np.nonzero(trial_types == 0)])
un_right_presses, good_right_presses = split_array_in_between_two(
right_presses_fr, pell_ts_exdouble, good_nosepokes)
self.info_arrays["R"] = good_right_presses
self.info_arrays["Un_R"] = un_right_presses
un_fi_err, fi_err = split_array_in_between_two(
right_presses_fi, pell_ts_exdouble, good_nosepokes)
self.info_arrays["Un_FI_Err"] = un_fi_err
self.info_arrays["FI_Err"] = fi_err
# extract trial start times
reward_times = self.get_rw_ts()
t_start_blocks = np.split(
reward_times, np.searchsorted(reward_times, block_ends))
t_start = []
for i, (x, tone_end) in enumerate(zip(t_start_blocks, block_ends)):
# replace trial start w next block start if empty
if len(x) == 0:
x = np.array([tone_end])
# replace last start in block w next block start
elif x[-1] < tone_end:
x[-1] = tone_end
t_start.append(np.array(x))
t_start = np.concatenate(t_start).ravel()
t_start = np.insert(t_start, 0, block_s[0])
# Trials start after tone ends
self.info_arrays["Trial_Start"] = t_start[:-1]
def _extract_metadata(self):
"""Private function to pull metadata out of lines."""
for i, name in enumerate(self.session_info.get_metadata()):
start = self.session_info.get_metadata_start(i)
self.metadata[name] = self.lines[i][start:]
def _extract_session_arrays(self):
"""Private function to pull session arrays out of lines."""
data_info = self.session_info.get_session_type_info(
self.get_metadata("name"))
if data_info is None:
print("Not parsing {}".format(self))
return
print("Parsing {}".format(self))
if self.verbose:
print("Parameters extracted:")
for i, (start_char, end_char, parameter) in enumerate(data_info):
c_data = self._extract_array(self.lines, start_char, end_char)
if parameter == "Experiment Variables":
mapping = self.session_info.get_session_variable_list(
self.get_metadata("name"))
for m, v in zip(mapping, c_data):
if m.endswith("(ticks)"):
m = m[:-7] + "(secs)"
v = v / 100
self.metadata[m] = v
self.info_arrays[parameter] = c_data
if self.verbose:
print(i, '-> {}: {}'.format(parameter, len(c_data)))
return self.info_arrays
def _init_trial_df(self):
""" Generates pandas dataframe based on trials per row. """
session_type = self.get_metadata('name')
stage = session_type[:2].replace('_', '') # Obtain stage number w/o _
pell_ts_exdouble, dpell = self.split_pell_ts()
reward_times = self.get_rw_ts()
trial_starts = self.get_arrays("Trial_Start")
# Assign schedule type to trials
schedule_type, sch_block = [], []
mod = []
# Reward times artificially inserted
mod_rw = self.get_insert()
if stage == '7' or stage == '6':
# End of tone
block_s = self.get_tone_starts() + 5
# from matplotlib import pyplot as plt
# plt.eventplot(blocks, colors='r', label='s')
# plt.eventplot(mod_rw, colors='g', label='e')
# plt.legend()
# plt.show()
tstarts_in_blocks = np.split(
trial_starts, np.searchsorted(trial_starts, block_s[1:]))
sch_type = self.get_arrays('Trial Type')
for i, block in enumerate(tstarts_in_blocks):
if sch_type[i] == 1:
b_type = 'FR'
elif sch_type[i] == 0:
b_type = 'FI'
for rw in block:
schedule_type.append(b_type)
sch_block.append(b_type + '-{}'.format(i))
else:
if stage == '4':
b_type = 'CR'
elif stage == '5a':
b_type = 'FR'
elif stage == '5b':
b_type = 'FI'
else:
b_type = 'NA'
for _ in trial_starts:
schedule_type.append(b_type)
sch_block.append(None)
# Rearrange timestamps based on trial per row
lever_ts = self.get_lever_ts(True)
err_ts = self.get_err_lever_ts(True)
trial_rw_ts = np.split(
reward_times,
(np.searchsorted(reward_times, trial_starts, side='right')[1:]))
trial_pell_ts = np.split(
pell_ts_exdouble,
(np.searchsorted(pell_ts_exdouble, trial_starts)[1:]))
trial_lever_ts = np.split(
lever_ts, (np.searchsorted(lever_ts, trial_starts)[1:]))
trial_err_ts = np.split(
err_ts, (np.searchsorted(err_ts, trial_starts)[1:]))
trial_dr_ts = np.split(
dpell, (np.searchsorted(dpell, trial_starts)[1:]))
# Tone end ts
trial_tone_end = np.split(
block_s, (np.searchsorted(block_s, trial_starts)[1:]))
# # Testing - print values
# x = trial_rw_ts
# for i, (a, b) in enumerate(zip(x, trial_starts)):
# print("Trial ", i)
# print("T_start: ", b)
# print(a)
# exit(-1)
# Assign mod value to trial type:
for rw in trial_rw_ts:
if rw:
if np.isclose(rw, mod_rw):
mod.append(1)
else:
mod.append(None)
else:
mod.append(None)
# Reconstruct Tone start times in correct trial location
from copy import deepcopy
trial_tone_start = deepcopy(trial_tone_end)
for i, t in enumerate(trial_tone_end):
if not len(t) == 0:
trial_tone_start[i] = trial_tone_end[i] - 5
# Initialize array for lever timestamps
# Max lever press per trial
trials_max_l = len(max(trial_lever_ts, key=len))
lever_arr = np.empty((len(trial_starts), trials_max_l,))
lever_arr.fill(np.nan)
first_response_arr = []
trials_max_err = len(max(trial_err_ts, key=len)
) # Max err press per trial
err_arr = np.empty((len(trial_starts), trials_max_err,))
err_arr.fill(np.nan)
# 2D array of lever timestamps
for i, (l, err) in enumerate(zip(trial_lever_ts, trial_err_ts)):
l_end = len(l)
lever_arr[i, :l_end] = l[:]
err_end = len(err)
err_arr[i, :err_end] = err[:]
first_response_arr.append(np.array([l[0]]))
# Splits lever ts in each trial into np.arrs for handling in pandas
# lever_arr = np.vsplit(lever_arr, i+1)
# err_arr = np.vsplit(err_arr, i+1)
lever_arr = list(lever_arr)
err_arr = list(err_arr)
# Arrays used for normalization of timestamps to trials
trial_norm = trial_starts
norm_rw = deepcopy(trial_rw_ts)
norm_pell = deepcopy(trial_pell_ts)
norm_lever = deepcopy(lever_arr)
norm_dr = deepcopy(trial_dr_ts)
norm_err = deepcopy(err_arr)
norm_tone = deepcopy(trial_tone_start)
norm_trial_s = deepcopy(trial_starts)
norm_first_response = deepcopy(first_response_arr)
# Normalize timestamps based on start of trial
for i, _ in enumerate(norm_rw):
norm_lever[i] -= trial_norm[i]
norm_err[i] -= trial_norm[i]
norm_dr[i] -= trial_norm[i]
norm_pell[i] -= trial_norm[i]
norm_rw[i] -= trial_norm[i]
norm_tone[i] -= trial_norm[i]
norm_trial_s[i] -= trial_norm[i]
norm_first_response[i] -= trial_norm[i]
# Convert trial_starts into list of np.arr of list
trial_starts = [np.array([x]) for x in trial_starts]
# Timestamps kept as original starting from session start
session_dict = {
'Reward_ts': trial_rw_ts,
'Pellet_ts': trial_pell_ts,
'D_Pellet_ts': trial_dr_ts,
'Schedule': schedule_type,
'Levers_ts': trial_lever_ts,
'Err_ts': trial_err_ts,
'Tone_s': trial_tone_start,
'Trial_s': trial_starts,
'First_response': first_response_arr,
'Sch_block': sch_block,
'Mod': mod
}
# for key, x in session_dict.items():
# print(key, len(x))
# Timestamps normalised to each trial start
trial_dict = {
'Reward_ts': norm_rw,
'Pellet_ts': norm_pell,
'D_Pellet_ts': norm_dr,
'Schedule': schedule_type,
'Levers_ts': norm_lever,
'Err_ts': norm_err,
'Tone_s': norm_tone,
'Trial_s': norm_trial_s,
'First_response': norm_first_response,
'Sch_block': sch_block,
'Mod': mod
}
for key, val in trial_dict.items():
print(
"Initialised trial df with {} trials and keys {}".format(
len(val), list(trial_dict.keys())))
break
self.trial_df = pd.DataFrame(session_dict)
self.trial_df_norm = pd.DataFrame(trial_dict)
# from bvmpc.bv_utils import check_fn
# self.trial_df = pd.DataFrame(session_dict).applymap(check_fn)
# self.trial_df_norm = pd.DataFrame(trial_dict).applymap(check_fn)
@staticmethod
def _extract_array(lines, start_char, end_char):
"""Private function to pull a single session from lines."""
def parse_line(line, dtype=np.float32):
return np.array(line.lstrip().split()[1:]).astype(dtype)
start_index = np.flatnonzero(lines == start_char)
stop_index = np.flatnonzero(lines == end_char)
if end_char == 'END':
# Last timepoint does not have a end_char
stop_index = [lines.size]
if start_index[0] + 1 == stop_index[0]:
return np.array([])
data_lines = lines[start_index[0] + 1:stop_index[0]]
last_line = parse_line(data_lines[-1])
arr = np.empty(
5 * (len(data_lines) - 1) + len(last_line),
dtype=np.float32)
for i, line in enumerate(data_lines):
numbers = parse_line(line)
st = 5 * i
arr[st:st + len(numbers)] = numbers
return arr
def _get_hdf5_name(self, ext="h5"):
return "{}_{}_{}_{}.{}".format(
self.get_metadata("subject"),
self.get_metadata("start_date").replace("/", "-"),
self.get_metadata("start_time")[:-3].replace(":", "-"),
self.get_metadata("name"), ext)
def __repr__(self):
"""
Return string representation of the Session.
Currently includes the date, subject and trial type.
"""
return (
self.get_metadata("start_date") + " " + self.get_subject_type())
Classes
class Session (h5_file=None, lines=None, neo_file=None, axona_file=None, s_type='6', neo_backend='nix', verbose=False, file_origin=None)-
The base class to hold MEDPC behaviour information.
An Operant Chamber Session holds data and methods related to this. Provide ONE of h5_file, lines, neo_file, and axona_file to initialise.
Parameters
h5_file:str - Default None- h5 file location to load the session from
lines:Listofstr - Default None- The lines in the MedPC file for this session.
neo_file:str - Default None- The location of a neo file to load from
axona_file:str - Default None- The location of a .inp file to load from
s_type:str - Default None- The type of session when using axona_file For now, leaving as 6 will work for everything.
neo_backend:str - Default "nix- If using neo_file, what backend to use
verbose:bool - Default False- Whether to print information while loading.
file_origin:str - Default None- Where the file originated from, mostly only useful if directly passing lines
See help(Session) for more info.
Expand source code
class Session: """ The base class to hold MEDPC behaviour information. An Operant Chamber Session holds data and methods related to this. Provide ONE of h5_file, lines, neo_file, and axona_file to initialise. Parameters ---------- h5_file: str - Default None h5 file location to load the session from lines: List of str - Default None The lines in the MedPC file for this session. neo_file: str - Default None The location of a neo file to load from axona_file: str - Default None The location of a .inp file to load from s_type: str - Default None The type of session when using axona_file For now, leaving as 6 will work for everything. neo_backend: str - Default "nix If using neo_file, what backend to use verbose: bool - Default False Whether to print information while loading. file_origin: str - Default None Where the file originated from, mostly only useful if directly passing lines """ def __init__( self, h5_file=None, lines=None, neo_file=None, axona_file=None, s_type="6", neo_backend="nix", verbose=False, file_origin=None): """See help(Session) for more info.""" self.session_info = SessionInfo() self.fname = None self.metadata = {} self.info_arrays = {} self.verbose = verbose self.file_origin = file_origin self.out_dir = None self.trial_df = None self.trial_df_norm = None self.cluster_results = None self.clust_reord_trial_df = None self.insert = [] # Stores inserted reward ts due to post-hoc correction self.title = None a = 0 if lines is None else 1 b = 0 if h5_file is None else 1 c = 0 if neo_file is None else 1 d = 0 if axona_file is None else 1 if (a + b + c + d != 1): print( "Error: Session takes one of" + "h5_file, lines, neo_file, and axona_file") return if lines is not None: self.lines = lines self._extract_metadata() self._extract_session_arrays() elif h5_file is not None: self.file_origin = h5_file self.h5_file = h5_file self._extract_h5_info() elif neo_file is not None: self.file_origin = neo_file self.neo_file = neo_file self.neo_backend = neo_backend self._extract_neo_info() elif axona_file is not None: if s_type is None: print("Error: Please provide a session type (6 or 7)") exit(-1) self.s_type = s_type self.file_origin = axona_file self.axona_file = axona_file self.fname = axona_file[:-3] self._extract_axona_info() else: print("Error: Unknown situation in Session init") exit(-1) def init_trial_dataframe(self): """Initialise the trial based Pandas dataframe for this session.""" self._init_trial_df() def get_title(self): if self.title is None: date = self.get_metadata('start_date').replace('/', '_') sub = self.get_metadata('subject') stage = self.get_stage() self.title = '{} {} S{}'.format(sub, date, stage) return self.title def get_date(self): """Return date in datetime format""" import datetime date = self.get_metadata('start_date') dd = datetime.datetime.strptime(date, '%m/%d/%y').date() return dd def get_insert(self): """ Return a list of inserted reward timestamps due to post-hoc correction. """ return self.insert def add_insert(self, to_insert): """ Add to list of inserted reward timestamps due to post-hoc correction. """ insert_list = self.get_insert() insert_list.append(to_insert) self.insert = insert_list def split_pell_ts(self): """ Return a tuple of arrays splitting up the rewards. (Pellets without doubles, time of doubles) """ pell_ts = self.get_arrays("Reward") dpell_bool = np.diff(pell_ts) < 0.8 # Provides index of double pell in pell_ts dpell_idx = np.nonzero(dpell_bool)[0] + 1 dpell = pell_ts[dpell_idx] # pell drop ts excluding double ts pell_ts_exdouble = np.delete(pell_ts, dpell_idx) return pell_ts_exdouble, dpell def time_taken(self): """Calculate how long the Session took in mins.""" start_time = self.get_metadata("start_time")[-8:].replace(' ', '0') end_time = self.get_metadata("end_time")[-8:].replace(' ', '0') fmt = '%H:%M:%S' tdelta = ( datetime.strptime(end_time, fmt) - datetime.strptime(start_time, fmt)) tdelta_mins = int(tdelta.total_seconds() / 60) return tdelta_mins def split_sess( self, norm=True, blocks=None, error_only=False, all_levers=False): """ Split a session up into multiple blocks. blocks: defines timepoints to split. returns 5 outputs: 1) timestamps split into rows depending on blocks input -> norm_reward_ts, norm_lever_ts, norm_err_ts, norm_double_r_ts 2) print to include in title and file name. Mainly for stage 7. """ session_type = self.get_metadata('name') stage = session_type[:2].replace('_', '') reward_times = self.get_rw_ts() timestamps = self.get_arrays() lever_ts = self.get_lever_ts() pell_ts = timestamps["Reward"] pell_double = np.nonzero(np.diff(pell_ts) < 0.8) # returns reward ts after d_pell reward_double = reward_times[ np.searchsorted( reward_times, pell_ts[pell_double], side='right')] err_lever_ts = [] if blocks is not None: pass else: # blocks = np.arange(5, 1830, 305) # Default split into schedules blocks = self.get_tone_starts() + 5 # Default split into schedules incl = "" # Initialize print for type of extracted lever_ts if stage == '7' and error_only: # plots errors only incl = '_Errors_Only' lever_ts = self.get_err_lever_ts() elif stage == '7' and all_levers: # plots all responses incl. errors incl = '_All' err_lever_ts = self.get_err_lever_ts() lever_ts = np.sort(np.concatenate(( lever_ts, err_lever_ts), axis=None)) elif stage == '7': # plots all responses exclu. errors incl = '_Correct Only' split_lever_ts = np.split(lever_ts, np.searchsorted(lever_ts, blocks)) split_reward_ts = np.split(reward_times, np.searchsorted(reward_times, blocks)) split_double_r_ts = np.split(reward_double, np.searchsorted(reward_double, blocks)) split_err_ts = np.split(err_lever_ts, np.searchsorted(err_lever_ts, blocks)) norm_reward_ts = [] norm_lever_ts = [] norm_err_ts = [] norm_double_r_ts = [] if norm: for i, l in enumerate(split_lever_ts[1:]): norm_lever_ts.append(np.append([0], l - blocks[i], axis=0)) norm_reward_ts.append(split_reward_ts[i + 1] - blocks[i]) norm_double_r_ts.append(split_double_r_ts[i + 1] - blocks[i]) if stage == '7' and all_levers: # plots all responses incl. errors norm_err_ts.append(split_err_ts[i + 1] - blocks[i]) else: norm_lever_ts = split_lever_ts[1:] norm_reward_ts = split_reward_ts[1:] norm_err_ts = split_err_ts[1:] norm_double_r_ts = split_double_r_ts[1:] return ( norm_reward_ts, norm_lever_ts, norm_err_ts, norm_double_r_ts, incl) def old_extract_features(self, should_scale=True): """ Extract per trial features This is the previous version of this function. Parameters ------ should_scale : bool, True Optional. Whether to scale the data to unit variance Returns ------- feat_df : pd.df pandas dataframe with """ df = self.get_trial_df_norm() feat_names = ["Trial_Len", "Pell", "Rw_lat", "Err", "1st_Resp", "Resp_n", "Avg_Press_Rate", "Avg_Press_Gradient"] # Number of features before lever histogram n_feat_bh = len(feat_names) # Set hist bin size for lever responses here h_bin_final = 4 for i in np.arange(h_bin_final): feat_names.append("L_Hist-{}".format(i)) features = np.zeros( shape=(len(df.index), len(feat_names))) for row in df.itertuples(): index = row.Index # Trial duration features[index, 0] = row.Reward_ts # Completion Latency features[index, 1] = row.Pellet_ts # Reward Latency features[index, 2] = row.Reward_ts - row.Pellet_ts # Number of err responses features[index, 3] = row.Err_ts.size # Time to first response features[index, 4] = row.First_response # Number of lever responses features[index, 5] = np.count_nonzero( ~np.isnan(row.Levers_ts)) x = row.Levers_ts press_times = x[~np.isnan(x)] # Average length between lever presses if len(press_times) > 1: features[index, 6] = np.mean(np.diff(press_times)) else: features[index, 6] = row.Reward_ts # Average rate of change of the rate of change. if len(press_times) > 2: features[index, 7] = np.mean( np.gradient(np.gradient(press_times, 1), 1)) else: features[index, 7] = 0 # Histogram values of lever responses lever_hist = np.histogram( x[~np.isnan(x)], bins=h_bin_final, density=True)[0] features[index, n_feat_bh:(n_feat_bh + h_bin_final)] = lever_hist # double_pellet_time = row.D_Pellet_ts # if len(double_pellet_time) == 0: # d_pell_feature = 0 # else: # d_pell_feature = double_pellet_time # features[index, 2] = d_pell_feature # err_hist = np.histogram( # row.Err_ts, bins=5, density=False)[0] feat_df = pd.DataFrame(features, columns=feat_names) # Drop features/columns with all zeros feat_df = feat_df.loc[:, (feat_df != 0).any(axis=0)] if should_scale: # Normalize features scaler = StandardScaler() data_scaled = scaler.fit_transform(feat_df) feat_df = pd.DataFrame(data_scaled, columns=feat_df.columns) return feat_df def extract_features(self, should_scale=True): """ Extract per trial features Parameters ------ should_scale : bool, True Optional. Whether to scale the data to unit variance Returns ------- feat_df : pd.df pandas dataframe with """ df = self.get_trial_df_norm() feat_names = ["Trial_Len", "Resp_n", "Avg_Press_Rate (s)", "Avg_Press_Gradient", "Max_in_10s"] # Number of features before lever histogram n_feat_bh = len(feat_names) # Set hist bin size for lever responses here # h_bin_final = 3 # bins = np.arange(0, 60 + 0.00001, 60 / h_bin_final) # for i in np.arange(h_bin_final): # feat_names.append("L_Hist-{}".format(i)) features = np.zeros( shape=(len(df.index), len(feat_names))) for row in df.itertuples(): index = row.Index # Trial duration features[index, 0] = row.Reward_ts x = row.Levers_ts press_times = x[~np.isnan(x)] # Number of lever responses features[index, 1] = np.count_nonzero( ~np.isnan(row.Levers_ts)) # Average length between lever presses if len(press_times) > 1: features[index, 2] = np.mean(np.diff(press_times)) else: features[index, 2] = row.Reward_ts # Average rate of change of the rate of change. if len(press_times) > 2: features[index, 3] = np.mean( np.gradient(np.gradient(press_times, 1), 1)) else: features[index, 3] = 0 # Find the max number of response 10 seconds apart time_len = 10 dt = time_len / 2 max_val = 0 for value in press_times: in_range = np.logical_and( press_times >= value - dt, press_times <= value + dt) num_in_range = np.count_nonzero(in_range) if num_in_range > max_val: max_val = num_in_range features[index, 4] = max_val # Histogram values of lever responses # lever_hist = np.histogram( # x[~np.isnan(x)], bins=bins, density=True)[0] # features[index, n_feat_bh:(n_feat_bh + h_bin_final)] = lever_hist feat_df = pd.DataFrame(features, columns=feat_names) # Drop features/columns with all zeros feat_df = feat_df.loc[:, (feat_df != 0).any(axis=0)] if should_scale: # Normalize features scaler = StandardScaler() data_scaled = scaler.fit_transform(feat_df) feat_df = pd.DataFrame(data_scaled, columns=feat_df.columns) return feat_df def perform_pca(self, n_components=5, should_scale=True): """ Perform PCA on per trial features Parameters ------ n_components : int or float the number of PCA components to compute if float, uses enough components to reach that much variance should_scale - whether to scale the data to unit variance Returns ------- tuple : (pd.df, pd.df, PCA) (features 2d array, PCA of features, PCA object) """ data = self.extract_features(should_scale=should_scale) pca = PCA(n_components=n_components) after_pca = pca.fit_transform(data) after_pca = pd.DataFrame( after_pca, columns=['PC{}'.format(x) for x in np.arange( after_pca.shape[1]) + 1]) print( "\nPCA fraction of explained variance", pca.explained_variance_ratio_) print( "Total explained variance: ", sum(pca.explained_variance_ratio_)) return data, after_pca, pca def perform_HDBSCAN(self, should_scale=True): """ Testing purposes only. Perform HDBSCAN on per trial features. """ fig, ax = plt.subplots(1, 2, figsize=(15, 8)) import hdbscan _, data, _ = self.perform_pca( n_components=None, should_scale=should_scale) ax[0].set_xlabel('PCA 1') ax[0].set_ylabel('PCA 2') df = self.get_trial_df_norm() markers = df['Schedule'] clusterer = hdbscan.HDBSCAN( min_cluster_size=2) clusterer.fit(data) print(clusterer.labels_.max()) # HDBSCAN(algorithm='best', alpha=1.0, approx_min_span_tree=True, # gen_min_span_tree=False, leaf_size=40, memory=Memory(cachedir=None), # metric='euclidean', min_cluster_size=5, min_samples=None, p=None) sns.scatterplot(data[:, 0], data[:, 1], s=50, linewidth=0, style=markers, hue=clusterer.labels_, palette='deep', ax=ax[0]) clusterer.condensed_tree_.plot(select_clusters=True, selection_palette=sns.color_palette('deep')[1:], axis=ax[1]) # Plots condensed dendogram plt.suptitle('HDBSCAN', fontsize=24) fig.text(0.5, 0.91, self.get_title(), transform=fig.transFigure, ha='center') plt.show() exit(-1) def perform_UMAP(self, should_scale=True): """ Testing purposes only. Perform UMAP on per trial features. """ import umap data = self.extract_features(should_scale=should_scale) df = self.get_trial_df_norm() color = [sns.color_palette()[x] for x in df.Schedule.astype('category').cat.codes] for n in (2, 5, 10, 20, 50, 100, 200): self.draw_umap(data, color, n_neighbors=n, title='n_neighbors = {}'.format(n)) plt.show() exit(-1) # reducer = umap.UMAP() # embedding = reducer.fit_transform(data) # plt.scatter(embedding[:, 0], embedding[:, 1], c=[ # sns.color_palette()[x] for x in df.Schedule.astype('category').cat.codes]) # plt.gca().set_aspect('equal', 'datalim') # plt.title('UMAP projection of the Behav', fontsize=24) # plt.show() return data def draw_umap( self, data, c, n_neighbors=15, min_dist=0.1, n_components=2, metric='euclidean', title=''): """ For testing out UMAP parameters """ import umap fit = umap.UMAP( n_neighbors=n_neighbors, min_dist=min_dist, n_components=n_components, metric=metric ) u = fit.fit_transform(data) fig = plt.figure() if n_components == 1: ax = fig.add_subplot(111) ax.scatter(u[:, 0], range(len(u)), c=c) if n_components == 2: ax = fig.add_subplot(111) ax.scatter(u[:, 0], u[:, 1], c=c) if n_components == 3: ax = fig.add_subplot(111, projection='3d') ax.scatter(u[:, 0], u[:, 1], u[:, 2], c=c, s=100) plt.title(title, fontsize=18) def hier_cluster( self, should_scale=True, should_pca=True, cutoff=None, test_methods=False): """ Perform hierarchical clustering on per trial features Parameters ------ should_scale : bool, True whether to scale the data to unit variance cutoff: int, None Optional. Level at which to cutoff dendrogram Returns ------- dict: {'Z' : np.array, 'reindex' : list[int], 'clusters' : np.array} {linkage matrix, index for sorting trials, cluster indices} """ if should_pca: _, data, _ = self.perform_pca( n_components=5) # Use PCA as features else: data = self.extract_features( should_scale=should_scale) # Use raw features from bvmpc.bv_utils import test_all_hier_clustering if test_methods: test_all_hier_clustering(data, verbose=True) Z = shc.linkage(data, method='centroid') # linkage matrix c, coph_dists = cophenet(Z, pdist(data)) print('Cophentic Correlation Coefficient: ', c) dend = shc.dendrogram(Z, no_plot=True, color_threshold=cutoff) reindex = list(map(int, dend['ivl'])) # index for sorting trials if cutoff is None: cutoff = 0.7 * max(Z[:, 2]) clusters = shc.fcluster(Z, cutoff, criterion='distance') self.cluster_results = { 'Z': Z, 'reindex': reindex, 'clusters': clusters, } return self.cluster_results def get_cluster_results(self, should_pca=True, cutoff=None): """ Returns cluster results from hier_cluster Returns ------- dict: {'Z' : np.array, 'reindex' : list[int], 'clusters' : np.array} {linkage matrix, index for sorting trials, cluster indices} """ if self.cluster_results is None: self.hier_cluster(should_pca=should_pca, cutoff=cutoff) return self.cluster_results def get_cluster_ordering(self): """ Returns cluster ordering from hier_cluster """ if self.cluster_results is None: self.hier_cluster() return self.cluster_results['reindex'] def get_cluster_ordered_df(self): """ Returns trial_df_norm in cluster order """ if self.clust_reord_trial_df is None: reindex = self.cluster_results['reindex'] self.clust_reord_trial_df = self.get_trial_df_norm().reindex(reindex) return self.clust_reord_trial_df def save_to_h5(self, out_dir, name=None): """Save information to a h5 file.""" location = name if name is None: location = self._get_hdf5_name() self.h5_file = os.path.join(out_dir, location) self._save_h5_info() def save_to_neo( self, out_dir, name=None, neo_backend="nix", remove_existing=False): """Save information to a neo file.""" location = name self.neo_backend = neo_backend if name is None: ext = self._get_neo_io(get_ext=True) location = self._get_hdf5_name(ext=ext) self.neo_file = os.path.join(out_dir, location) self._save_neo_info(remove_existing) def get_trial_df(self): """Get dataframe split into trials without normalisation (time). Returns ------- mod: used to identify trials where reward timestamps modified post-hoc trial_df = { 'Reward_ts': reward_times, 'Pellet_ts': pell_ts_exdouble, 'D_Pellet_ts': trial_dr_ts, 'Schedule': schedule_type, 'Levers_ts': trial_lever_ts, 'Err_ts': trial_err_ts, 'Tone_s': trial_tone_start, 'Trial_s': trial_norm[:-1], 'Mod': mod } """ if self.trial_df is None: self.init_trial_dataframe() return self.trial_df def get_trial_df_norm(self): """Get dataframe split into trials with normalisation (time).""" if self.trial_df_norm is None: self.init_trial_dataframe() return self.trial_df_norm def get_metadata(self, key=None): """ Get the metadata for the Session. Parameters ---------- key : str - Default None Possible Keys: "start_date", "end_date", "subject", "experiment", "group", "box", "start_time", "end_time", "name" Returns ------- str : If key is a valid key. Dict : If key is None, all the metadata. """ if key: return self.metadata.get(key, None) return self.metadata def get_subject_type(self): """Return the subject and session type as a string.""" subject = self.get_metadata("subject") name = self.get_metadata("name") return 'Subject: {}, Trial Type {}'.format(subject, name) def get_ratio(self): """Return the fixed ratio as an int.""" ratio = self.get_metadata("fixed_ratio") if ratio is not None: return int(ratio) return None def get_interval(self): """Return the fixed ratio as an int.""" interval = self.get_metadata("fixed_interval (secs)") if interval is not None: return int(interval) return None def get_stage(self): """Obtain the stage number (without _).""" session_type = self.get_metadata('name') stage = session_type[:2].replace('_', '') return stage def get_arrays(self, key=None): """ Return the info arrays in the session. Parameters ---------- key : str - Default None The name of the info array to get. Returns ------- np.ndarray: If key is a valid key. [] : If key is not a valid key Dict : If key is None, all the info arrays. """ if key: return self.info_arrays.get(key, []) return self.info_arrays def get_lever_ts(self, include_un=True): """ Get the timestamps of the lever presses. Parameters ---------- include_un : bool - Default True Include lever presses that were unnecessary for the reward. Returns ------- np.ndarray : A numpy array of sorted timestamps. """ levers = [ self.get_arrays("R"), self.get_arrays("L")] if include_un: levers.append(self.get_arrays("Un_R")) levers.append(self.get_arrays("Un_L")) return np.sort(np.concatenate(levers, axis=None)) def get_one_lever_ts(self, lever_type, include_un=True): """ Get the timestamps of an individual lever press side. Lever type is expected to be "L" or "R". """ if (lever_type != "L") and (lever_type != "R"): raise ValueError( "lever_type is expected to be L or R, entered {}".format( lever_type)) levers = [self.get_arrays(lever_type)] if include_un: levers.append(self.get_arrays("Un_" + lever_type)) return np.sort(np.concatenate(levers, axis=None)) def get_err_lever_ts(self, include_un=True): """ Get the timestamps of the error/opposite lever presses. Parameters ---------- include_un : bool - Default True Include lever presses that were unnecessary for the reward. Returns ------- np.ndarray : A numpy array of sorted timestamps. """ levers = [ self.get_arrays("FR_Err"), self.get_arrays("FI_Err")] if include_un: levers.append(self.get_arrays("Un_FR_Err")) levers.append(self.get_arrays("Un_FI_Err")) return np.sort(np.concatenate(levers, axis=None)) def get_rw_ts(self): """ Get the timestamps of rewards. Corrected for session switching/ending without reward collection. Returns ------- np.ndarray : A numpy array of sorted timestamps. """ session_type = self.get_metadata('name') stage = session_type[:2].replace('_', '') pell_ts = self.get_arrays("Reward") reward_times = self.get_arrays("Nosepoke") pell_ts_exdouble = self.split_pell_ts()[0] reward_times = self.get_arrays("Nosepoke") trial_len = self.get_metadata("trial_length (mins)") * 60 if stage == '7' or stage == '6': # Check if trial switched before reward collection # -> Adds collection as switch time block_ends = self.get_block_ends() split_pell_ts = split_into_blocks( pell_ts_exdouble, blocks=block_ends) split_reward_ts = split_into_blocks( reward_times, blocks=block_ends) for i, (pell, reward) in enumerate( zip(split_pell_ts, split_reward_ts)): if len(pell) > len(reward): block_len = block_ends[i] reward_times = np.insert( reward_times, np.searchsorted( reward_times, block_len), block_len) # if last reward time < last pellet dispensed, # assume animal picked reward at end of session. # Checks if last block contains responses first if reward_times[-1] < pell_ts[-1]: if stage == '7' or stage == '6': reward_times = np.append(reward_times, block_ends[-1]) else: reward_times = np.append(reward_times, trial_len) return np.sort(reward_times, axis=None) def get_block_ends(self): """Return block end times - Tone end times + end of session""" sound = self.info_arrays.get("sound", []) if len(sound) != 0: # Axona block_ends = np.concatenate([sound[1:], [sound[-1] + 305]]) else: # MED-PC trial_len = self.get_metadata("trial_length (mins)") * 60 trial_len += 5 repeated_trial_len = (trial_len) * 6 block_ends = np.arange(trial_len, repeated_trial_len, trial_len) return block_ends def get_tone_starts(self): """Return tone start times""" sound = self.info_arrays.get("sound", []) if len(sound) != 0: # Axona block_starts = np.array(sound - 5) else: # MED-PC trial_len = self.get_metadata("trial_length (mins)") * 60 repeated_trial_len = (trial_len) * 6 block_starts = np.arange(0, repeated_trial_len, trial_len) return block_starts def get_valid_tdf(self, excl_dr=False, norm=False): """ Returns trial_df of excluding: First trial in FI blocks Trials with post-hoc modification of reward timestamp Trials more than 60s Parameters ---------- excl_dr : bool, False Optional. Excludes double reward trials norm : bool, False Optional. Return trial_df_norm with masks applied """ if norm: df = self.get_trial_df_norm() else: df = self.get_trial_df() # Masks used for filtering trials mod_mask = df.Mod.notnull() # trials with post-hoc rw correction FI_s_mask = (df.Tone_s.str.len() != 0) & ( df.Schedule == 'FI') # 1st trial of each FI blocks tlen_mask = (df.Reward_ts - df.Trial_s) > 60 # trials longer then 60s df = df[(~mod_mask) & (~FI_s_mask) & (~tlen_mask)] if excl_dr: dr_mask = df.D_Pellet_ts.str.len() != 0 df = df[(~dr_mask)] return df def _save_neo_info(self, remove_existing): """Private function to save info to neo file.""" if os.path.isfile(self.neo_file): if remove_existing: os.remove(self.neo_file) else: print("Skipping {} as it already exists".format( self.neo_file) + " - set remove_existing to True to remove") return from neo.core import Block, Segment, Event from quantities import s anots = self.get_metadata() anots["nix_name"] = "Block_Main" anots["protocol"] = anots.pop("name") blk = Block(name="Block_Main", **anots) seg = Segment(name="Segment_Main", nix_name="Segment_Main") blk.segments.append(seg) # Could consider splitting by trials using index in seg for key, val in self.get_arrays().items(): e = Event( times=val * s, labels=None, name=key, nix_name="Event_" + key) seg.events.append(e) nio = self._get_neo_io() nio.write_block(blk) nio.close() def _extract_neo_info(self): """Private function to extract info from neo file.""" nio = self._get_neo_io() block = nio.read()[0] nio.close() annotations = block.annotations for key, val in annotations.items(): if key == "protocol": key = "name" self.metadata[key] = val for event in block.segments[0].events: key = event.name self.info_arrays[key] = ( event.times.rescale('s').magnitude) def _get_neo_io(self, get_ext=False): backend = self.neo_backend if backend == "nix": if get_ext: return "nix" from neo.io import NixIO nio = NixIO(filename=self.neo_file) elif backend == "nsdf": if get_ext: return "h5" from neo.io import NSDFIO nio = NSDFIO(filename=self.neo_file) elif backend == "matlab": if get_ext: return "mat" from neo.io import NeoMatlabIO nio = NeoMatlabIO(filename=self.neo_file) else: print( "Backend {} not recognised, defaulting to nix".format( backend)) from neo.io import NixIO if get_ext: return "nix" nio = NixIO(filename=self.neo_file) return nio def _save_h5_info(self): """Private function to save info to h5 file.""" import h5py with h5py.File(self.h5_file, "w", libver="latest") as f: for key, val in self.get_metadata().items(): f.attrs[key] = val for key, val in self.get_arrays().items(): f.create_dataset(key, data=val, dtype=np.float32) def _extract_h5_info(self): """Private function to pull info from h5 file.""" import h5py with h5py.File(self.h5_file, "r", libver="latest") as f: for key, val in f.attrs.items(): self.metadata[key] = val for key in f.keys(): self.info_arrays[key] = f[key][()] def _extract_axona_info(self): """Extract from .inp .set and .log files for session conversion.""" # Extract metadata from set file self.axona_setfile = self.axona_file[:-3] + "set" self.axona_set = AxonaSet(self.axona_setfile) for k, v in self.session_info.get_axona_metadata_map().items(): self.metadata[k] = self.axona_set.get_val(v) # Numerical metadata extract from log file # **No log file in some cases. Static method for now** self.metadata["fixed_ratio"] = 6 self.metadata["double_reward_window (secs)"] = 10 self.metadata["fixed_interval (secs)"] = 30 self.metadata["num_trials"] = 6 self.metadata["trial_length (mins)"] = 5 self.metadata['subject'] = os.path.basename( self.axona_file).split("_")[0] # Extract the timestamp arrays from the axona data .inp file self.axona_info = AxonaInput(self.axona_file) for k, v in self.session_info.get_input_channel(self.s_type).items(): self.info_arrays[k] = self.axona_info.get_times( "I", v[0], v[1]) for k, v in self.session_info.get_output_channel(self.s_type).items(): self.info_arrays[k] = self.axona_info.get_times( "O", v[0], v[1]) self._convert_axona_info() def _convert_axona_info(self): """Perform postprocessing on the extracted axona timestamp arrays.""" left_presses = self.info_arrays.get("left_lever", []) right_presses = self.info_arrays.get("right_lever", []) nosepokes = self.info_arrays.get("all_nosepokes", []) block_ends = self.get_block_ends() block_s = self.get_tone_starts() + 5 # End of tone # Check for errors in presses during Tone presentation left_presses = check_error_during_tone( left_presses, block_s, l_type=" left ") right_presses = check_error_during_tone( right_presses, block_s, l_type=" right ") # Extract nosepokes as necessary and unecessary pell_ts_exdouble, _ = self.split_pell_ts() good_nosepokes, un_nosepokes = split_array_with_another( nosepokes, pell_ts_exdouble) split_nosepokes = split_into_blocks( good_nosepokes, blocks=block_ends) split_pellets = split_into_blocks( pell_ts_exdouble, blocks=block_ends) split_all_nosepokes = split_into_blocks( nosepokes, blocks=block_ends) for i in range(6): b1, b2 = split_nosepokes[i], split_pellets[i] if len(b2) > len(b1): print("block: {}, End-time: {}".format(i, block_ends[i])) # print("good nosepokes: {}".format(good_nosepokes)) # print("nosepokes: {}".format(b1)) # print("pellets: {}".format(b2)) last_nosepoke_idx = -1 for j in range(i, -1, -1): bl = split_all_nosepokes[j] if len(bl) == 0: continue # Finds last nosepoke in this block, or previous block last_nosepoke_arr = np.nonzero( np.abs(nosepokes - bl[-1]) <= 0.00001) last_nosepoke_idx = last_nosepoke_arr[0][0] break # Replaces overflowed nosepoke w block end in main array to_insert = block_ends[i] - 0.001 print(' Insert: ', to_insert) self.add_insert(to_insert) # if i == 5: # to_insert = pell_ts_exdouble[-1] + 0.01 nosepokes = np.insert( nosepokes, last_nosepoke_idx + 1, to_insert) good_nosepokes, un_nosepokes = split_array_with_another( nosepokes, pell_ts_exdouble) # if i < 5: # ignores first nosepoke in next block in split arrays split_nosepokes = split_into_blocks( good_nosepokes, blocks=block_ends) split_all_nosepokes = split_into_blocks( nosepokes, blocks=block_ends) # print("Corrected:", good_nosepokes) split_nosepokes = split_into_blocks( good_nosepokes, blocks=block_ends) split_pellets = split_into_blocks( pell_ts_exdouble, blocks=block_ends) for i, (b1, b2) in enumerate(zip(split_nosepokes, split_pellets)): if b2.shape != b1.shape: print("Error, nosepokes in blocks don't match pellets") print("{} nosepokes, {} pellets, in block {}".format( len(b1), len(b2), i + 1)) exit(-1) self.info_arrays["Nosepoke"] = good_nosepokes self.info_arrays["Un_Nosepoke"] = un_nosepokes fi_starts = self.info_arrays["left_light"] fr_starts = self.info_arrays["right_light"] trial_types = np.zeros(6) # set trial types - 1 is FR, 0 is FI for i in range(3): j = int(fi_starts[i] // 305) trial_types[j] = 0 j = int(fr_starts[i] // 305) trial_types[j] = 1 self.info_arrays["Trial Type"] = trial_types # TODO parse other file to remove fixed value self.metadata["fixed_interval (secs)"] = 30 fi = self.get_metadata("fixed_interval (secs)") # Set left presses and unnecessary left presses split_left_presses = split_into_blocks( left_presses, blocks=block_ends) left_presses_fi = np.concatenate( split_left_presses[np.nonzero(trial_types == 0)]) left_presses_fr = np.concatenate( split_left_presses[np.nonzero(trial_types == 1)]) good_nosepokes_fi = np.concatenate( split_into_blocks(good_nosepokes, blocks=block_ends)[ np.nonzero(trial_types == 0)]) fi_allow_times = np.add(good_nosepokes_fi, fi) good_left_presses, un_left_presses = split_array_with_another( left_presses_fi, fi_allow_times) self.info_arrays["L"] = good_left_presses self.info_arrays["Un_L"] = un_left_presses un_fr_err, fr_err = split_array_in_between_two( left_presses_fr, pell_ts_exdouble, good_nosepokes) self.info_arrays["Un_FR_Err"] = un_fr_err self.info_arrays["FR_Err"] = fr_err # set right presses and unnecessary right presses split_right_presses = split_into_blocks( right_presses, blocks=block_ends) right_presses_fr = np.concatenate( split_right_presses[np.nonzero(trial_types == 1)]) right_presses_fi = np.concatenate( split_right_presses[np.nonzero(trial_types == 0)]) un_right_presses, good_right_presses = split_array_in_between_two( right_presses_fr, pell_ts_exdouble, good_nosepokes) self.info_arrays["R"] = good_right_presses self.info_arrays["Un_R"] = un_right_presses un_fi_err, fi_err = split_array_in_between_two( right_presses_fi, pell_ts_exdouble, good_nosepokes) self.info_arrays["Un_FI_Err"] = un_fi_err self.info_arrays["FI_Err"] = fi_err # extract trial start times reward_times = self.get_rw_ts() t_start_blocks = np.split( reward_times, np.searchsorted(reward_times, block_ends)) t_start = [] for i, (x, tone_end) in enumerate(zip(t_start_blocks, block_ends)): # replace trial start w next block start if empty if len(x) == 0: x = np.array([tone_end]) # replace last start in block w next block start elif x[-1] < tone_end: x[-1] = tone_end t_start.append(np.array(x)) t_start = np.concatenate(t_start).ravel() t_start = np.insert(t_start, 0, block_s[0]) # Trials start after tone ends self.info_arrays["Trial_Start"] = t_start[:-1] def _extract_metadata(self): """Private function to pull metadata out of lines.""" for i, name in enumerate(self.session_info.get_metadata()): start = self.session_info.get_metadata_start(i) self.metadata[name] = self.lines[i][start:] def _extract_session_arrays(self): """Private function to pull session arrays out of lines.""" data_info = self.session_info.get_session_type_info( self.get_metadata("name")) if data_info is None: print("Not parsing {}".format(self)) return print("Parsing {}".format(self)) if self.verbose: print("Parameters extracted:") for i, (start_char, end_char, parameter) in enumerate(data_info): c_data = self._extract_array(self.lines, start_char, end_char) if parameter == "Experiment Variables": mapping = self.session_info.get_session_variable_list( self.get_metadata("name")) for m, v in zip(mapping, c_data): if m.endswith("(ticks)"): m = m[:-7] + "(secs)" v = v / 100 self.metadata[m] = v self.info_arrays[parameter] = c_data if self.verbose: print(i, '-> {}: {}'.format(parameter, len(c_data))) return self.info_arrays def _init_trial_df(self): """ Generates pandas dataframe based on trials per row. """ session_type = self.get_metadata('name') stage = session_type[:2].replace('_', '') # Obtain stage number w/o _ pell_ts_exdouble, dpell = self.split_pell_ts() reward_times = self.get_rw_ts() trial_starts = self.get_arrays("Trial_Start") # Assign schedule type to trials schedule_type, sch_block = [], [] mod = [] # Reward times artificially inserted mod_rw = self.get_insert() if stage == '7' or stage == '6': # End of tone block_s = self.get_tone_starts() + 5 # from matplotlib import pyplot as plt # plt.eventplot(blocks, colors='r', label='s') # plt.eventplot(mod_rw, colors='g', label='e') # plt.legend() # plt.show() tstarts_in_blocks = np.split( trial_starts, np.searchsorted(trial_starts, block_s[1:])) sch_type = self.get_arrays('Trial Type') for i, block in enumerate(tstarts_in_blocks): if sch_type[i] == 1: b_type = 'FR' elif sch_type[i] == 0: b_type = 'FI' for rw in block: schedule_type.append(b_type) sch_block.append(b_type + '-{}'.format(i)) else: if stage == '4': b_type = 'CR' elif stage == '5a': b_type = 'FR' elif stage == '5b': b_type = 'FI' else: b_type = 'NA' for _ in trial_starts: schedule_type.append(b_type) sch_block.append(None) # Rearrange timestamps based on trial per row lever_ts = self.get_lever_ts(True) err_ts = self.get_err_lever_ts(True) trial_rw_ts = np.split( reward_times, (np.searchsorted(reward_times, trial_starts, side='right')[1:])) trial_pell_ts = np.split( pell_ts_exdouble, (np.searchsorted(pell_ts_exdouble, trial_starts)[1:])) trial_lever_ts = np.split( lever_ts, (np.searchsorted(lever_ts, trial_starts)[1:])) trial_err_ts = np.split( err_ts, (np.searchsorted(err_ts, trial_starts)[1:])) trial_dr_ts = np.split( dpell, (np.searchsorted(dpell, trial_starts)[1:])) # Tone end ts trial_tone_end = np.split( block_s, (np.searchsorted(block_s, trial_starts)[1:])) # # Testing - print values # x = trial_rw_ts # for i, (a, b) in enumerate(zip(x, trial_starts)): # print("Trial ", i) # print("T_start: ", b) # print(a) # exit(-1) # Assign mod value to trial type: for rw in trial_rw_ts: if rw: if np.isclose(rw, mod_rw): mod.append(1) else: mod.append(None) else: mod.append(None) # Reconstruct Tone start times in correct trial location from copy import deepcopy trial_tone_start = deepcopy(trial_tone_end) for i, t in enumerate(trial_tone_end): if not len(t) == 0: trial_tone_start[i] = trial_tone_end[i] - 5 # Initialize array for lever timestamps # Max lever press per trial trials_max_l = len(max(trial_lever_ts, key=len)) lever_arr = np.empty((len(trial_starts), trials_max_l,)) lever_arr.fill(np.nan) first_response_arr = [] trials_max_err = len(max(trial_err_ts, key=len) ) # Max err press per trial err_arr = np.empty((len(trial_starts), trials_max_err,)) err_arr.fill(np.nan) # 2D array of lever timestamps for i, (l, err) in enumerate(zip(trial_lever_ts, trial_err_ts)): l_end = len(l) lever_arr[i, :l_end] = l[:] err_end = len(err) err_arr[i, :err_end] = err[:] first_response_arr.append(np.array([l[0]])) # Splits lever ts in each trial into np.arrs for handling in pandas # lever_arr = np.vsplit(lever_arr, i+1) # err_arr = np.vsplit(err_arr, i+1) lever_arr = list(lever_arr) err_arr = list(err_arr) # Arrays used for normalization of timestamps to trials trial_norm = trial_starts norm_rw = deepcopy(trial_rw_ts) norm_pell = deepcopy(trial_pell_ts) norm_lever = deepcopy(lever_arr) norm_dr = deepcopy(trial_dr_ts) norm_err = deepcopy(err_arr) norm_tone = deepcopy(trial_tone_start) norm_trial_s = deepcopy(trial_starts) norm_first_response = deepcopy(first_response_arr) # Normalize timestamps based on start of trial for i, _ in enumerate(norm_rw): norm_lever[i] -= trial_norm[i] norm_err[i] -= trial_norm[i] norm_dr[i] -= trial_norm[i] norm_pell[i] -= trial_norm[i] norm_rw[i] -= trial_norm[i] norm_tone[i] -= trial_norm[i] norm_trial_s[i] -= trial_norm[i] norm_first_response[i] -= trial_norm[i] # Convert trial_starts into list of np.arr of list trial_starts = [np.array([x]) for x in trial_starts] # Timestamps kept as original starting from session start session_dict = { 'Reward_ts': trial_rw_ts, 'Pellet_ts': trial_pell_ts, 'D_Pellet_ts': trial_dr_ts, 'Schedule': schedule_type, 'Levers_ts': trial_lever_ts, 'Err_ts': trial_err_ts, 'Tone_s': trial_tone_start, 'Trial_s': trial_starts, 'First_response': first_response_arr, 'Sch_block': sch_block, 'Mod': mod } # for key, x in session_dict.items(): # print(key, len(x)) # Timestamps normalised to each trial start trial_dict = { 'Reward_ts': norm_rw, 'Pellet_ts': norm_pell, 'D_Pellet_ts': norm_dr, 'Schedule': schedule_type, 'Levers_ts': norm_lever, 'Err_ts': norm_err, 'Tone_s': norm_tone, 'Trial_s': norm_trial_s, 'First_response': norm_first_response, 'Sch_block': sch_block, 'Mod': mod } for key, val in trial_dict.items(): print( "Initialised trial df with {} trials and keys {}".format( len(val), list(trial_dict.keys()))) break self.trial_df = pd.DataFrame(session_dict) self.trial_df_norm = pd.DataFrame(trial_dict) # from bvmpc.bv_utils import check_fn # self.trial_df = pd.DataFrame(session_dict).applymap(check_fn) # self.trial_df_norm = pd.DataFrame(trial_dict).applymap(check_fn) @staticmethod def _extract_array(lines, start_char, end_char): """Private function to pull a single session from lines.""" def parse_line(line, dtype=np.float32): return np.array(line.lstrip().split()[1:]).astype(dtype) start_index = np.flatnonzero(lines == start_char) stop_index = np.flatnonzero(lines == end_char) if end_char == 'END': # Last timepoint does not have a end_char stop_index = [lines.size] if start_index[0] + 1 == stop_index[0]: return np.array([]) data_lines = lines[start_index[0] + 1:stop_index[0]] last_line = parse_line(data_lines[-1]) arr = np.empty( 5 * (len(data_lines) - 1) + len(last_line), dtype=np.float32) for i, line in enumerate(data_lines): numbers = parse_line(line) st = 5 * i arr[st:st + len(numbers)] = numbers return arr def _get_hdf5_name(self, ext="h5"): return "{}_{}_{}_{}.{}".format( self.get_metadata("subject"), self.get_metadata("start_date").replace("/", "-"), self.get_metadata("start_time")[:-3].replace(":", "-"), self.get_metadata("name"), ext) def __repr__(self): """ Return string representation of the Session. Currently includes the date, subject and trial type. """ return ( self.get_metadata("start_date") + " " + self.get_subject_type())Methods
def add_insert(self, to_insert)-
Add to list of inserted reward timestamps due to post-hoc correction.
Expand source code
def add_insert(self, to_insert): """ Add to list of inserted reward timestamps due to post-hoc correction. """ insert_list = self.get_insert() insert_list.append(to_insert) self.insert = insert_list def draw_umap(self, data, c, n_neighbors=15, min_dist=0.1, n_components=2, metric='euclidean', title='')-
For testing out UMAP parameters
Expand source code
def draw_umap( self, data, c, n_neighbors=15, min_dist=0.1, n_components=2, metric='euclidean', title=''): """ For testing out UMAP parameters """ import umap fit = umap.UMAP( n_neighbors=n_neighbors, min_dist=min_dist, n_components=n_components, metric=metric ) u = fit.fit_transform(data) fig = plt.figure() if n_components == 1: ax = fig.add_subplot(111) ax.scatter(u[:, 0], range(len(u)), c=c) if n_components == 2: ax = fig.add_subplot(111) ax.scatter(u[:, 0], u[:, 1], c=c) if n_components == 3: ax = fig.add_subplot(111, projection='3d') ax.scatter(u[:, 0], u[:, 1], u[:, 2], c=c, s=100) plt.title(title, fontsize=18) def extract_features(self, should_scale=True)-
Extract per trial features
Parameters
should_scale:bool, True- Optional. Whether to scale the data to unit variance
Returns
feat_df:pd.df- pandas dataframe with
Expand source code
def extract_features(self, should_scale=True): """ Extract per trial features Parameters ------ should_scale : bool, True Optional. Whether to scale the data to unit variance Returns ------- feat_df : pd.df pandas dataframe with """ df = self.get_trial_df_norm() feat_names = ["Trial_Len", "Resp_n", "Avg_Press_Rate (s)", "Avg_Press_Gradient", "Max_in_10s"] # Number of features before lever histogram n_feat_bh = len(feat_names) # Set hist bin size for lever responses here # h_bin_final = 3 # bins = np.arange(0, 60 + 0.00001, 60 / h_bin_final) # for i in np.arange(h_bin_final): # feat_names.append("L_Hist-{}".format(i)) features = np.zeros( shape=(len(df.index), len(feat_names))) for row in df.itertuples(): index = row.Index # Trial duration features[index, 0] = row.Reward_ts x = row.Levers_ts press_times = x[~np.isnan(x)] # Number of lever responses features[index, 1] = np.count_nonzero( ~np.isnan(row.Levers_ts)) # Average length between lever presses if len(press_times) > 1: features[index, 2] = np.mean(np.diff(press_times)) else: features[index, 2] = row.Reward_ts # Average rate of change of the rate of change. if len(press_times) > 2: features[index, 3] = np.mean( np.gradient(np.gradient(press_times, 1), 1)) else: features[index, 3] = 0 # Find the max number of response 10 seconds apart time_len = 10 dt = time_len / 2 max_val = 0 for value in press_times: in_range = np.logical_and( press_times >= value - dt, press_times <= value + dt) num_in_range = np.count_nonzero(in_range) if num_in_range > max_val: max_val = num_in_range features[index, 4] = max_val # Histogram values of lever responses # lever_hist = np.histogram( # x[~np.isnan(x)], bins=bins, density=True)[0] # features[index, n_feat_bh:(n_feat_bh + h_bin_final)] = lever_hist feat_df = pd.DataFrame(features, columns=feat_names) # Drop features/columns with all zeros feat_df = feat_df.loc[:, (feat_df != 0).any(axis=0)] if should_scale: # Normalize features scaler = StandardScaler() data_scaled = scaler.fit_transform(feat_df) feat_df = pd.DataFrame(data_scaled, columns=feat_df.columns) return feat_df def get_arrays(self, key=None)-
Return the info arrays in the session.
Parameters
key:str - Default None- The name of the info array to get.
Returns
np.ndarray: If key is a valid key. [] : If key is not a valid key Dict : If key is None, all the info arrays.
Expand source code
def get_arrays(self, key=None): """ Return the info arrays in the session. Parameters ---------- key : str - Default None The name of the info array to get. Returns ------- np.ndarray: If key is a valid key. [] : If key is not a valid key Dict : If key is None, all the info arrays. """ if key: return self.info_arrays.get(key, []) return self.info_arrays def get_block_ends(self)-
Return block end times - Tone end times + end of session
Expand source code
def get_block_ends(self): """Return block end times - Tone end times + end of session""" sound = self.info_arrays.get("sound", []) if len(sound) != 0: # Axona block_ends = np.concatenate([sound[1:], [sound[-1] + 305]]) else: # MED-PC trial_len = self.get_metadata("trial_length (mins)") * 60 trial_len += 5 repeated_trial_len = (trial_len) * 6 block_ends = np.arange(trial_len, repeated_trial_len, trial_len) return block_ends def get_cluster_ordered_df(self)-
Returns trial_df_norm in cluster order
Expand source code
def get_cluster_ordered_df(self): """ Returns trial_df_norm in cluster order """ if self.clust_reord_trial_df is None: reindex = self.cluster_results['reindex'] self.clust_reord_trial_df = self.get_trial_df_norm().reindex(reindex) return self.clust_reord_trial_df def get_cluster_ordering(self)-
Returns cluster ordering from hier_cluster
Expand source code
def get_cluster_ordering(self): """ Returns cluster ordering from hier_cluster """ if self.cluster_results is None: self.hier_cluster() return self.cluster_results['reindex'] def get_cluster_results(self, should_pca=True, cutoff=None)-
Returns cluster results from hier_cluster
Returns
dict:{'Z' : np.array, 'reindex' : list[int], 'clusters' : np.array}- {linkage matrix, index for sorting trials, cluster indices}
Expand source code
def get_cluster_results(self, should_pca=True, cutoff=None): """ Returns cluster results from hier_cluster Returns ------- dict: {'Z' : np.array, 'reindex' : list[int], 'clusters' : np.array} {linkage matrix, index for sorting trials, cluster indices} """ if self.cluster_results is None: self.hier_cluster(should_pca=should_pca, cutoff=cutoff) return self.cluster_results def get_date(self)-
Return date in datetime format
Expand source code
def get_date(self): """Return date in datetime format""" import datetime date = self.get_metadata('start_date') dd = datetime.datetime.strptime(date, '%m/%d/%y').date() return dd def get_err_lever_ts(self, include_un=True)-
Get the timestamps of the error/opposite lever presses.
Parameters
include_un:bool - Default True- Include lever presses that were unnecessary for the reward.
Returns
np.ndarray : A numpy array of sorted timestamps.
Expand source code
def get_err_lever_ts(self, include_un=True): """ Get the timestamps of the error/opposite lever presses. Parameters ---------- include_un : bool - Default True Include lever presses that were unnecessary for the reward. Returns ------- np.ndarray : A numpy array of sorted timestamps. """ levers = [ self.get_arrays("FR_Err"), self.get_arrays("FI_Err")] if include_un: levers.append(self.get_arrays("Un_FR_Err")) levers.append(self.get_arrays("Un_FI_Err")) return np.sort(np.concatenate(levers, axis=None)) def get_insert(self)-
Return a list of inserted reward timestamps due to post-hoc correction.
Expand source code
def get_insert(self): """ Return a list of inserted reward timestamps due to post-hoc correction. """ return self.insert def get_interval(self)-
Return the fixed ratio as an int.
Expand source code
def get_interval(self): """Return the fixed ratio as an int.""" interval = self.get_metadata("fixed_interval (secs)") if interval is not None: return int(interval) return None def get_lever_ts(self, include_un=True)-
Get the timestamps of the lever presses.
Parameters
include_un:bool - Default True- Include lever presses that were unnecessary for the reward.
Returns
np.ndarray : A numpy array of sorted timestamps.
Expand source code
def get_lever_ts(self, include_un=True): """ Get the timestamps of the lever presses. Parameters ---------- include_un : bool - Default True Include lever presses that were unnecessary for the reward. Returns ------- np.ndarray : A numpy array of sorted timestamps. """ levers = [ self.get_arrays("R"), self.get_arrays("L")] if include_un: levers.append(self.get_arrays("Un_R")) levers.append(self.get_arrays("Un_L")) return np.sort(np.concatenate(levers, axis=None)) def get_metadata(self, key=None)-
Get the metadata for the Session.
Parameters
key:str - Default None- Possible Keys: "start_date", "end_date", "subject", "experiment", "group", "box", "start_time", "end_time", "name"
Returns
str : If key is a valid key. Dict : If key is None, all the metadata.
Expand source code
def get_metadata(self, key=None): """ Get the metadata for the Session. Parameters ---------- key : str - Default None Possible Keys: "start_date", "end_date", "subject", "experiment", "group", "box", "start_time", "end_time", "name" Returns ------- str : If key is a valid key. Dict : If key is None, all the metadata. """ if key: return self.metadata.get(key, None) return self.metadata def get_one_lever_ts(self, lever_type, include_un=True)-
Get the timestamps of an individual lever press side. Lever type is expected to be "L" or "R".
Expand source code
def get_one_lever_ts(self, lever_type, include_un=True): """ Get the timestamps of an individual lever press side. Lever type is expected to be "L" or "R". """ if (lever_type != "L") and (lever_type != "R"): raise ValueError( "lever_type is expected to be L or R, entered {}".format( lever_type)) levers = [self.get_arrays(lever_type)] if include_un: levers.append(self.get_arrays("Un_" + lever_type)) return np.sort(np.concatenate(levers, axis=None)) def get_ratio(self)-
Return the fixed ratio as an int.
Expand source code
def get_ratio(self): """Return the fixed ratio as an int.""" ratio = self.get_metadata("fixed_ratio") if ratio is not None: return int(ratio) return None def get_rw_ts(self)-
Get the timestamps of rewards.
Corrected for session switching/ending without reward collection.
Returns
np.ndarray : A numpy array of sorted timestamps.
Expand source code
def get_rw_ts(self): """ Get the timestamps of rewards. Corrected for session switching/ending without reward collection. Returns ------- np.ndarray : A numpy array of sorted timestamps. """ session_type = self.get_metadata('name') stage = session_type[:2].replace('_', '') pell_ts = self.get_arrays("Reward") reward_times = self.get_arrays("Nosepoke") pell_ts_exdouble = self.split_pell_ts()[0] reward_times = self.get_arrays("Nosepoke") trial_len = self.get_metadata("trial_length (mins)") * 60 if stage == '7' or stage == '6': # Check if trial switched before reward collection # -> Adds collection as switch time block_ends = self.get_block_ends() split_pell_ts = split_into_blocks( pell_ts_exdouble, blocks=block_ends) split_reward_ts = split_into_blocks( reward_times, blocks=block_ends) for i, (pell, reward) in enumerate( zip(split_pell_ts, split_reward_ts)): if len(pell) > len(reward): block_len = block_ends[i] reward_times = np.insert( reward_times, np.searchsorted( reward_times, block_len), block_len) # if last reward time < last pellet dispensed, # assume animal picked reward at end of session. # Checks if last block contains responses first if reward_times[-1] < pell_ts[-1]: if stage == '7' or stage == '6': reward_times = np.append(reward_times, block_ends[-1]) else: reward_times = np.append(reward_times, trial_len) return np.sort(reward_times, axis=None) def get_stage(self)-
Obtain the stage number (without _).
Expand source code
def get_stage(self): """Obtain the stage number (without _).""" session_type = self.get_metadata('name') stage = session_type[:2].replace('_', '') return stage def get_subject_type(self)-
Return the subject and session type as a string.
Expand source code
def get_subject_type(self): """Return the subject and session type as a string.""" subject = self.get_metadata("subject") name = self.get_metadata("name") return 'Subject: {}, Trial Type {}'.format(subject, name) def get_title(self)-
Expand source code
def get_title(self): if self.title is None: date = self.get_metadata('start_date').replace('/', '_') sub = self.get_metadata('subject') stage = self.get_stage() self.title = '{} {} S{}'.format(sub, date, stage) return self.title def get_tone_starts(self)-
Return tone start times
Expand source code
def get_tone_starts(self): """Return tone start times""" sound = self.info_arrays.get("sound", []) if len(sound) != 0: # Axona block_starts = np.array(sound - 5) else: # MED-PC trial_len = self.get_metadata("trial_length (mins)") * 60 repeated_trial_len = (trial_len) * 6 block_starts = np.arange(0, repeated_trial_len, trial_len) return block_starts def get_trial_df(self)-
Get dataframe split into trials without normalisation (time).
Returns
mod:used to identify trials where reward timestamps modified post-hoc- trial_df = { 'Reward_ts': reward_times, 'Pellet_ts': pell_ts_exdouble, 'D_Pellet_ts': trial_dr_ts, 'Schedule': schedule_type, 'Levers_ts': trial_lever_ts, 'Err_ts': trial_err_ts, 'Tone_s': trial_tone_start, 'Trial_s': trial_norm[:-1], 'Mod': mod }
Expand source code
def get_trial_df(self): """Get dataframe split into trials without normalisation (time). Returns ------- mod: used to identify trials where reward timestamps modified post-hoc trial_df = { 'Reward_ts': reward_times, 'Pellet_ts': pell_ts_exdouble, 'D_Pellet_ts': trial_dr_ts, 'Schedule': schedule_type, 'Levers_ts': trial_lever_ts, 'Err_ts': trial_err_ts, 'Tone_s': trial_tone_start, 'Trial_s': trial_norm[:-1], 'Mod': mod } """ if self.trial_df is None: self.init_trial_dataframe() return self.trial_df def get_trial_df_norm(self)-
Get dataframe split into trials with normalisation (time).
Expand source code
def get_trial_df_norm(self): """Get dataframe split into trials with normalisation (time).""" if self.trial_df_norm is None: self.init_trial_dataframe() return self.trial_df_norm def get_valid_tdf(self, excl_dr=False, norm=False)-
Returns trial_df of excluding: First trial in FI blocks Trials with post-hoc modification of reward timestamp Trials more than 60s
Parameters
excl_dr:bool, False- Optional. Excludes double reward trials
norm:bool, False- Optional. Return trial_df_norm with masks applied
Expand source code
def get_valid_tdf(self, excl_dr=False, norm=False): """ Returns trial_df of excluding: First trial in FI blocks Trials with post-hoc modification of reward timestamp Trials more than 60s Parameters ---------- excl_dr : bool, False Optional. Excludes double reward trials norm : bool, False Optional. Return trial_df_norm with masks applied """ if norm: df = self.get_trial_df_norm() else: df = self.get_trial_df() # Masks used for filtering trials mod_mask = df.Mod.notnull() # trials with post-hoc rw correction FI_s_mask = (df.Tone_s.str.len() != 0) & ( df.Schedule == 'FI') # 1st trial of each FI blocks tlen_mask = (df.Reward_ts - df.Trial_s) > 60 # trials longer then 60s df = df[(~mod_mask) & (~FI_s_mask) & (~tlen_mask)] if excl_dr: dr_mask = df.D_Pellet_ts.str.len() != 0 df = df[(~dr_mask)] return df def hier_cluster(self, should_scale=True, should_pca=True, cutoff=None, test_methods=False)-
Perform hierarchical clustering on per trial features
Parameters
should_scale:bool, True- whether to scale the data to unit variance
cutoff:int, None- Optional. Level at which to cutoff dendrogram
Returns
dict:{'Z' : np.array, 'reindex' : list[int], 'clusters' : np.array}- {linkage matrix, index for sorting trials, cluster indices}
Expand source code
def hier_cluster( self, should_scale=True, should_pca=True, cutoff=None, test_methods=False): """ Perform hierarchical clustering on per trial features Parameters ------ should_scale : bool, True whether to scale the data to unit variance cutoff: int, None Optional. Level at which to cutoff dendrogram Returns ------- dict: {'Z' : np.array, 'reindex' : list[int], 'clusters' : np.array} {linkage matrix, index for sorting trials, cluster indices} """ if should_pca: _, data, _ = self.perform_pca( n_components=5) # Use PCA as features else: data = self.extract_features( should_scale=should_scale) # Use raw features from bvmpc.bv_utils import test_all_hier_clustering if test_methods: test_all_hier_clustering(data, verbose=True) Z = shc.linkage(data, method='centroid') # linkage matrix c, coph_dists = cophenet(Z, pdist(data)) print('Cophentic Correlation Coefficient: ', c) dend = shc.dendrogram(Z, no_plot=True, color_threshold=cutoff) reindex = list(map(int, dend['ivl'])) # index for sorting trials if cutoff is None: cutoff = 0.7 * max(Z[:, 2]) clusters = shc.fcluster(Z, cutoff, criterion='distance') self.cluster_results = { 'Z': Z, 'reindex': reindex, 'clusters': clusters, } return self.cluster_results def init_trial_dataframe(self)-
Initialise the trial based Pandas dataframe for this session.
Expand source code
def init_trial_dataframe(self): """Initialise the trial based Pandas dataframe for this session.""" self._init_trial_df() def old_extract_features(self, should_scale=True)-
Extract per trial features This is the previous version of this function.
Parameters
should_scale:bool, True- Optional. Whether to scale the data to unit variance
Returns
feat_df:pd.df- pandas dataframe with
Expand source code
def old_extract_features(self, should_scale=True): """ Extract per trial features This is the previous version of this function. Parameters ------ should_scale : bool, True Optional. Whether to scale the data to unit variance Returns ------- feat_df : pd.df pandas dataframe with """ df = self.get_trial_df_norm() feat_names = ["Trial_Len", "Pell", "Rw_lat", "Err", "1st_Resp", "Resp_n", "Avg_Press_Rate", "Avg_Press_Gradient"] # Number of features before lever histogram n_feat_bh = len(feat_names) # Set hist bin size for lever responses here h_bin_final = 4 for i in np.arange(h_bin_final): feat_names.append("L_Hist-{}".format(i)) features = np.zeros( shape=(len(df.index), len(feat_names))) for row in df.itertuples(): index = row.Index # Trial duration features[index, 0] = row.Reward_ts # Completion Latency features[index, 1] = row.Pellet_ts # Reward Latency features[index, 2] = row.Reward_ts - row.Pellet_ts # Number of err responses features[index, 3] = row.Err_ts.size # Time to first response features[index, 4] = row.First_response # Number of lever responses features[index, 5] = np.count_nonzero( ~np.isnan(row.Levers_ts)) x = row.Levers_ts press_times = x[~np.isnan(x)] # Average length between lever presses if len(press_times) > 1: features[index, 6] = np.mean(np.diff(press_times)) else: features[index, 6] = row.Reward_ts # Average rate of change of the rate of change. if len(press_times) > 2: features[index, 7] = np.mean( np.gradient(np.gradient(press_times, 1), 1)) else: features[index, 7] = 0 # Histogram values of lever responses lever_hist = np.histogram( x[~np.isnan(x)], bins=h_bin_final, density=True)[0] features[index, n_feat_bh:(n_feat_bh + h_bin_final)] = lever_hist # double_pellet_time = row.D_Pellet_ts # if len(double_pellet_time) == 0: # d_pell_feature = 0 # else: # d_pell_feature = double_pellet_time # features[index, 2] = d_pell_feature # err_hist = np.histogram( # row.Err_ts, bins=5, density=False)[0] feat_df = pd.DataFrame(features, columns=feat_names) # Drop features/columns with all zeros feat_df = feat_df.loc[:, (feat_df != 0).any(axis=0)] if should_scale: # Normalize features scaler = StandardScaler() data_scaled = scaler.fit_transform(feat_df) feat_df = pd.DataFrame(data_scaled, columns=feat_df.columns) return feat_df def perform_HDBSCAN(self, should_scale=True)-
Testing purposes only. Perform HDBSCAN on per trial features.
Expand source code
def perform_HDBSCAN(self, should_scale=True): """ Testing purposes only. Perform HDBSCAN on per trial features. """ fig, ax = plt.subplots(1, 2, figsize=(15, 8)) import hdbscan _, data, _ = self.perform_pca( n_components=None, should_scale=should_scale) ax[0].set_xlabel('PCA 1') ax[0].set_ylabel('PCA 2') df = self.get_trial_df_norm() markers = df['Schedule'] clusterer = hdbscan.HDBSCAN( min_cluster_size=2) clusterer.fit(data) print(clusterer.labels_.max()) # HDBSCAN(algorithm='best', alpha=1.0, approx_min_span_tree=True, # gen_min_span_tree=False, leaf_size=40, memory=Memory(cachedir=None), # metric='euclidean', min_cluster_size=5, min_samples=None, p=None) sns.scatterplot(data[:, 0], data[:, 1], s=50, linewidth=0, style=markers, hue=clusterer.labels_, palette='deep', ax=ax[0]) clusterer.condensed_tree_.plot(select_clusters=True, selection_palette=sns.color_palette('deep')[1:], axis=ax[1]) # Plots condensed dendogram plt.suptitle('HDBSCAN', fontsize=24) fig.text(0.5, 0.91, self.get_title(), transform=fig.transFigure, ha='center') plt.show() exit(-1) def perform_UMAP(self, should_scale=True)-
Testing purposes only. Perform UMAP on per trial features.
Expand source code
def perform_UMAP(self, should_scale=True): """ Testing purposes only. Perform UMAP on per trial features. """ import umap data = self.extract_features(should_scale=should_scale) df = self.get_trial_df_norm() color = [sns.color_palette()[x] for x in df.Schedule.astype('category').cat.codes] for n in (2, 5, 10, 20, 50, 100, 200): self.draw_umap(data, color, n_neighbors=n, title='n_neighbors = {}'.format(n)) plt.show() exit(-1) # reducer = umap.UMAP() # embedding = reducer.fit_transform(data) # plt.scatter(embedding[:, 0], embedding[:, 1], c=[ # sns.color_palette()[x] for x in df.Schedule.astype('category').cat.codes]) # plt.gca().set_aspect('equal', 'datalim') # plt.title('UMAP projection of the Behav', fontsize=24) # plt.show() return data def perform_pca(self, n_components=5, should_scale=True)-
Perform PCA on per trial features
Parameters
n_components:intorfloat- the number of PCA components to compute if float, uses enough components to reach that much variance
should_scale - whether to scale the data to unit variance
Returns
tuple:(pd.df, pd.df, PCA)- (features 2d array, PCA of features, PCA object)
Expand source code
def perform_pca(self, n_components=5, should_scale=True): """ Perform PCA on per trial features Parameters ------ n_components : int or float the number of PCA components to compute if float, uses enough components to reach that much variance should_scale - whether to scale the data to unit variance Returns ------- tuple : (pd.df, pd.df, PCA) (features 2d array, PCA of features, PCA object) """ data = self.extract_features(should_scale=should_scale) pca = PCA(n_components=n_components) after_pca = pca.fit_transform(data) after_pca = pd.DataFrame( after_pca, columns=['PC{}'.format(x) for x in np.arange( after_pca.shape[1]) + 1]) print( "\nPCA fraction of explained variance", pca.explained_variance_ratio_) print( "Total explained variance: ", sum(pca.explained_variance_ratio_)) return data, after_pca, pca def save_to_h5(self, out_dir, name=None)-
Save information to a h5 file.
Expand source code
def save_to_h5(self, out_dir, name=None): """Save information to a h5 file.""" location = name if name is None: location = self._get_hdf5_name() self.h5_file = os.path.join(out_dir, location) self._save_h5_info() def save_to_neo(self, out_dir, name=None, neo_backend='nix', remove_existing=False)-
Save information to a neo file.
Expand source code
def save_to_neo( self, out_dir, name=None, neo_backend="nix", remove_existing=False): """Save information to a neo file.""" location = name self.neo_backend = neo_backend if name is None: ext = self._get_neo_io(get_ext=True) location = self._get_hdf5_name(ext=ext) self.neo_file = os.path.join(out_dir, location) self._save_neo_info(remove_existing) def split_pell_ts(self)-
Return a tuple of arrays splitting up the rewards.
(Pellets without doubles, time of doubles)
Expand source code
def split_pell_ts(self): """ Return a tuple of arrays splitting up the rewards. (Pellets without doubles, time of doubles) """ pell_ts = self.get_arrays("Reward") dpell_bool = np.diff(pell_ts) < 0.8 # Provides index of double pell in pell_ts dpell_idx = np.nonzero(dpell_bool)[0] + 1 dpell = pell_ts[dpell_idx] # pell drop ts excluding double ts pell_ts_exdouble = np.delete(pell_ts, dpell_idx) return pell_ts_exdouble, dpell def split_sess(self, norm=True, blocks=None, error_only=False, all_levers=False)-
Split a session up into multiple blocks.
blocks: defines timepoints to split.
returns 5 outputs: 1) timestamps split into rows depending on blocks input -> norm_reward_ts, norm_lever_ts, norm_err_ts, norm_double_r_ts 2) print to include in title and file name. Mainly for stage 7.
Expand source code
def split_sess( self, norm=True, blocks=None, error_only=False, all_levers=False): """ Split a session up into multiple blocks. blocks: defines timepoints to split. returns 5 outputs: 1) timestamps split into rows depending on blocks input -> norm_reward_ts, norm_lever_ts, norm_err_ts, norm_double_r_ts 2) print to include in title and file name. Mainly for stage 7. """ session_type = self.get_metadata('name') stage = session_type[:2].replace('_', '') reward_times = self.get_rw_ts() timestamps = self.get_arrays() lever_ts = self.get_lever_ts() pell_ts = timestamps["Reward"] pell_double = np.nonzero(np.diff(pell_ts) < 0.8) # returns reward ts after d_pell reward_double = reward_times[ np.searchsorted( reward_times, pell_ts[pell_double], side='right')] err_lever_ts = [] if blocks is not None: pass else: # blocks = np.arange(5, 1830, 305) # Default split into schedules blocks = self.get_tone_starts() + 5 # Default split into schedules incl = "" # Initialize print for type of extracted lever_ts if stage == '7' and error_only: # plots errors only incl = '_Errors_Only' lever_ts = self.get_err_lever_ts() elif stage == '7' and all_levers: # plots all responses incl. errors incl = '_All' err_lever_ts = self.get_err_lever_ts() lever_ts = np.sort(np.concatenate(( lever_ts, err_lever_ts), axis=None)) elif stage == '7': # plots all responses exclu. errors incl = '_Correct Only' split_lever_ts = np.split(lever_ts, np.searchsorted(lever_ts, blocks)) split_reward_ts = np.split(reward_times, np.searchsorted(reward_times, blocks)) split_double_r_ts = np.split(reward_double, np.searchsorted(reward_double, blocks)) split_err_ts = np.split(err_lever_ts, np.searchsorted(err_lever_ts, blocks)) norm_reward_ts = [] norm_lever_ts = [] norm_err_ts = [] norm_double_r_ts = [] if norm: for i, l in enumerate(split_lever_ts[1:]): norm_lever_ts.append(np.append([0], l - blocks[i], axis=0)) norm_reward_ts.append(split_reward_ts[i + 1] - blocks[i]) norm_double_r_ts.append(split_double_r_ts[i + 1] - blocks[i]) if stage == '7' and all_levers: # plots all responses incl. errors norm_err_ts.append(split_err_ts[i + 1] - blocks[i]) else: norm_lever_ts = split_lever_ts[1:] norm_reward_ts = split_reward_ts[1:] norm_err_ts = split_err_ts[1:] norm_double_r_ts = split_double_r_ts[1:] return ( norm_reward_ts, norm_lever_ts, norm_err_ts, norm_double_r_ts, incl) def time_taken(self)-
Calculate how long the Session took in mins.
Expand source code
def time_taken(self): """Calculate how long the Session took in mins.""" start_time = self.get_metadata("start_time")[-8:].replace(' ', '0') end_time = self.get_metadata("end_time")[-8:].replace(' ', '0') fmt = '%H:%M:%S' tdelta = ( datetime.strptime(end_time, fmt) - datetime.strptime(start_time, fmt)) tdelta_mins = int(tdelta.total_seconds() / 60) return tdelta_mins