import requests
import numpy as np
import pandas as pd
from genepy3d.io.base import CatmaidApiTokenAuth
from genepy3d.obj import trees
[docs]
class Catmaid:
"""Support reading neuron data from Catmaid [1].
Attributes:
dfneu (pandas dataframe): neuron table.
dfcon (pandas dataframe): connector table.
References:
.. [1] https://catmaid.readthedocs.io/en/stable/index.html
"""
def _assign_dfneu(self,dfneu):
"""Check and assign neuron data to class attribute.
"""
if dfneu is None:
raise Exception("need to provide neuron table.")
# remove nan
dfneu.dropna(inplace=True)
# cast type
dfneu['neuron_id'] = dfneu['neuron_id'].astype('int')
dfneu['treenode_id'] = dfneu['treenode_id'].astype('int')
dfneu['structure_id'] = dfneu['structure_id'].astype('int')
dfneu['parent_treenode_id'] = dfneu['parent_treenode_id'].astype('int')
self.dfneu = dfneu
def _assign_dfcon(self,dfcon):
"""Check and assign connector data to class attribute.
"""
if dfcon is None:
self.dfcon = None
# remove nan
dfcon.dropna(inplace=True) # drop invalid rows
# constraint columns to int
dfcon['connector_id'] = dfcon['connector_id'].astype('int')
dfcon['neuron_id'] = dfcon['neuron_id'].astype('int')
dfcon['treenode_id'] = dfcon['treenode_id'].astype('int')
self.dfcon = dfcon
def _compute_dfsyn(self):
"""Compute synaptic relationship from connector data.
"""
if self.dfcon is not None:
# getting synapse detail
syninfo = []
conidlist = self.dfcon['connector_id'].unique()
for conid in conidlist:
pre = self.dfcon[(self.dfcon['connector_id']==conid)&(self.dfcon['relation_id']=='presynaptic_to')][['neuron_id','treenode_id']].values.tolist()
posts = self.dfcon[(self.dfcon['connector_id']==conid)&(self.dfcon['relation_id']=='postsynaptic_to')][['neuron_id','treenode_id']].values.tolist()
if len(pre)==0:
continue
elif len(pre)>1:
raise ValueError('unknown situation: multiple presynaptic_to')
break
else:
if len(posts)==0:
continue
else:
for post in posts:
syninfo.append(pre[0]+post)
# check syninfo
if len(syninfo)==0:
self.dfsyn = None
else:
self.dfsyn = pd.DataFrame(syninfo,columns=['pre_neuron_id','pre_treenode_id','post_neuron_id','post_treenode_id'])
def __init__(self,dfneu,dfcon=None):
self._assign_dfneu(dfneu)
self._assign_dfcon(dfcon)
self._compute_dfsyn()
[docs]
@classmethod
def from_csv(cls, neuronfile, confile=None):
"""Read neuron data from Catmaid csv files.
Args:
neuronfile (str): path to neuron csv file.
confile (str): path to connector csv file.
Returns:
Catmaid object.
"""
# read neuronfile
try:
dfneu = pd.read_csv(neuronfile)
except:
raise Exception('Failed to load neuron file.')
labels = dfneu.columns.values
refined_labels = []
for lbl in labels:
refined_labels.append(lbl.split()[0].lower()) # extract all column names, remove irregular characters
if all(lbl in refined_labels for lbl in ['neuron_name','neuron_id','treenode_id','structure_id','x','y','z','r','parent_treenode_id']):
dfneu.columns = refined_labels
dfneu = dfneu[['neuron_name','neuron_id','treenode_id','structure_id','x','y','z','r','parent_treenode_id']] # select only conventional columns
elif all(lbl in refined_labels for lbl in ['neuron','skeleton_id','treenode_id','parent_treenode_id','x','y','z','r']):
dfneu.columns = refined_labels
dfneu['structure_id'] = 0 # add structure_id column
dfneu.rename(columns={'neuron':'neuron_name','skeleton_id':'neuron_id'},inplace=True) # rename following the convention
dfneu = dfneu[['neuron_name','neuron_id','treenode_id','structure_id','x','y','z','r','parent_treenode_id']] # select only conventional columns
dfneu['parent_treenode_id'].fillna(-1,inplace=True) # replace NaN by -1
else:
raise ValueError("The file must contain columns 'neuron','skeleton_id','treenode_id','parent_treenode_id','x','y','z','r'.")
# read connector file
dfcon = None
if confile is not None:
try:
dfcon = pd.read_csv(confile)
except:
raise Exception ('Failed to load connector file.')
labels = dfcon.columns.values
refined_labels = []
for lbl in labels:
refined_labels.append(lbl.split()[0].lower())
if all(lbl in refined_labels for lbl in ['connector_id','neuron_id','treenode_id','relation_id']):
dfcon.columns = refined_labels
dfcon = dfcon[['connector_id','neuron_id','treenode_id','relation_id']]
elif all(lbl in refined_labels for lbl in ['connector_id','skeleton_id','treenode_id','relation_id']):
dfcon.columns = refined_labels
dfcon.rename(columns={'skeleton_id':'neuron_id'},inplace=True) # rename some columns
dfcon = dfcon[['connector_id','neuron_id','treenode_id','relation_id']]
else:
raise ValueError("The file must contain columns 'connector_id','skeleton_id','treenode_id','relation_id'.")
return cls(dfneu,dfcon)
[docs]
@staticmethod
def get_neuron_id_from_server(host,token,project_id):
"""Return list of neuron IDs from given project ID.
Args:
host (str): address of CATMAID server.
token (str): authenticated string.
project_id (int): project ID.
Returns:
array of int.
"""
# restful request
linkrequest = host+'{}/skeletons/'.format(project_id)
res = requests.get(linkrequest,auth=CatmaidApiTokenAuth(token))
if res.status_code!=200:
raise ValueError('something wrong: check again your host, token or project id.')
else:
if isinstance(res.json(),dict):
raise ValueError('something wrong: check again your project id.')
else: # should be a list
return np.array(res.json())
[docs]
@classmethod
def from_server(cls,host,token,project_id,neuron_id=None):
"""Import neuron data from Catmaid server.
Args:
host (str): Catmaid host address.
token (str): authentication token.
project_id (int): project ID.
neuron_id (int|list of int): list of neuron IDs.
Returns:
Catmaid object.
"""
if neuron_id is None:
neuronlst = cls.get_neuron_id_from_server(host,token,project_id)
elif isinstance(neuron_id,(int,np.integer)):
neuronlst = [neuron_id]
else:
neuronlst = neuron_id
neuinfo, coninfo = [], []
# query data from catmaid
for neuid in neuronlst:
linkrequest = host+'{}/skeleton/{}/json'.format(project_id,neuid)
res = requests.get(linkrequest,auth=CatmaidApiTokenAuth(token))
if res.status_code!=200:
raise ValueError('something wrong: check again your host, token, project id or neuron id.')
else:
if isinstance(res.json(),dict):
raise ValueError('something wrong: check again your project id or neuron id.')
else: # should be a list
res = np.array(res.json())
# get neuron name
try:
# neuname = res[0].encode('ascii','ignore') # hdf5 does not support unicode
neuname = res[0]
except:
neuname = 'genepy3d'
subneu, subcon = [], []
# get neuron info
try:
for iske in res[1]:
if iske[1] is not None:
iske_sub = [neuname, neuid, iske[0], 0, iske[3], iske[4], iske[5], iske[6], iske[1]]
else:
iske_sub = [neuname, neuid, iske[0], 0, iske[3], iske[4], iske[5], iske[6], -1]
subneu.append(iske_sub)
except:
subneu = []
# get connector info
try:
for icon in res[3]:
relation_type = 'presynaptic_to'
if icon[2]==1:
relation_type = 'postsynaptic_to'
icon_sub = [icon[1], neuid, icon[0], relation_type]
subcon.append(icon_sub)
except:
subcon = []
neuinfo = neuinfo + subneu
coninfo = coninfo + subcon
# create dataframe from data
if len(neuinfo)==0:
raise Exception("neuron table is empty.")
else:
dfneu = pd.DataFrame(neuinfo,columns=['neuron_name','neuron_id','treenode_id','structure_id','x','y','z','r','parent_treenode_id'])
if len(coninfo)==0:
dfcon = None
else:
dfcon = pd.DataFrame(coninfo,columns=['connector_id','neuron_id','treenode_id','relation_id'])
return cls(dfneu,dfcon)
[docs]
def get_neuron_id(self,neuron_name=None):
"""Return neuron IDs from neuron names.
Args:
neuron_name (str | array of str): list of neuron names.
Returns:
pandas Series whose index is name and value is ID.
"""
if neuron_name is None:
subdf = self.dfneu[['neuron_id','neuron_name']].drop_duplicates().copy() # get all neuron ids
subdf.set_index("neuron_name",inplace=True)
return subdf
else:
if isinstance(neuron_name,str): # only one neuron name
try:
subdf = self.dfneu[self.dfneu['neuron_name']==neuron_name][['neuron_id','neuron_name']].drop_duplicates().copy()
subdf.set_index("neuron_name",inplace=True)
return subdf
except:
return None
else:
try:
subdf = self.dfneu[self.dfneu['neuron_name'].isin(neuron_name)][['neuron_id','neuron_name']].drop_duplicates().copy()
subdf.set_index("neuron_name",inplace=True)
return subdf
except:
return None
[docs]
def get_neuron_name(self,neuron_id=None):
"""Return neuron names from neuron IDs.
Args:
neuron_id (int | array of int): list of neuron names.
Returns:
pandas Series whose index is ID and value is name.
"""
if neuron_id is None:
subdf = self.dfneu[['neuron_id','neuron_name']].drop_duplicates().copy() # get all neuron ids
subdf.set_index("neuron_id",inplace=True)
return subdf
else:
if isinstance(neuron_id,(int,np.integer)): # only one neuron name
try:
subdf = self.dfneu[self.dfneu['neuron_id']==neuron_id][['neuron_id','neuron_name']].drop_duplicates().copy()
subdf.set_index("neuron_id",inplace=True)
return subdf
except:
return None
else:
try:
subdf = self.dfneu[self.dfneu['neuron_id'].isin(neuron_id)][['neuron_id','neuron_name']].drop_duplicates().copy()
subdf.set_index("neuron_id",inplace=True)
return subdf
except:
return None
[docs]
def get_synaptic_relation(self,relation_id="presynaptic_to",nb_connectors=1):
"""Return neurons and corresponding treenodes filtered by their synaptic relations.
A neuron can have multiple presynaptic gates (for receiving signals from others neurons),
and only one postsynaptic gate (for sending signal).
Args:
relation_id (str): presynaptic_to or postsynaptic_to.
nb_connectors (uint): number of connectors. If None, then return all neurons regardess their number of connectors.
Returns:
Dictionary whose keys are neuron_id, values are treenode_ids.
"""
subdf = self.dfcon[self.dfcon['relation_id']==relation_id].copy()
subdf_counts = subdf.groupby(['neuron_id'])['treenode_id'].count()
dic = {}
if nb_connectors is None:
neulst = subdf_counts.index.values
else:
neulst = subdf_counts[subdf_counts==nb_connectors].index.values
for neuron_id in neulst:
dic[neuron_id] = list(subdf[subdf["neuron_id"]==neuron_id]["treenode_id"].values)
return dic
[docs]
def get_innervation_relation(self,nb_innervations=1):
"""Return neurons filtered by innervation relation.
Innervation: a neuron receives signals from one (mono) or many (multi) other neurons via its presynaptic gates.
We assume innervated neuron as postsynaptic neuron, the ones who make innervation are presynaptic neurons.
Args:
nb_innervations (int): number innervated presynaptic neurons. If None, then return all neurons.
Returns:
dictionary whose key is postsynaptic neuron, value is innervating presynaptic neurons.
"""
dic = {}
if self.dfsyn is not None:
subdf = self.dfsyn.sort_values(['post_neuron_id'])
subdf_counts = subdf.groupby(['post_neuron_id'])['pre_neuron_id'].count()
if nb_innervations is None:
neulst = subdf_counts.index.values
else:
neulst = subdf_counts[subdf_counts==nb_innervations].index.values
for neuron_id in neulst:
dic[neuron_id] = subdf[subdf["post_neuron_id"]==neuron_id][["pre_neuron_id","pre_treenode_id"]].values.tolist()
return dic
[docs]
def get_neurons(self,neuron_id=None,scales=(1.,1.,1.)):
"""Return neurons (Tree object) of a given list of neuron IDs.
Args:
neuron_id (int | array of int): list of neuron IDs.
scales (tuple (float)): define x, y, z scales
Returns:
list of neurons (``tree.Tree``)
"""
# check neuron_id
if neuron_id is None:
neuronlst = self.get_neuron_id().values.flatten()
else:
if isinstance(neuron_id,(int,np.integer)): # only one item.
neuronlst = [neuron_id]
elif isinstance(neuron_id, (list, np.ndarray)): # array-like
neuronlst = neuron_id
else:
raise Exception('neuron_id must be int or array-like.')
dic = {}
for neuid in neuronlst:
subdfneu = self.dfneu[self.dfneu['neuron_id']==neuid].copy()
if len(subdfneu)==0:
raise ValueError("check again neuron id.")
else:
neuname = self.get_neuron_name(neuid).values.flatten()[0]
subdfcon = None
if self.dfcon is not None:
subdfcon = self.dfcon[self.dfcon['neuron_id']==neuid].copy()
if len(subdfcon)==0:
subdfcon = None
# scale x, y and z
subdfneu["x"] = subdfneu["x"]*scales[0]
subdfneu["y"] = subdfneu["y"]*scales[1]
subdfneu["z"] = subdfneu["z"]*scales[2]
dic[neuid] = trees.Tree.from_table(subdfneu,subdfcon,neuid,neuname)
if len(neuronlst)==1: #only 1 tiem
return dic[neuid]
else:
return dic
[docs]
def to_csv(self,neuron_file="dfneu.csv",connector_file="dfcon.csv",neuron_id=None):
"""Export the neuron table and connector table into csv files.
We can extract for a subset of neuron given by ``neuron_id``.
Args:
neuron_file (str): file path to save the neuron table.
connector_file (str): file path to save the connector table.
neuron_id (list of int): list of neuron IDs to be exported. If None, then all neurons were exported.
"""
if neuron_id is None:
neulst = self.get_neuron_id().values.flatten()
elif isinstance(neuron_id,(int,np.integer)):
neulst = [neuron_id]
else:
neulst = neuron_id
conidlst = self.dfcon[self.dfcon["neuron_id"].isin(neulst)]["connector_id"].unique()
subdfcon = self.dfcon[self.dfcon["connector_id"].isin(conidlst)].copy()
fullneulst = subdfcon["neuron_id"].unique()
subdfneu = self.dfneu[self.dfneu["neuron_id"].isin(fullneulst)].copy()
try:
subdfneu.to_csv(neuron_file)
subdfcon.to_csv(connector_file)
except:
raise Exception("Fail when exporting to csv.")