Beispiel #1
0
    def __init__(self,
                 resolution=None,
                 cache=True,
                 manifest_file=None,
                 ccf_version=None,
                 base_uri=None,
                 version=None):

        if manifest_file is None:
            manifest_file = get_default_manifest_file('mouse_connectivity')

        if version is None:
            version = self.MANIFEST_VERSION

        if resolution is None:
            resolution = MouseConnectivityApi.VOXEL_RESOLUTION_25_MICRONS

        if ccf_version is None:
            ccf_version = MouseConnectivityApi.CCF_VERSION_DEFAULT

        super(MouseConnectivityCache,
              self).__init__(resolution,
                             reference_space_key=ccf_version,
                             cache=cache,
                             manifest=manifest_file,
                             version=version)

        self.api = MouseConnectivityApi(base_uri=base_uri)
Beispiel #2
0
    def __init__(self):
        # get mouse connectivity cache and structure tree
        self.mcc = MouseConnectivityCache(manifest_file=os.path.join(
            self.mouse_connectivity_cache, "manifest.json"))
        self.structure_tree = self.mcc.get_structure_tree()

        # get ontologies API and brain structures sets
        self.oapi = OntologiesApi()

        # get reference space
        self.space = ReferenceSpaceApi()
        self.spacecache = ReferenceSpaceCache(
            manifest=os.path.join(
                self.annotated_volume_fld, "manifest.json"
            ),  # downloaded files are stored relative to here
            resolution=int(self.resolution[0]),
            reference_space_key=
            "annotation/ccf_2017",  # use the latest version of the CCF
        )
        self.annotated_volume, _ = self.spacecache.get_annotation_volume()

        # mouse connectivity API [used for tractography]
        self.mca = MouseConnectivityApi()

        # Get tree search api
        self.tree_search = TreeSearchApi()
Beispiel #3
0
def mca(mock_imports):
    _, MCA = mock_imports

    ju.read_url_get = \
        Mock(name='read_url_get',
             return_value={'msg': _msg})
    ju.write = \
        Mock(name='write')

    ju.read = \
        Mock(name='read',
             return_value=_pd_msg)

    pj.read_json = \
        Mock(name='read_json',
             return_value=_pd_msg)

    pd.DataFrame.to_csv = \
        Mock(name='to_csv')

    pd.DataFrame.read_csv = \
        Mock(name='read_csv',
             return_value=_csv_msg)

    Manifest.safe_mkdir = Mock(name='safe_mkdir')

    mca = MCA()
    mca.retrieve_file_over_http = Mock(name='retrieve_file_over_http')

    return mca
    def __init__(self,
                 resolution=25,
                 cache=True,
                 manifest_file='mouse_connectivity_manifest.json',
                 base_uri=None):
        super(MouseConnectivityCache, self).__init__(manifest=manifest_file,
                                                     cache=cache)

        self.resolution = resolution
        self.api = MouseConnectivityApi(base_uri=base_uri)
Beispiel #5
0
    def __init__(self,
                 projection_metric="projection_energy",
                 base_dir=None,
                 **kwargs):
        """ 
		Set up file paths and Allen SDKs
		
		:param base_dir: path to directory to use for saving data (default value None)
		:param path_fiprojection_metricle: - str, metric to quantify the strength of projections from the Allen Connectome. (default value 'projection_energy')
		:param kwargs: can be used to pass path to individual data folders. See brainrender/Utils/paths_manager.py

		"""

        Paths.__init__(self, base_dir=base_dir, **kwargs)

        self.projection_metric = projection_metric

        # get mouse connectivity cache and structure tree
        self.mcc = MouseConnectivityCache(manifest_file=os.path.join(
            self.mouse_connectivity_cache, "manifest.json"))
        self.structure_tree = self.mcc.get_structure_tree()

        # get ontologies API and brain structures sets
        self.oapi = OntologiesApi()
        self.get_structures_sets()

        # get reference space
        self.space = ReferenceSpaceApi()
        self.spacecache = ReferenceSpaceCache(
            manifest=os.path.join(
                self.annotated_volume, "manifest.json"
            ),  # downloaded files are stored relative to here
            resolution=self.resolution,
            reference_space_key=
            "annotation/ccf_2017"  # use the latest version of the CCF
        )
        self.annotated_volume, _ = self.spacecache.get_annotation_volume()

        # mouse connectivity API [used for tractography]
        self.mca = MouseConnectivityApi()

        # Get tree search api
        self.tree_search = TreeSearchApi()

        # Get some metadata about experiments
        self.all_experiments = self.mcc.get_experiments(dataframe=True)
        self.strains = sorted(
            [x for x in set(self.all_experiments.strain) if x is not None])
        self.transgenic_lines = sorted(
            set([
                x for x in set(self.all_experiments.transgenic_line)
                if x is not None
            ]))
Beispiel #6
0
def connectivity():
    nrrd.read = MagicMock(name='nrrd_read_file',
                          return_value=('mock_annotation_data',
                                        'mock_annotation_image'))
    conn_api = MouseConnectivityApi()
    download_link = '/path/to/link'
    conn_api.do_query = MagicMock(return_value=download_link)

    conn_api.json_msg_query = MagicMock(name='json_msg_query')
    conn_api.retrieve_file_over_http = \
        MagicMock(name='retrieve_file_over_http')

    return conn_api
def connectivity():
    nrrd.read = MagicMock(name='nrrd_read_file',
                          return_value=('mock_annotation_data',
                                        'mock_annotation_image'))
    conn_api = MouseConnectivityApi()
    download_link = '/path/to/link'
    conn_api.do_query = MagicMock(return_value=download_link)

    conn_api.json_msg_query = MagicMock(name='json_msg_query')
    conn_api.retrieve_file_over_http = \
        MagicMock(name='retrieve_file_over_http')

    return conn_api
Beispiel #8
0
def allen_get_raw_annotation(annotation_dir,
                             version='annotation/ccf_2017',
                             resolution=10):
    import nrrd
    from allensdk.api.queries.mouse_connectivity_api import MouseConnectivityApi
    annotation_path = pjoin(
        annotation_dir, 'annotation_{0}_{1}.nrrd'.format(version, resolution))
    if not os.path.isdir(annotation_dir):
        os.makedirs(annotation_dir)
    if not os.path.isfile(annotation_path):
        mcapi = MouseConnectivityApi()
        mcapi.download_annotation_volume(version, resolution, annotation_path)
    annotation, meta = nrrd.read(annotation_path)
    return annotation, meta
Beispiel #9
0
def test_compute_centroid():
    # ------------------------------------------------------------------------
    # tests computed centroid is same as MouseConnApi with 'true' inj density
    # pull 'data' from mcc fixture
    a = np.random.rand(4, 4, 4)
    b = np.random.rand(4, 4, 4)

    # compute allensdk centroid
    api = MouseConnectivityApi()
    mcc_centroid = api.calculate_injection_centroid(a, b, 1)

    # 'true' injection density
    _compute_true_injection_density(a, b, inplace=True)

    assert_array_almost_equal(compute_centroid(a), mcc_centroid)
    def __init__(self,
                 resolution=None,
                 cache=True,
                 manifest_file=None,
                 ccf_version=None,
                 base_uri=None,
                 version=None):

        if manifest_file is None:
            manifest_file = get_default_manifest_file('mouse_connectivity')

        if version is None:
            version = self.MANIFEST_VERSION

        if resolution is None:
            resolution = MouseConnectivityApi.VOXEL_RESOLUTION_25_MICRONS

        if ccf_version is None:
            ccf_version = MouseConnectivityApi.CCF_VERSION_DEFAULT

        super(MouseConnectivityCache, self).__init__(
            resolution, reference_space_key=ccf_version, cache=cache,
            manifest=manifest_file, version=version)

        self.api = MouseConnectivityApi(base_uri=base_uri)
    def __init__(self,
                 resolution=None,
                 cache=True,
                 manifest_file='mouse_connectivity_manifest.json',
                 ccf_version=None,
                 base_uri=None):
        super(MouseConnectivityCache, self).__init__(manifest=manifest_file,
                                                     cache=cache)

        if resolution is None:
            self.resolution = MouseConnectivityApi.VOXEL_RESOLUTION_25_MICRONS
        else:
            self.resolution = resolution
        self.api = MouseConnectivityApi(base_uri=base_uri)

        if ccf_version is None:
            ccf_version = MouseConnectivityApi.CCF_VERSION_DEFAULT
        self.ccf_version = ccf_version
Beispiel #12
0
    def __init__(self, projection_metric="projection_energy", paths_file=None):
        """ path_file {[str]} -- [Path to a YAML file specifying paths to data folders, to replace default paths] (default: {None}) """

        Paths.__init__(self, paths_file=paths_file)

        self.projection_metric = projection_metric

        # get mouse connectivity cache and structure tree
        self.mcc = MouseConnectivityCache(manifest_file=os.path.join(
            self.mouse_connectivity_cache, "manifest.json"))
        self.structure_tree = self.mcc.get_structure_tree()

        # get ontologies API and brain structures sets
        self.oapi = OntologiesApi()
        self.get_structures_sets()

        # get reference space
        self.space = ReferenceSpaceApi()
        self.spacecache = ReferenceSpaceCache(
            manifest=os.path.join(
                "Data/ABA", "manifest.json"
            ),  # downloaded files are stored relative to here
            resolution=self.resolution,
            reference_space_key=
            "annotation/ccf_2017"  # use the latest version of the CCF
        )
        self.annotated_volume, _ = self.spacecache.get_annotation_volume()

        # mouse connectivity API [used for tractography]
        self.mca = MouseConnectivityApi()

        # Get tree search api
        self.tree_search = TreeSearchApi()

        # Get some metadata about experiments
        self.all_experiments = self.mcc.get_experiments(dataframe=True)
        self.strains = sorted(
            [x for x in set(self.all_experiments.strain) if x is not None])
        self.transgenic_lines = sorted(
            set([
                x for x in set(self.all_experiments.transgenic_line)
                if x is not None
            ]))
Beispiel #13
0
    def __init__(self,  base_dir=None, **kwargs):
        """ 
        Set up file paths and Allen SDKs
        
        :param base_dir: path to directory to use for saving data (default value None)
        :param kwargs: can be used to pass path to individual data folders. See brainrender/Utils/paths_manager.py

        """

        Atlas.__init__(self, base_dir=base_dir, **kwargs)
        self.meshes_folder = self.mouse_meshes # where the .obj mesh for each region is saved

        # get mouse connectivity cache and structure tree
        self.mcc = MouseConnectivityCache(manifest_file=os.path.join(self.mouse_connectivity_cache, "manifest.json"))
        self.structure_tree = self.mcc.get_structure_tree()
        
        # get ontologies API and brain structures sets
        self.oapi = OntologiesApi()
        self.get_structures_sets()

        # get reference space
        self.space = ReferenceSpaceApi()
        self.spacecache = ReferenceSpaceCache(
            manifest=os.path.join(self.annotated_volume_fld, "manifest.json"),  # downloaded files are stored relative to here
            resolution=self.resolution,
            reference_space_key="annotation/ccf_2017"  # use the latest version of the CCF
            )
        self.annotated_volume, _ = self.spacecache.get_annotation_volume()

        # mouse connectivity API [used for tractography]
        self.mca = MouseConnectivityApi()

        # Get tree search api
        self.tree_search = TreeSearchApi()

        # Store all regions metadata [If there's internet connection]
        if self.other_sets is not None: 
            self.regions = self.other_sets["Structures whose surfaces are represented by a precomputed mesh"].sort_values('acronym')
            self.region_acronyms = list(self.other_sets["Structures whose surfaces are represented by a precomputed mesh"].sort_values(
                                                'acronym').acronym.values)
Beispiel #14
0
 def __init__(self,
              mcc,
              experiment_id,
              output_dir,
              anno,
              meta,
              rsp,
              logger,
              zoom=8,
              remove_transform_data=True):
     self.remove_transform_data = remove_transform_data
     self.output_dir = output_dir
     os.makedirs(output_dir, exist_ok=True)
     self.mcc = mcc
     self.mapi = MouseConnectivityApi()
     self.anno, self.meta = anno, meta
     self.rsp = rsp
     self.zoom = 8 - zoom
     self.id = experiment_id
     assert zoom >= 0
     self.details = self.mapi.get_experiment_detail(self.id)
     image_resolution = self.details[0]['sub_images'][0]['resolution']
     self.two_d = 1.0 / image_resolution
     self.size = self.mcc.resolution * self.two_d / (2**self.zoom)
     self.dims = (self.details[0]['sub_images'][0]['height'] //
                  (2**self.zoom),
                  self.details[0]['sub_images'][0]['width'] //
                  (2**self.zoom))
     self.root_points = np.array(np.where(self.anno != 0)).T
     self.logger = logger
     self.logger.info(
         f"Initializing displacement transform data for {self.id}...")
     self.__init_transform__()
     self.logger.info(
         f"Performing displacement transformation for {self.id}...")
     self.__init_transformed_points__()
 def __init__(self, mcc, experiment_id, directory, brain_seg_data_dir, parent_struct_id,
              experiment_fields_to_save, details, logger, default_struct_id=997):
     self.experiment_fields_to_save = experiment_fields_to_save
     self.default_struct_id = default_struct_id
     self.parent_struct_id = parent_struct_id
     self.brain_seg_data_dir = brain_seg_data_dir
     self.directory = directory
     self.mcc = mcc
     self.id = experiment_id
     mapi = MouseConnectivityApi()
     while True:
         try:
             self.details = {**details, **(mapi.get_experiment_detail(self.id)[0])}
             break
         except simplejson.errors.JSONDecodeError or urllib.error.URLError or urllib.error.URLError:
             time.sleep(1.0)
     self.logger = logger
     self.subimages = {i['section_number']: i for i in self.details['sub_images']}
     self.seg_data = np.load(f'{self.brain_seg_data_dir}/{self.id}/{self.id}-sections.npz')['arr_0']
     self.structure_tree = self.mcc.get_structure_tree()
     self.structure_ids = self.get_requested_structure_children()
     with open(f'{self.directory}/bboxes.pickle', "rb") as f:
         bboxes = pickle.load(f)
     self.bboxes = {k: v for k, v in bboxes.items() if v}
    def __init__(
        self,
        resolution=None,
        cache=True,
        manifest_file="mouse_connectivity_manifest.json",
        ccf_version=None,
        base_uri=None,
    ):
        super(MouseConnectivityCache, self).__init__(manifest=manifest_file, cache=cache)

        if resolution is None:
            self.resolution = MouseConnectivityApi.VOXEL_RESOLUTION_25_MICRONS
        else:
            self.resolution = resolution
        self.api = MouseConnectivityApi(base_uri=base_uri)

        if ccf_version is None:
            ccf_version = MouseConnectivityApi.CCF_VERSION_DEFAULT
        self.ccf_version = ccf_version
    def __init__(self, resolution=25, cache=True, manifest_file='manifest.json'):
        super(MouseConnectivityCache, self).__init__(manifest=manifest_file, cache=cache)

        self.resolution = resolution
        self.api = MouseConnectivityApi()
Beispiel #18
0
oapi = OntologiesApi()
structure_graph = oapi.get_structures_with_sets([adultMouseStructureGraphID])
# Removes some unused fields returned by the query:
structure_graph = StructureTree.clean_structures(structure_graph)
tree = StructureTree(structure_graph)

# Example:
# tree.get_structures_by_name(['Dorsal auditory area'])
# The annotation download writes a file, so we will need somwhere to put it
annotation_dir = os.path.dirname(structIDSource)
Manifest.safe_mkdir(annotation_dir)
annotation_path = os.path.join(annotation_dir, 'annotation.nrrd')

#-------------------------------------------------------------------------------
# Use the connectivity API:
mcapi = MouseConnectivityApi()
# The name of the latest ccf version (a string):
annotation_version = mcapi.CCF_VERSION_DEFAULT
if not os.path.exists(annotation_path):
    mcapi.download_annotation_volume(annotation_version, resolution,
                                     annotation_path)
annotation, meta = nrrd.read(annotation_path)

# Build a reference space from a StructureTree and annotation volume, the third argument is
# the resolution of the space in microns
rsp = ReferenceSpace(tree, annotation, [resolution, resolution, resolution])

#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# So now we're ready to go through structures, and extract their coordinates
structureID_df = pd.read_csv(structIDSource)
Beispiel #19
0
from allensdk.api.queries.mouse_connectivity_api import MouseConnectivityApi

mca = MouseConnectivityApi()

# get metadata for all non-Cre experiments
experiments = mca.experiment_source_search(injection_structures='root',
                                           transgenic_lines=0)

# download the projection density volume for one of the experiments
pd = mca.download_projection_density('example.nrrd',
                                     experiments[0]['id'],
                                     resolution=25)
Beispiel #20
0
class ABA:
    """
        This class augments the functionality of
        BrainGlobeAtlas with methods specific to the Allen
        Mouse Brain atlas and necessary to populate scenes in 
        brainrender. These include stuff like fetching streamlines
        and neuronal morphology data. 
    """

    atlas_name = "ABA"
    resolution = 25

    excluded_regions = ["fiber tracts"]

    # Used for streamlines
    base_url = "https://neuroinformatics.nl/HBP/allen-connectivity-viewer/json/streamlines_NNN.json.gz"

    def __init__(self):
        # get mouse connectivity cache and structure tree
        self.mcc = MouseConnectivityCache(manifest_file=os.path.join(
            self.mouse_connectivity_cache, "manifest.json"))
        self.structure_tree = self.mcc.get_structure_tree()

        # get ontologies API and brain structures sets
        self.oapi = OntologiesApi()

        # get reference space
        self.space = ReferenceSpaceApi()
        self.spacecache = ReferenceSpaceCache(
            manifest=os.path.join(
                self.annotated_volume_fld, "manifest.json"
            ),  # downloaded files are stored relative to here
            resolution=int(self.resolution[0]),
            reference_space_key=
            "annotation/ccf_2017",  # use the latest version of the CCF
        )
        self.annotated_volume, _ = self.spacecache.get_annotation_volume()

        # mouse connectivity API [used for tractography]
        self.mca = MouseConnectivityApi()

        # Get tree search api
        self.tree_search = TreeSearchApi()

    # ------------------------- Scene population methods ------------------------- #
    def get_neurons(
        self,
        neurons,
        color=None,
        display_axon=True,
        display_dendrites=True,
        alpha=1,
        neurite_radius=None,
        soma_radius=None,
        use_cache=True,
    ):
        """
        Gets rendered morphological data of neurons reconstructions downloaded from the
        Mouse Light project at Janelia (or other sources). 
        Accepts neurons argument as:
            - file(s) with morphological data
            - vedo mesh actor(s) of entire neurons reconstructions
            - dictionary or list of dictionary with actors for different neuron parts

        :param neurons: str, list, dict. File(s) with neurons data or list of rendered neurons.
        :param display_axon, display_dendrites: if set to False the corresponding neurite is not rendered
        :param color: default None. Can be:
                - None: each neuron is given a random color
                - color: rbg, hex etc. If a single color is passed all neurons will have that color
                - cmap: str with name of a colormap: neurons are colored based on their sequential order and cmap
                - dict: a dictionary specifying a color for soma, dendrites and axon actors, will be the same for all neurons
                - list: a list of length = number of neurons with either a single color for each neuron
                        or a dictionary of colors for each neuron
        :param alpha: float in range 0,1. Neurons transparency
        :param neurite_radius: float > 0 , radius of tube actor representing neurites
        :param use_cache: bool, if True a cache is used to avoid having to crate a neuron's mesh anew, otherwise a new mesh is created
        """

        if not isinstance(neurons, (list, tuple)):
            neurons = [neurons]

        # ---------------------------------- Render ---------------------------------- #
        _neurons_actors = []
        for neuron in neurons:
            neuron_actors = {"soma": None, "dendrites": None, "axon": None}

            # Deal with neuron as filepath
            if isinstance(neuron, str):
                if os.path.isfile(neuron):
                    if neuron.endswith(".swc"):
                        neuron_actors, _ = get_neuron_actors_with_morphapi(
                            swcfile=neuron,
                            neurite_radius=neurite_radius,
                            soma_radius=soma_radius,
                            use_cache=use_cache,
                        )
                    else:
                        raise NotImplementedError(
                            "Currently we can only parse morphological reconstructions from swc files"
                        )
                else:
                    raise ValueError(
                        f"Passed neruon {neuron} is not a valid input. Maybe the file doesn't exist?"
                    )

            # Deal with neuron as single actor
            elif isinstance(neuron, Mesh):
                # A single actor was passed, maybe it's the entire neuron
                neuron_actors["soma"] = neuron  # store it as soma
                pass

            # Deal with neuron as dictionary of actor
            elif isinstance(neuron, dict):
                neuron_actors["soma"] = neuron.pop("soma", None)
                neuron_actors["axon"] = neuron.pop("axon", None)

                # Get dendrites actors
                if ("apical_dendrites" in neuron.keys()
                        or "basal_dendrites" in neuron.keys()):
                    if "apical_dendrites" not in neuron.keys():
                        neuron_actors["dendrites"] = neuron["basal_dendrites"]
                    elif "basal_dendrites" not in neuron.keys():
                        neuron_actors["dendrites"] = neuron["apical_dendrites"]
                    else:
                        neuron_actors["dendrites"] = merge(
                            neuron["apical_dendrites"],
                            neuron["basal_dendrites"],
                        )
                else:
                    neuron_actors["dendrites"] = neuron.pop("dendrites", None)

            # Deal with neuron as instance of Neuron from morphapi
            elif isinstance(neuron, Neuron):
                neuron_actors, _ = get_neuron_actors_with_morphapi(
                    neuron=neuron,
                    neurite_radius=neurite_radius,
                    use_cache=use_cache,
                )
            # Deal with other inputs
            else:
                raise ValueError(
                    f"Passed neuron {neuron} is not a valid input")

            # Check that we don't have anything weird in neuron_actors
            for key, act in neuron_actors.items():
                if act is not None:
                    if not isinstance(act, Mesh):
                        raise ValueError(
                            f"Neuron actor {key} is {type(act)} but should be a vedo Mesh. Not: {act}"
                        )

            if not display_axon:
                neuron_actors["axon"] = None
            if not display_dendrites:
                neuron_actors["dendrites"] = None
            _neurons_actors.append(neuron_actors)

        # Color actors
        colors = parse_neurons_colors(neurons, color)
        for n, neuron in enumerate(_neurons_actors):
            if neuron["axon"] is not None:
                neuron["axon"].c(colors["axon"][n])
                neuron["axon"].name = "neuron-axon"
            if neuron["soma"] is not None:
                neuron["soma"].c(colors["soma"][n])
                neuron["soma"].name = "neuron-soma"
            if neuron["dendrites"] is not None:
                neuron["dendrites"].c(colors["dendrites"][n])
                neuron["dendrites"].name = "neuron-dendrites"

        # Return
        return return_list_smart(_neurons_actors), None

    def get_tractography(
        self,
        tractography,
        color=None,
        color_by="manual",
        others_alpha=1,
        verbose=True,
        VIP_regions=[],
        VIP_color=None,
        others_color="white",
        include_all_inj_regions=False,
        display_injection_volume=True,
    ):
        """
        Renders tractography data and adds it to the scene. A subset of tractography data can receive special treatment using the  with VIP regions argument:
        if the injection site for the tractography data is in a VIP regions, this is colored differently.

        :param tractography: list of dictionaries with tractography data
        :param color: color of rendered tractography data

        :param color_by: str, specifies which criteria to use to color the tractography (Default value = "manual")
                        options:
                            -  manual, define color of each tract
                            - target_region, color by the injected region

        :param others_alpha: float (Default value = 1)
        :param verbose: bool (Default value = True)
        :param VIP_regions: list of brain regions with VIP treatement (Default value = [])
        :param VIP_color: str, color to use for VIP data (Default value = None)
        :param others_color: str, color for not VIP data (Default value = "white")
        :param include_all_inj_regions: bool (Default value = False)
        :param display_injection_volume: float, if True a spehere is added to display the injection coordinates and volume (Default value = True)
        """

        # check argument
        if not isinstance(tractography, list):
            if isinstance(tractography, dict):
                tractography = [tractography]
            else:
                raise ValueError(
                    "the 'tractography' variable passed must be a list of dictionaries"
                )
        else:
            if not isinstance(tractography[0], dict):
                raise ValueError(
                    "the 'tractography' variable passed must be a list of dictionaries"
                )

        if not isinstance(VIP_regions, list):
            raise ValueError("VIP_regions should be a list of acronyms")

        COLORS = parse_tractography_colors(
            tractography,
            include_all_inj_regions,
            color=color,
            color_by=color_by,
            VIP_regions=VIP_regions,
            VIP_color=VIP_color,
            others_color=others_color,
        )
        COLORS = [
            c if c is not None else self._get_from_structure(
                t["structure-abbrev"], "rgb_triplet")
            for c, t in zip(COLORS, tractography)
        ]

        # add actors to represent tractography data
        actors, structures_acronyms = [], []
        if brainrender.VERBOSE and verbose:
            print("Structures found to be projecting to target: ")

        # Loop over injection experiments
        for i, (t, color) in enumerate(zip(tractography, COLORS)):
            # Use allen metadata
            if include_all_inj_regions:
                inj_structures = [
                    x["abbreviation"] for x in t["injection-structures"]
                ]
            else:
                inj_structures = [
                    self.get_structure_ancestors(t["structure-abbrev"])[-1]
                ]

            if (brainrender.VERBOSE and verbose and not is_any_item_in_list(
                    inj_structures, structures_acronyms)):
                print("     -- ({})".format(t["structure-abbrev"]))
                structures_acronyms.append(t["structure-abbrev"])

            # get tractography points and represent as list
            if color_by == "target_region" and not is_any_item_in_list(
                    inj_structures, VIP_regions):
                alpha = others_alpha
            else:
                alpha = brainrender.TRACTO_ALPHA

            if alpha == 0:
                continue  # skip transparent ones

            # represent injection site as sphere
            if display_injection_volume:
                actors.append(
                    shapes.Sphere(
                        pos=t["injection-coordinates"],
                        c=color,
                        r=brainrender.INJECTION_VOLUME_SIZE *
                        t["injection-volume"],
                        alpha=brainrender.TRACTO_ALPHA,
                    ))
                actors[-1].name = (str(t["injection-coordinates"]) +
                                   "_injection")

            points = [p["coord"] for p in t["path"]]
            actors.append(
                shapes.Tube(
                    points,
                    r=brainrender.TRACTO_RADIUS,
                    c=color,
                    alpha=alpha,
                    res=brainrender.TRACTO_RES,
                ))
            actors[-1].name = str(t["injection-coordinates"]) + "_tractography"

        return actors

    def get_streamlines(self, sl_file, color=None, *args, **kwargs):
        """
        Render streamline data downloaded from https://neuroinformatics.nl/HBP/allen-connectivity-viewer/streamline-downloader.html

        :param sl_file: path to JSON file with streamliens data [or list of files]
        :param color: either a single color or a list of colors to color each streamline individually
        :param *args:
        :param **kwargs:

        """
        if not isinstance(sl_file, (list, tuple)):
            sl_file = [sl_file]

        # get a list of colors of length len(sl_file)
        if color is not None:
            if isinstance(color, (list, tuple)):
                if isinstance(color[0], (float, int)):  # it's an rgb color
                    color = [color for i in sl_file]
                elif len(color) != len(sl_file):
                    raise ValueError(
                        "Wrong number of colors, should be one per streamline or 1"
                    )
            else:
                color = [color for i in sl_file]
        else:
            color = ["salmon" for i in sl_file]

        actors = []
        if isinstance(sl_file[0],
                      (str, pd.DataFrame)):  # we have a list of files to add
            for slf, col in track(
                    zip(sl_file, color),
                    total=len(sl_file),
                    description="parsing streamlines",
            ):
                if isinstance(slf, str):
                    streamlines = parse_streamline(color=col,
                                                   filepath=slf,
                                                   *args,
                                                   **kwargs)
                else:
                    streamlines = parse_streamline(color=col,
                                                   data=slf,
                                                   *args,
                                                   **kwargs)
                actors.extend(streamlines)
        else:
            raise ValueError(
                "unrecognized argument sl_file: {}".format(sl_file))

        return actors

    # ----------------------------------- Utils ---------------------------------- #
    def get_projection_tracts_to_target(self, p0=None, **kwargs):
        """
        Gets tractography data for all experiments whose projections reach the brain region or location of iterest.
        
        :param p0: list of 3 floats with AP-DV-ML coordinates of point to be used as seed (Default value = None)
        :param **kwargs: 
        """

        # check args
        if p0 is None:
            raise ValueError("Please pass coordinates")
        elif isinstance(p0, np.ndarray):
            p0 = list(p0)
        elif not isinstance(p0, (list, tuple)):
            raise ValueError("Invalid argument passed (p0): {}".format(p0))

        p0 = [np.int(p) for p in p0]
        tract = self.mca.experiment_spatial_search(seed_point=p0, **kwargs)

        if isinstance(tract, str):
            raise ValueError(
                "Something went wrong with query, query error message:\n{}".
                format(tract))
        else:
            return tract

    def download_streamlines_for_region(self, region, *args, **kwargs):
        """
            Using the Allen Mouse Connectivity data and corresponding API, this function finds expeirments whose injections
            were targeted to the region of interest and downloads the corresponding streamlines data. By default, experiements
            are selected for only WT mice and onl when the region was the primary injection target. Look at "ABA.experiments_source_search"
            to see how to change this behaviour.

            :param region: str with region to use for research
            :param *args: arguments for ABA.experiments_source_search
            :param **kwargs: arguments for ABA.experiments_source_search

        """
        # Get experiments whose injections were targeted to the region
        region_experiments = experiments_source_search(self.mca, region, *args,
                                                       **kwargs)
        try:
            return download_streamlines(
                region_experiments.id.values,
                streamlines_folder=self.streamlines_cache,
            )
        except:
            print(f"Could not download streamlines for region {region}")
            return [], []  # <- there were no experiments in the target region

    def download_streamlines_to_region(self,
                                       p0,
                                       *args,
                                       mouse_line="wt",
                                       **kwargs):
        """
            Using the Allen Mouse Connectivity data and corresponding API, this function finds injection experiments
            which resulted in fluorescence being found in the target point, then downloads the streamlines data.

            :param p0: list of floats with AP-DV-ML coordinates
            :param mouse_line: str with name of the mouse line to use(Default value = "wt")
            :param *args: 
            :param **kwargs: 

        """
        experiments = pd.DataFrame(self.get_projection_tracts_to_target(p0=p0))
        if mouse_line == "wt":
            experiments = experiments.loc[experiments["transgenic-line"] == ""]
        else:
            if not isinstance(mouse_line, list):
                experiments = experiments.loc[experiments["transgenic-line"] ==
                                              mouse_line]
            else:
                raise NotImplementedError(
                    "ops, you've found a bug!. For now you can only pass one mouse line at the time, sorry."
                )
        return download_streamlines(experiments.id.values,
                                    streamlines_folder=self.streamlines_cache)
Beispiel #21
0
class MouseConnectivityCache(ReferenceSpaceCache):
    """
    Cache class for storing and accessing data related to the adult mouse
    Connectivity Atlas.  By default, this class will cache any downloaded
    metadata or files in well known locations defined in a manifest file.
    This behavior can be disabled.

    Attributes
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    api: MouseConnectivityApi instance
        Used internally to make API queries.

    Parameters
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    ccf_version: string
        Desired version of the Common Coordinate Framework.  This affects the annotation 
        volume (get_annotation_volume) and structure masks (get_structure_mask). 
        Must be one of (MouseConnectivityApi.CCF_2015, MouseConnectivityApi.CCF_2016). 
        Default: MouseConnectivityApi.CCF_2016

    cache: boolean
        Whether the class should save results of API queries to locations specified
        in the manifest file.  Queries for files (as opposed to metadata) must have a
        file location.  If caching is disabled, those locations must be specified
        in the function call (e.g. get_projection_density(file_name='file.nrrd')).

    manifest_file: string
        File name of the manifest to be read.  Default is "mouse_connectivity_manifest.json".

    """

    PROJECTION_DENSITY_KEY = 'PROJECTION_DENSITY'
    INJECTION_DENSITY_KEY = 'INJECTION_DENSITY'
    INJECTION_FRACTION_KEY = 'INJECTION_FRACTION'
    DATA_MASK_KEY = 'DATA_MASK'
    STRUCTURE_UNIONIZES_KEY = 'STRUCTURE_UNIONIZES'
    EXPERIMENTS_KEY = 'EXPERIMENTS'

    MANIFEST_VERSION = 1.2

    SUMMARY_STRUCTURE_SET_ID = 167587189
    DEFAULT_STRUCTURE_SET_IDS = tuple([SUMMARY_STRUCTURE_SET_ID])

    @property
    def default_structure_ids(self):

        if not hasattr(self, '_default_structure_ids'):
            tree = self.get_structure_tree()
            default_structures = tree.get_structures_by_set_id(
                MouseConnectivityCache.DEFAULT_STRUCTURE_SET_IDS)
            self._default_structure_ids = [
                st['id'] for st in default_structures
            ]

        return self._default_structure_ids

    def __init__(self,
                 resolution=None,
                 cache=True,
                 manifest_file='mouse_connectivity_manifest.json',
                 ccf_version=None,
                 base_uri=None,
                 version=None):

        if version is None:
            version = self.MANIFEST_VERSION

        if resolution is None:
            resolution = MouseConnectivityApi.VOXEL_RESOLUTION_25_MICRONS

        if ccf_version is None:
            ccf_version = MouseConnectivityApi.CCF_VERSION_DEFAULT

        super(MouseConnectivityCache,
              self).__init__(resolution,
                             reference_space_key=ccf_version,
                             cache=cache,
                             manifest=manifest_file,
                             version=version)

        self.api = MouseConnectivityApi(base_uri=base_uri)

    def get_projection_density(self, experiment_id, file_name=None):
        """
        Read a projection density volume for a single experiment.  Download it
        first if it doesn't exist.  Projection density is the proportion of
        of projecting pixels in a grid voxel in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.PROJECTION_DENSITY_KEY,
                                        experiment_id, self.resolution)

        self.api.download_projection_density(file_name,
                                             experiment_id,
                                             self.resolution,
                                             strategy='lazy')

        return nrrd.read(file_name)

    def get_injection_density(self, experiment_id, file_name=None):
        """
        Read an injection density volume for a single experiment. Download it
        first if it doesn't exist.  Injection density is the proportion of
        projecting pixels in a grid voxel only including pixels that are
        part of the injection site in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.INJECTION_DENSITY_KEY,
                                        experiment_id, self.resolution)
        self.api.download_injection_density(file_name,
                                            experiment_id,
                                            self.resolution,
                                            strategy='lazy')

        return nrrd.read(file_name)

    def get_injection_fraction(self, experiment_id, file_name=None):
        """
        Read an injection fraction volume for a single experiment. Download it
        first if it doesn't exist.  Injection fraction is the proportion of
        pixels in the injection site in a grid voxel in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.INJECTION_FRACTION_KEY,
                                        experiment_id, self.resolution)
        self.api.download_injection_fraction(file_name,
                                             experiment_id,
                                             self.resolution,
                                             strategy='lazy')

        return nrrd.read(file_name)

    def get_data_mask(self, experiment_id, file_name=None):
        """
        Read a data mask volume for a single experiment. Download it
        first if it doesn't exist.  Data mask is a binary mask of
        voxels that have valid data.  Only use valid data in analysis!

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.DATA_MASK_KEY,
                                        experiment_id, self.resolution)
        self.api.download_data_mask(file_name,
                                    experiment_id,
                                    self.resolution,
                                    strategy='lazy')

        return nrrd.read(file_name)

    def get_experiments(self,
                        dataframe=False,
                        file_name=None,
                        cre=None,
                        injection_structure_ids=None):
        """
        Read a list of experiments that match certain criteria.  If caching is enabled,
        this will save the whole (unfiltered) list of experiments to a file.

        Parameters
        ----------

        dataframe: boolean
            Return the list of experiments as a Pandas DataFrame.  If False,
            return a list of dictionaries.  Default False.

        file_name: string
            File name to save/read the structures table.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.

        """

        file_name = self.get_cache_path(file_name, self.EXPERIMENTS_KEY)

        if os.path.exists(file_name):
            experiments = json_utilities.read(file_name)
        else:
            experiments = self.api.experiment_source_search(
                injection_structures='root')

            # removing these elements because they are specific to a particular
            # resolution
            for e in experiments:
                del e['num-voxels']
                del e['injection-volume']
                del e['sum']
                del e['name']

            if self.cache:
                Manifest.safe_make_parent_dirs(file_name)

                json_utilities.write(file_name, experiments)

        # filter the read/downloaded list of experiments
        experiments = self.filter_experiments(experiments, cre,
                                              injection_structure_ids)

        if dataframe:
            experiments = pd.DataFrame(experiments)
            experiments.set_index(['id'], inplace=True, drop=False)

        return experiments

    def filter_experiments(self,
                           experiments,
                           cre=None,
                           injection_structure_ids=None):
        """
        Take a list of experiments and filter them by cre status and injection structure.

        Parameters
        ----------

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.
        """

        if cre is True:
            experiments = [e for e in experiments if e['transgenic-line']]
        elif cre is False:
            experiments = [e for e in experiments if not e['transgenic-line']]
        elif cre is not None:
            cre = [c.lower() for c in cre]
            experiments = [
                e for e in experiments if e['transgenic-line'].lower() in cre
            ]

        if injection_structure_ids is not None:
            structure_ids = MouseConnectivityCache.validate_structure_ids(
                injection_structure_ids)

            descendant_ids = reduce(op.add, self.get_structure_tree()\
                                    .descendant_ids(injection_structure_ids))
            experiments = [
                e for e in experiments if e['structure-id'] in descendant_ids
            ]

        return experiments

    def get_experiment_structure_unionizes(self,
                                           experiment_id,
                                           file_name=None,
                                           is_injection=None,
                                           structure_ids=None,
                                           include_descendants=False,
                                           hemisphere_ids=None):
        """
        Retrieve the structure unionize data for a specific experiment.  Filter by
        structure, injection status, and hemisphere.

        Parameters
        ----------

        experiment_id: int
            ID of the experiment of interest.  Corresponds to section_data_set_id in the API.

        file_name: string
            File name to save/read the experiments list.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.

        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a specific set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.

        """

        file_name = self.get_cache_path(file_name,
                                        self.STRUCTURE_UNIONIZES_KEY,
                                        experiment_id)

        filter_fn = functools.partial(self.filter_structure_unionizes,
                                      is_injection=is_injection,
                                      structure_ids=structure_ids,
                                      include_descendants=include_descendants,
                                      hemisphere_ids=hemisphere_ids)

        col_rn = lambda x: pd.DataFrame(x).rename(
            columns={'section_data_set_id': 'experiment_id'})

        return self.api.get_structure_unionizes(
            [experiment_id],
            path=file_name,
            strategy='lazy',
            pre=col_rn,
            post=filter_fn,
            writer=lambda p, x: pd.DataFrame(x).to_csv(p),
            reader=pd.DataFrame.from_csv)

    def rank_structures(self,
                        experiment_ids,
                        is_injection,
                        structure_ids=None,
                        hemisphere_ids=None,
                        rank_on='normalized_projection_volume',
                        n=5,
                        threshold=10**-2):
        '''Produces one or more (per experiment) ranked lists of brain structures, using a specified data field.

        Parameters
        ----------
        experiment_ids : list of int
            Obtain injection_structures for these experiments.
        is_injection : boolean
            Use data from only injection (or non-injection) unionizes.
        structure_ids : list of int, optional
            Consider only these structures. It is a good idea to make sure that these structures are not spatially 
            overlapping; otherwise your results will contain redundant information. Defaults to the summary 
            structures - a brain-wide list of nonoverlapping mid-level structures.
        hemisphere_ids : list of int, optional
            Consider only these hemispheres (1: left, 2: right, 3: both). Like with structures, 
            you might get redundant results if you select overlapping options. Defaults to [1, 2].
        rank_on : str, optional
            Rank unionize data using this field (descending). Defaults to normalized_projection_volume.
        n : int, optional
            Return only the top n structures.
        threshold : float, optional
            Consider only records whose data value - specified by the rank_on parameter - exceeds this value.

        Returns
        -------
        list : 
            Each element (1 for each input experiment) is a list of dictionaries. The dictionaries describe the top
            injection structures in descending order. They are specified by their structure and hemisphere id fields and 
            additionally report the value specified by the rank_on parameter.

        '''

        output_keys = [
            'experiment_id', rank_on, 'hemisphere_id', 'structure_id'
        ]
        filter_fields = lambda fieldname: fieldname in output_keys

        if hemisphere_ids is None:
            hemisphere_ids = [1, 2]
        if structure_ids is None:
            structure_ids = self.default_structure_ids

        unionizes = self.get_structure_unionizes(experiment_ids,
                                                 is_injection=is_injection,
                                                 structure_ids=structure_ids,
                                                 hemisphere_ids=hemisphere_ids,
                                                 include_descendants=False)
        unionizes = unionizes[unionizes[rank_on] > threshold]

        results = []
        for eid in experiment_ids:

            this_experiment_unionizes = unionizes[unionizes['experiment_id'] ==
                                                  eid]
            this_experiment_unionizes = this_experiment_unionizes.sort_values(
                by=rank_on, ascending=False)
            this_experiment_unionizes = this_experiment_unionizes.select(
                filter_fields, axis=1)

            records = this_experiment_unionizes.to_dict('record')
            if len(records) > n:
                records = records[:n]
            results.append(records)

        return results

    def filter_structure_unionizes(self,
                                   unionizes,
                                   is_injection=None,
                                   structure_ids=None,
                                   include_descendants=False,
                                   hemisphere_ids=None):
        """
        Take a list of unionzes and return a subset of records filtered by injection status, structure, and
        hemisphere.

        Parameters
        ----------
        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.
        """
        if is_injection is not None:
            unionizes = unionizes[unionizes.is_injection == is_injection]

        if structure_ids is not None:
            structure_ids = MouseConnectivityCache.validate_structure_ids(
                structure_ids)

            if include_descendants:
                structure_ids = reduce(
                    op.add,
                    self.get_structure_tree().descendant_ids(structure_ids))
            else:
                structure_ids = set(structure_ids)

            unionizes = unionizes[unionizes['structure_id'].isin(
                structure_ids)]

        if hemisphere_ids is not None:
            unionizes = unionizes[unionizes['hemisphere_id'].isin(
                hemisphere_ids)]

        return unionizes

    def get_structure_unionizes(self,
                                experiment_ids,
                                is_injection=None,
                                structure_ids=None,
                                include_descendants=False,
                                hemisphere_ids=None):
        """
        Get structure unionizes for a set of experiment IDs.  Filter the results by injection status,
        structure, and hemisphere.

        Parameters
        ----------
        experiment_ids: list
            List of experiment IDs.  Corresponds to section_data_set_id in the API.

        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a specific set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.
        """

        unionizes = [
            self.get_experiment_structure_unionizes(
                eid,
                is_injection=is_injection,
                structure_ids=structure_ids,
                include_descendants=include_descendants,
                hemisphere_ids=hemisphere_ids) for eid in experiment_ids
        ]

        return pd.concat(unionizes, ignore_index=True)

    def get_projection_matrix(self,
                              experiment_ids,
                              projection_structure_ids=None,
                              hemisphere_ids=None,
                              parameter='projection_volume',
                              dataframe=False):

        if projection_structure_ids is None:
            projection_structure_ids = self.default_structure_ids

        unionizes = self.get_structure_unionizes(
            experiment_ids,
            is_injection=False,
            structure_ids=projection_structure_ids,
            include_descendants=False,
            hemisphere_ids=hemisphere_ids)

        hemisphere_ids = set(unionizes['hemisphere_id'].values.tolist())

        nrows = len(experiment_ids)
        ncolumns = len(projection_structure_ids) * len(hemisphere_ids)

        matrix = np.empty((nrows, ncolumns))
        matrix[:] = np.NAN

        row_lookup = {}
        for idx, e in enumerate(experiment_ids):
            row_lookup[e] = idx

        column_lookup = {}
        columns = []

        cidx = 0
        hlabel = {1: '-L', 2: '-R', 3: ''}

        acronym_map = self.get_structure_tree().value_map(
            lambda x: x['id'], lambda x: x['acronym'])

        for hid in hemisphere_ids:
            for sid in projection_structure_ids:
                column_lookup[(hid, sid)] = cidx
                label = acronym_map[sid] + hlabel[hid]
                columns.append({
                    'hemisphere_id': hid,
                    'structure_id': sid,
                    'label': label
                })
                cidx += 1

        for _, row in unionizes.iterrows():
            ridx = row_lookup[row['experiment_id']]
            k = (row['hemisphere_id'], row['structure_id'])
            cidx = column_lookup[k]
            matrix[ridx, cidx] = row[parameter]

        if dataframe:
            warnings.warn("dataframe argument is deprecated.")
            all_experiments = self.get_experiments(dataframe=True)

            rows_df = all_experiments.loc[experiment_ids]

            cols_df = pd.DataFrame(columns)

            return {'matrix': matrix, 'rows': rows_df, 'columns': cols_df}
        else:
            return {
                'matrix': matrix,
                'rows': experiment_ids,
                'columns': columns
            }

    def add_manifest_paths(self, manifest_builder):
        """
        Construct a manifest for this Cache class and save it in a file.

        Parameters
        ----------

        file_name: string
            File location to save the manifest.

        """

        manifest_builder = super(MouseConnectivityCache,
                                 self).add_manifest_paths(manifest_builder)

        manifest_builder.add_path(self.EXPERIMENTS_KEY,
                                  'experiments.json',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.STRUCTURE_UNIONIZES_KEY,
                                  'experiment_%d/structure_unionizes.csv',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.INJECTION_DENSITY_KEY,
                                  'experiment_%d/injection_density_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.INJECTION_FRACTION_KEY,
                                  'experiment_%d/injection_fraction_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.DATA_MASK_KEY,
                                  'experiment_%d/data_mask_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.PROJECTION_DENSITY_KEY,
                                  'experiment_%d/projection_density_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        return manifest_builder
Beispiel #22
0
import pandas as pd
from rich.progress import track
from rich import print
from loguru import logger
from myterial import orange

try:
    from allensdk.api.queries.mouse_connectivity_api import (
        MouseConnectivityApi, )

    mca = MouseConnectivityApi()
    allen_sdk_installed = True
except ModuleNotFoundError:  # pragma: no cover
    allen_sdk_installed = False  # pragma: no cover

from brainrender._utils import listify
from brainrender._io import request
from brainrender import base_dir

streamlines_folder = base_dir / "streamlines"
streamlines_folder.mkdir(exist_ok=True)


def experiments_source_search(SOI):
    """
    Returns data about experiments whose injection was in the SOI, structure of interest
    :param SOI: str, structure of interest. Acronym of structure to use as seed for teh search
    :param source:  (Default value = True)
    """

    transgenic_id = 0  # id = 0 means use only wild type
Beispiel #23
0
class ABA:
    """
        This class augments the functionality of
        BrainGlobeAtlas with methods specific to the Allen
        Mouse Brain atlas and necessary to populate scenes in 
        brainrender. These include stuff like fetching streamlines
        and neuronal morphology data. 
    """

    atlas_name = "ABA"

    excluded_regions = ["fiber tracts"]

    # Used for streamlines
    base_url = "https://neuroinformatics.nl/HBP/allen-connectivity-viewer/json/streamlines_NNN.json.gz"

    def __init__(self):
        # mouse connectivity API [used for tractography]
        if allen_sdk_installed:
            self.mca = MouseConnectivityApi()
        else:
            self.mca = None

    # ------------------------- Scene population methods ------------------------- #

    def get_tractography(
        self,
        tractography,
        color=None,
        color_by="manual",
        others_alpha=1,
        verbose=True,
        VIP_regions=[],
        VIP_color=None,
        others_color="white",
        include_all_inj_regions=False,
        display_injection_volume=True,
    ):
        """
        Renders tractography data and adds it to the scene. A subset of tractography data can receive special treatment using the  with VIP regions argument:
        if the injection site for the tractography data is in a VIP regions, this is colored differently.

        :param tractography: list of dictionaries with tractography data
        :param color: color of rendered tractography data

        :param color_by: str, specifies which criteria to use to color the tractography (Default value = "manual")
                        options:
                            -  manual, define color of each tract
                            - target_region, color by the injected region

        :param others_alpha: float (Default value = 1)
        :param verbose: bool (Default value = True)
        :param VIP_regions: list of brain regions with VIP treatement (Default value = [])
        :param VIP_color: str, color to use for VIP data (Default value = None)
        :param others_color: str, color for not VIP data (Default value = "white")
        :param include_all_inj_regions: bool (Default value = False)
        :param display_injection_volume: float, if True a spehere is added to display the injection coordinates and volume (Default value = True)
        """

        # check argument
        if not isinstance(tractography, list):
            if isinstance(tractography, dict):
                tractography = [tractography]
            else:
                raise ValueError(
                    "the 'tractography' variable passed must be a list of dictionaries"
                )
        else:
            if not isinstance(tractography[0], dict):
                raise ValueError(
                    "the 'tractography' variable passed must be a list of dictionaries"
                )

        if not isinstance(VIP_regions, list):
            raise ValueError("VIP_regions should be a list of acronyms")

        COLORS = parse_tractography_colors(
            tractography,
            include_all_inj_regions,
            color=color,
            color_by=color_by,
            VIP_regions=VIP_regions,
            VIP_color=VIP_color,
            others_color=others_color,
        )
        COLORS = [
            c if c is not None else self._get_from_structure(
                t["structure-abbrev"], "rgb_triplet")
            for c, t in zip(COLORS, tractography)
        ]

        # add actors to represent tractography data
        actors, structures_acronyms = [], []
        if brainrender.VERBOSE and verbose:
            print("Structures found to be projecting to target: ")

        # Loop over injection experiments
        for i, (t, color) in enumerate(zip(tractography, COLORS)):
            # Use allen metadata
            if include_all_inj_regions:
                inj_structures = [
                    x["abbreviation"] for x in t["injection-structures"]
                ]
            else:
                inj_structures = [
                    self.get_structure_ancestors(t["structure-abbrev"])[-1]
                ]

            if (brainrender.VERBOSE and verbose and not is_any_item_in_list(
                    inj_structures, structures_acronyms)):
                print("     -- ({})".format(t["structure-abbrev"]))
                structures_acronyms.append(t["structure-abbrev"])

            # get tractography points and represent as list
            if color_by == "target_region" and not is_any_item_in_list(
                    inj_structures, VIP_regions):
                alpha = others_alpha
            else:
                alpha = brainrender.TRACTO_ALPHA

            if alpha == 0:
                continue  # skip transparent ones

            # represent injection site as sphere
            if display_injection_volume:
                actors.append(
                    shapes.Sphere(
                        pos=t["injection-coordinates"],
                        c=color,
                        r=brainrender.INJECTION_VOLUME_SIZE *
                        t["injection-volume"],
                        alpha=brainrender.TRACTO_ALPHA,
                    ))
                actors[-1].name = (str(t["injection-coordinates"]) +
                                   "_injection")

            points = [p["coord"] for p in t["path"]]
            actors.append(
                shapes.Tube(
                    points,
                    r=brainrender.TRACTO_RADIUS,
                    c=color,
                    alpha=alpha,
                    res=brainrender.TRACTO_RES,
                ))
            actors[-1].name = str(t["injection-coordinates"]) + "_tractography"

        return actors

    def get_streamlines(self, sl_file, color=None, *args, **kwargs):
        """
        Render streamline data downloaded from https://neuroinformatics.nl/HBP/allen-connectivity-viewer/streamline-downloader.html

        :param sl_file: path to JSON file with streamliens data [or list of files]
        :param color: either a single color or a list of colors to color each streamline individually
        :param *args:
        :param **kwargs:

        """
        if not isinstance(sl_file, (list, tuple)):
            sl_file = [sl_file]

        # get a list of colors of length len(sl_file)
        if color is not None:
            if isinstance(color, (list, tuple)):
                if isinstance(color[0], (float, int)):  # it's an rgb color
                    color = [color for i in sl_file]
                elif len(color) != len(sl_file):
                    raise ValueError(
                        "Wrong number of colors, should be one per streamline or 1"
                    )
            else:
                color = [color for i in sl_file]
        else:
            color = ["salmon" for i in sl_file]

        actors = []
        if isinstance(sl_file[0],
                      (str, pd.DataFrame)):  # we have a list of files to add
            for slf, col in track(
                    zip(sl_file, color),
                    total=len(sl_file),
                    description="parsing streamlines",
            ):
                if isinstance(slf, str):
                    streamlines = parse_streamline(color=col,
                                                   filepath=slf,
                                                   *args,
                                                   **kwargs)
                else:
                    streamlines = parse_streamline(color=col,
                                                   data=slf,
                                                   *args,
                                                   **kwargs)

                actors.extend(streamlines)
        else:
            raise ValueError(
                "unrecognized argument sl_file: {}".format(sl_file))

        return actors

    # ----------------------------------- Utils ---------------------------------- #
    def get_projection_tracts_to_target(self, p0=None, **kwargs):
        """
        Gets tractography data for all experiments whose projections reach the brain region or location of iterest.
        
        :param p0: list of 3 floats with AP-DV-ML coordinates of point to be used as seed (Default value = None)
        :param **kwargs: 
        """

        if self.mca is None:
            raise ModuleNotFoundError(
                'You need allen sdk to use this functino: "pip install allensdk"'
            )

        # check args
        if p0 is None:
            raise ValueError("Please pass coordinates")
        elif isinstance(p0, np.ndarray):
            p0 = list(p0)
        elif not isinstance(p0, (list, tuple)):
            raise ValueError("Invalid argument passed (p0): {}".format(p0))

        p0 = [np.int(p) for p in p0]
        tract = self.mca.experiment_spatial_search(seed_point=p0, **kwargs)

        if isinstance(tract, str):
            raise ValueError(
                "Something went wrong with query, query error message:\n{}".
                format(tract))
        else:
            return tract

    def download_streamlines_for_region(self, region, *args, **kwargs):
        """
            Using the Allen Mouse Connectivity data and corresponding API, this function finds expeirments whose injections
            were targeted to the region of interest and downloads the corresponding streamlines data. By default, experiements
            are selected for only WT mice and onl when the region was the primary injection target. Look at "ABA.experiments_source_search"
            to see how to change this behaviour.

            :param region: str with region to use for research
            :param *args: arguments for ABA.experiments_source_search
            :param **kwargs: arguments for ABA.experiments_source_search

        """
        if self.mca is None:
            raise ModuleNotFoundError(
                'You need allen sdk to use this functino: "pip install allensdk"'
            )

        # Get experiments whose injections were targeted to the region
        region_experiments = experiments_source_search(self.mca, region, *args,
                                                       **kwargs)
        try:
            return download_streamlines(
                region_experiments.id.values,
                streamlines_folder=self.streamlines_cache,
            )
        except:
            print(f"Could not download streamlines for region {region}")
            return [], []  # <- there were no experiments in the target region

    def download_streamlines_to_region(self,
                                       p0,
                                       *args,
                                       mouse_line="wt",
                                       **kwargs):
        """
            Using the Allen Mouse Connectivity data and corresponding API, this function finds injection experiments
            which resulted in fluorescence being found in the target point, then downloads the streamlines data.

            :param p0: list of floats with AP-DV-ML coordinates
            :param mouse_line: str with name of the mouse line to use(Default value = "wt")
            :param *args: 
            :param **kwargs: 

        """
        experiments = pd.DataFrame(self.get_projection_tracts_to_target(p0=p0))
        if mouse_line == "wt":
            experiments = experiments.loc[experiments["transgenic-line"] == ""]
        else:
            if not isinstance(mouse_line, list):
                experiments = experiments.loc[experiments["transgenic-line"] ==
                                              mouse_line]
            else:
                raise NotImplementedError(
                    "ops, you've found a bug!. For now you can only pass one mouse line at the time, sorry."
                )
        return download_streamlines(experiments.id.values,
                                    streamlines_folder=self.streamlines_cache)
Beispiel #24
0
 def __init__(self):
     # mouse connectivity API [used for tractography]
     if allen_sdk_installed:
         self.mca = MouseConnectivityApi()
     else:
         self.mca = None
class MouseConnectivityCache(ReferenceSpaceCache):
    """
    Cache class for storing and accessing data related to the adult mouse
    Connectivity Atlas.  By default, this class will cache any downloaded
    metadata or files in well known locations defined in a manifest file.
    This behavior can be disabled.

    Attributes
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    api: MouseConnectivityApi instance
        Used internally to make API queries.

    Parameters
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    ccf_version: string
        Desired version of the Common Coordinate Framework.  This affects the annotation
        volume (get_annotation_volume) and structure masks (get_structure_mask).
        Must be one of (MouseConnectivityApi.CCF_2015, MouseConnectivityApi.CCF_2016).
        Default: MouseConnectivityApi.CCF_2016

    cache: boolean
        Whether the class should save results of API queries to locations specified
        in the manifest file.  Queries for files (as opposed to metadata) must have a
        file location.  If caching is disabled, those locations must be specified
        in the function call (e.g. get_projection_density(file_name='file.nrrd')).

    manifest_file: string
        File name of the manifest to be read.  Default is "mouse_connectivity_manifest.json".

    """

    PROJECTION_DENSITY_KEY = 'PROJECTION_DENSITY'
    INJECTION_DENSITY_KEY = 'INJECTION_DENSITY'
    INJECTION_FRACTION_KEY = 'INJECTION_FRACTION'
    DATA_MASK_KEY = 'DATA_MASK'
    STRUCTURE_UNIONIZES_KEY = 'STRUCTURE_UNIONIZES'
    EXPERIMENTS_KEY = 'EXPERIMENTS'
    DEFORMATION_FIELD_HEADER_KEY = 'DEFORMATION_FIELD_HEADER'
    DEFORMATION_FIELD_VOXEL_KEY = 'DEFORMATION_FIELD_VOXELS'
    ALIGNMENT3D_KEY = 'ALIGNMENT3D'

    MANIFEST_VERSION = 1.3

    SUMMARY_STRUCTURE_SET_ID = 167587189
    DEFAULT_STRUCTURE_SET_IDS = tuple([SUMMARY_STRUCTURE_SET_ID])

    DFMFLD_RESOLUTIONS = (25,)

    @property
    def default_structure_ids(self):

        if not hasattr(self, '_default_structure_ids'):
            tree = self.get_structure_tree()
            default_structures = tree.get_structures_by_set_id(MouseConnectivityCache.DEFAULT_STRUCTURE_SET_IDS)
            self._default_structure_ids = [st['id'] for st in default_structures]

        return self._default_structure_ids

    def __init__(self,
                 resolution=None,
                 cache=True,
                 manifest_file=None,
                 ccf_version=None,
                 base_uri=None,
                 version=None):

        if manifest_file is None:
            manifest_file = get_default_manifest_file('mouse_connectivity')

        if version is None:
            version = self.MANIFEST_VERSION

        if resolution is None:
            resolution = MouseConnectivityApi.VOXEL_RESOLUTION_25_MICRONS

        if ccf_version is None:
            ccf_version = MouseConnectivityApi.CCF_VERSION_DEFAULT

        super(MouseConnectivityCache, self).__init__(
            resolution, reference_space_key=ccf_version, cache=cache,
            manifest=manifest_file, version=version)

        self.api = MouseConnectivityApi(base_uri=base_uri)


    def get_projection_density(self, experiment_id, file_name=None):
        """
        Read a projection density volume for a single experiment.  Download it
        first if it doesn't exist.  Projection density is the proportion of
        of projecting pixels in a grid voxel in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name,
                                        self.PROJECTION_DENSITY_KEY,
                                        experiment_id,
                                        self.resolution)

        self.api.download_projection_density(
            file_name, experiment_id, self.resolution, strategy='lazy')

        return nrrd.read(file_name)

    def get_injection_density(self, experiment_id, file_name=None):
        """
        Read an injection density volume for a single experiment. Download it
        first if it doesn't exist.  Injection density is the proportion of
        projecting pixels in a grid voxel only including pixels that are
        part of the injection site in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name,
                                        self.INJECTION_DENSITY_KEY,
                                        experiment_id,
                                        self.resolution)
        self.api.download_injection_density(
            file_name, experiment_id, self.resolution, strategy='lazy')

        return nrrd.read(file_name)

    def get_injection_fraction(self, experiment_id, file_name=None):
        """
        Read an injection fraction volume for a single experiment. Download it
        first if it doesn't exist.  Injection fraction is the proportion of
        pixels in the injection site in a grid voxel in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name,
                                        self.INJECTION_FRACTION_KEY,
                                        experiment_id,
                                        self.resolution)
        self.api.download_injection_fraction(
            file_name, experiment_id, self.resolution, strategy='lazy')

        return nrrd.read(file_name)

    def get_data_mask(self, experiment_id, file_name=None):
        """
        Read a data mask volume for a single experiment. Download it
        first if it doesn't exist.  Data mask is a binary mask of
        voxels that have valid data.  Only use valid data in analysis!

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name,
                                        self.DATA_MASK_KEY,
                                        experiment_id,
                                        self.resolution)
        self.api.download_data_mask(
            file_name, experiment_id, self.resolution, strategy='lazy')

        return nrrd.read(file_name)


    def get_experiments(self, dataframe=False, file_name=None, cre=None, injection_structure_ids=None):
        """
        Read a list of experiments that match certain criteria.  If caching is enabled,
        this will save the whole (unfiltered) list of experiments to a file.

        Parameters
        ----------

        dataframe: boolean
            Return the list of experiments as a Pandas DataFrame.  If False,
            return a list of dictionaries.  Default False.

        file_name: string
            File name to save/read the structures table.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.

        """

        file_name = self.get_cache_path(file_name, self.EXPERIMENTS_KEY)

        experiments = self.api.get_experiments_api(path=file_name,
                                                   strategy='lazy',
                                                   **Cache.cache_json())

        for e in experiments:
            # renaming id
            e['id'] = e['data_set_id']
            del e['data_set_id']

            # simplify trangsenic line
            tl = e.get('transgenic_line', None)
            if tl:
                e['transgenic_line'] = tl['name']

            # parse the injection structures
            injs = [ int(i) for i in e['injection_structures'].split('/') ]
            e['injection_structures'] = injs
            e['primary_injection_structure'] = injs[0]

            # remove storage dir
            del e['storage_directory']


        # filter the read/downloaded list of experiments
        experiments = self.filter_experiments(
            experiments, cre, injection_structure_ids)

        if dataframe:
            experiments = pd.DataFrame(experiments)
            experiments.set_index(['id'], inplace=True, drop=False)

        return experiments

    def filter_experiments(self, experiments, cre=None, injection_structure_ids=None):
        """
        Take a list of experiments and filter them by cre status and injection structure.

        Parameters
        ----------

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.
        """

        if cre is True:
            experiments = [e for e in experiments if e['transgenic_line']]
        elif cre is False:
            experiments = [e for e in experiments if not e['transgenic_line']]
        elif cre is not None:
            cre = [ c.lower() for c in cre ]
            experiments = [e for e in experiments if e['transgenic_line'] is not None and e['transgenic_line'].lower() in cre]

        if injection_structure_ids is not None:
            structure_ids = MouseConnectivityCache.validate_structure_ids(injection_structure_ids)
            descendant_ids = set(reduce(op.add, self.get_structure_tree().descendant_ids(injection_structure_ids)))
            
            experiments = [e for e in experiments if e['structure_id'] in descendant_ids]

        return experiments

    def get_experiment_structure_unionizes(self, experiment_id,
                                           file_name=None,
                                           is_injection=None,
                                           structure_ids=None,
                                           include_descendants=False,
                                           hemisphere_ids=None):
        """
        Retrieve the structure unionize data for a specific experiment.  Filter by
        structure, injection status, and hemisphere.

        Parameters
        ----------

        experiment_id: int
            ID of the experiment of interest.  Corresponds to section_data_set_id in the API.

        file_name: string
            File name to save/read the experiments list.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.

        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a specific set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.

        """

        file_name = self.get_cache_path(file_name,
                                        self.STRUCTURE_UNIONIZES_KEY,
                                        experiment_id)

        filter_fn = functools.partial(self.filter_structure_unionizes,
                                      is_injection=is_injection,
                                      structure_ids=structure_ids,
                                      include_descendants=include_descendants,
                                      hemisphere_ids=hemisphere_ids)

        col_rn = lambda x: pd.DataFrame(x).rename(columns={
            'section_data_set_id': 'experiment_id'})

        return self.api.get_structure_unionizes([experiment_id],
                                                path=file_name,
                                                strategy='lazy',
                                                pre=col_rn,
                                                post=filter_fn,
                                                writer=lambda p, x : pd.DataFrame(x).to_csv(p),
                                                reader=lambda x: pd.read_csv(x, index_col=0, parse_dates=True))

    def rank_structures(self, experiment_ids, is_injection, structure_ids=None, hemisphere_ids=None,
                        rank_on='normalized_projection_volume', n=5, threshold=10**-2):
        '''Produces one or more (per experiment) ranked lists of brain structures, using a specified data field.

        Parameters
        ----------
        experiment_ids : list of int
            Obtain injection_structures for these experiments.
        is_injection : boolean
            Use data from only injection (or non-injection) unionizes.
        structure_ids : list of int, optional
            Consider only these structures. It is a good idea to make sure that these structures are not spatially
            overlapping; otherwise your results will contain redundant information. Defaults to the summary
            structures - a brain-wide list of nonoverlapping mid-level structures.
        hemisphere_ids : list of int, optional
            Consider only these hemispheres (1: left, 2: right, 3: both). Like with structures,
            you might get redundant results if you select overlapping options. Defaults to [1, 2].
        rank_on : str, optional
            Rank unionize data using this field (descending). Defaults to normalized_projection_volume.
        n : int, optional
            Return only the top n structures.
        threshold : float, optional
            Consider only records whose data value - specified by the rank_on parameter - exceeds this value.

        Returns
        -------
        list :
            Each element (1 for each input experiment) is a list of dictionaries. The dictionaries describe the top
            injection structures in descending order. They are specified by their structure and hemisphere id fields and
            additionally report the value specified by the rank_on parameter.

        '''

        output_keys = ['experiment_id', rank_on, 'hemisphere_id', 'structure_id']
        filter_fields = lambda fieldname: fieldname in output_keys

        if hemisphere_ids is None:
            hemisphere_ids = [1, 2]
        if structure_ids is None:
            structure_ids = self.default_structure_ids

        unionizes = self.get_structure_unionizes(experiment_ids,
                                                 is_injection=is_injection,
                                                 structure_ids=structure_ids,
                                                 hemisphere_ids=hemisphere_ids,
                                                 include_descendants=False)
        unionizes = unionizes[unionizes[rank_on] > threshold]

        results = []
        for eid in experiment_ids:

            this_experiment_unionizes = unionizes[unionizes['experiment_id'] == eid]
            this_experiment_unionizes = this_experiment_unionizes.sort_values(by=rank_on, ascending=False)
            this_experiment_unionizes = this_experiment_unionizes.select(filter_fields, axis=1)

            records = this_experiment_unionizes.to_dict('record')
            if len(records) > n:
                records = records[:n]
            results.append(records)

        return results

    def filter_structure_unionizes(self, unionizes,
                                   is_injection=None,
                                   structure_ids=None,
                                   include_descendants=False,
                                   hemisphere_ids=None):
        """
        Take a list of unionzes and return a subset of records filtered by injection status, structure, and
        hemisphere.

        Parameters
        ----------
        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.
        """
        if is_injection is not None:
            unionizes = unionizes[unionizes.is_injection == is_injection]

        if structure_ids is not None:
            structure_ids = MouseConnectivityCache.validate_structure_ids(structure_ids)

            if include_descendants:
                structure_ids = reduce(op.add, self.get_structure_tree().descendant_ids(structure_ids))
            else:
                structure_ids = set(structure_ids)


            unionizes = unionizes[
                unionizes['structure_id'].isin(structure_ids)]

        if hemisphere_ids is not None:
            unionizes = unionizes[
                unionizes['hemisphere_id'].isin(hemisphere_ids)]

        return unionizes

    def get_structure_unionizes(self, experiment_ids,
                                is_injection=None,
                                structure_ids=None,
                                include_descendants=False,
                                hemisphere_ids=None):
        """
        Get structure unionizes for a set of experiment IDs.  Filter the results by injection status,
        structure, and hemisphere.

        Parameters
        ----------
        experiment_ids: list
            List of experiment IDs.  Corresponds to section_data_set_id in the API.

        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a specific set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.
        """

        unionizes = [self.get_experiment_structure_unionizes(eid,
                                                             is_injection=is_injection,
                                                             structure_ids=structure_ids,
                                                             include_descendants=include_descendants,
                                                             hemisphere_ids=hemisphere_ids)
                     for eid in experiment_ids]

        return pd.concat(unionizes, ignore_index=True, sort=True)

    def get_projection_matrix(self, experiment_ids,
                              projection_structure_ids=None,
                              hemisphere_ids=None,
                              parameter='projection_volume',
                              dataframe=False):

        if projection_structure_ids is None:
            projection_structure_ids = self.default_structure_ids

        unionizes = self.get_structure_unionizes(experiment_ids,
                                                 is_injection=False,
                                                 structure_ids=projection_structure_ids,
                                                 include_descendants=False,
                                                 hemisphere_ids=hemisphere_ids)

        hemisphere_ids = set(unionizes['hemisphere_id'].values.tolist())

        nrows = len(experiment_ids)
        ncolumns = len(projection_structure_ids) * len(hemisphere_ids)

        matrix = np.empty((nrows, ncolumns))
        matrix[:] = np.NAN

        row_lookup = {}
        for idx, e in enumerate(experiment_ids):
            row_lookup[e] = idx

        column_lookup = {}
        columns = []

        cidx = 0
        hlabel = {1: '-L', 2: '-R', 3: ''}

        acronym_map = self.get_structure_tree().value_map(lambda x: x['id'],
                                                          lambda x: x['acronym'])

        for hid in hemisphere_ids:
            for sid in projection_structure_ids:
                column_lookup[(hid, sid)] = cidx
                label = acronym_map[sid] + hlabel[hid]
                columns.append(
                    {'hemisphere_id': hid, 'structure_id': sid, 'label': label})
                cidx += 1

        for _, row in unionizes.iterrows():
            ridx = row_lookup[row['experiment_id']]
            k = (row['hemisphere_id'], row['structure_id'])
            cidx = column_lookup[k]
            matrix[ridx, cidx] = row[parameter]

        if dataframe:
            warnings.warn("dataframe argument is deprecated.")
            all_experiments = self.get_experiments(dataframe=True)

            rows_df = all_experiments.loc[experiment_ids]

            cols_df = pd.DataFrame(columns)

            return {'matrix': matrix, 'rows': rows_df, 'columns': cols_df}
        else:
            return {'matrix': matrix, 'rows': experiment_ids, 'columns': columns}


    def get_deformation_field(self, section_data_set_id, header_path=None, voxel_path=None):
        ''' Extract the local alignment parameters for this dataset. This a 3D vector image (3 components) describing 
        a deformable local mapping from CCF voxels to this section data set's affine-aligned image stack.

        Parameters
        ----------
            section_data_set_id : int
                Download the deformation field for this data set
            header_path : str, optional
                If supplied, the deformation field header will be downloaded to this path.
            voxel_path : str, optiona
                If supplied, the deformation field voxels will be downloaded to this path.

        Returns
        -------
            numpy.ndarray : 
                3D X 3 component vector array (origin 0, 0, 0; 25-micron isometric resolution) defining a 
                deformable transformation from CCF-space to affine-transformed image space.

        '''

        if self.resolution not in self.DFMFLD_RESOLUTIONS:
            warnings.warn(
                'deformation fields are only available at {} isometric resolutions, but this is a '\
                '{}-micron cache'.format(self.DFMFLD_RESOLUTIONS, self.resolution)
            )

        header_path = self.get_cache_path(header_path, self.DEFORMATION_FIELD_HEADER_KEY, section_data_set_id)
        voxel_path = self.get_cache_path(voxel_path, self.DEFORMATION_FIELD_VOXEL_KEY, section_data_set_id)

        if not (os.path.exists(header_path) and os.path.exists(voxel_path)):
            Manifest.safe_make_parent_dirs(header_path)
            Manifest.safe_make_parent_dirs(voxel_path)
            self.api.download_deformation_field(
                section_data_set_id,
                header_path=header_path,
                voxel_path=voxel_path
                )

        return sitk.GetArrayFromImage(sitk.ReadImage(str(header_path))) # TODO the str call here is only necessary in 2.7


    def get_affine_parameters(self, section_data_set_id, direction='trv', file_name=None):
        ''' Extract the parameters of the 3D affine tranformation mapping this section data set's image-space stack to 
        CCF-space (or vice-versa).

        Parameters
        ----------
        section_data_set_id : int
            download the parameters for this data set.
        direction : str, optional
            Valid options are:
                trv : "transform from reference to volume". Maps CCF points to image space points. If you are 
                    resampling data into CCF, this is the direction you want.
                tvr : "transform from volume to reference". Maps image space points to CCF points.
        file_name : str
            If provided, store the downloaded file here.
 
        Returns
        -------
        alignment : numpy.ndarray
            4 X 3 matrix. In order to transform a point [X_1, X_2, X_3] run 
                np.dot([X_1, X_2, X_3, 1], alignment). In 
            to build a SimpleITK affine transform run:
                transform = sitk.AffineTransform(3)
                transform.SetParameters(alignment.flatten())

        '''

        if not direction in ('trv', 'tvr'):
            raise ArgumentError('invalid direction: {}. direction must be one of tvr, trv'.format(direction))

        file_name = self.get_cache_path(file_name, self.ALIGNMENT3D_KEY)

        raw_alignment = self.api.download_alignment3d(
            strategy='lazy',
            path=file_name,
            section_data_set_id=section_data_set_id,
            **Cache.cache_json())
    
        alignment_re = re.compile('{}_(?P<index>\d+)'.format(direction))
        alignment = np.zeros((4, 3), dtype=float)

        for entry, value in raw_alignment.items():
            match = alignment_re.match(entry)
            if match is not None:
                alignment.flat[int(match.group('index'))] = value
        
        return alignment


    def add_manifest_paths(self, manifest_builder):
        """
        Construct a manifest for this Cache class and save it in a file.

        Parameters
        ----------

        file_name: string
            File location to save the manifest.

        """

        manifest_builder = super(MouseConnectivityCache, self).add_manifest_paths(manifest_builder)

        manifest_builder.add_path(self.EXPERIMENTS_KEY,
                                  'experiments.json',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.STRUCTURE_UNIONIZES_KEY,
                                  'experiment_%d/structure_unionizes.csv',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.INJECTION_DENSITY_KEY,
                                  'experiment_%d/injection_density_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.INJECTION_FRACTION_KEY,
                                  'experiment_%d/injection_fraction_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.DATA_MASK_KEY,
                                  'experiment_%d/data_mask_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.PROJECTION_DENSITY_KEY,
                                  'experiment_%d/projection_density_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.DEFORMATION_FIELD_HEADER_KEY,
                                 'experiment_%d/dfmfld.mhd',
                                 parent_key='BASEDIR',
                                 typename='file')

        manifest_builder.add_path(self.DEFORMATION_FIELD_VOXEL_KEY,
                                 'experiment_%d/dfmfld.raw',
                                 parent_key='BASEDIR',
                                 typename='file')

        manifest_builder.add_path(self.ALIGNMENT3D_KEY,
                                  'experiment_%d/alignment3d.json',
                                  parent_key='BASEDIR',
                                  typename='file')

        return manifest_builder
class MouseConnectivityCache(Cache):
    """
    Cache class for storing and accessing data related to the adult mouse
    Connectivity Atlas.  By default, this class will cache any downloaded 
    metadata or files in well known locations defined in a manifest file.  
    This behavior can be disabled.

    Attributes
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    api: MouseConnectivityApi instance
        Used internally to make API queries.

    Parameters
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    cache: boolean
        Whether the class should save results of API queries to locations specified
        in the manifest file.  Queries for files (as opposed to metadata) must have a
        file location.  If caching is disabled, those locations must be specified
        in the function call (e.g. get_projection_density(file_name='file.nrrd')).

    manifest_file: string
        File name of the manifest to be read.  Default is "mouse_connectivity_manifest.json".
        
    """

    ANNOTATION_KEY = 'ANNOTATION'
    TEMPLATE_KEY = 'TEMPLATE'
    PROJECTION_DENSITY_KEY = 'PROJECTION_DENSITY'
    INJECTION_DENSITY_KEY = 'INJECTION_DENSITY'
    INJECTION_FRACTION_KEY = 'INJECTION_FRACTION'
    DATA_MASK_KEY = 'DATA_MASK'
    STRUCTURE_UNIONIZES_KEY = 'STRUCTURE_UNIONIZES'
    EXPERIMENTS_KEY = 'EXPERIMENTS'
    STRUCTURES_KEY = 'STRUCTURES'
    STRUCTURE_MASK_KEY = 'STRUCTURE_MASK'

    def __init__(self,
                 resolution=25,
                 cache=True,
                 manifest_file='mouse_connectivity_manifest.json',
                 base_uri=None):
        super(MouseConnectivityCache, self).__init__(manifest=manifest_file,
                                                     cache=cache)

        self.resolution = resolution
        self.api = MouseConnectivityApi(base_uri=base_uri)

    def get_annotation_volume(self, file_name=None):
        """ 
        Read the annotation volume.  Download it first if it doesn't exist.

        Parameters
        ----------

        file_name: string
            File name to store the annotation volume.  If it already exists, 
            it will be read from this file.  If file_name is None, the 
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.ANNOTATION_KEY,
                                        self.resolution)

        if file_name is None:
            raise Exception(
                "No save file name provided for annotation volume.")

        if os.path.exists(file_name):
            annotation, info = nrrd.read(file_name)
        else:
            Manifest.safe_mkdir(os.path.dirname(file_name))

            annotation, info = self.api.download_annotation_volume(
                self.resolution, file_name)

        return annotation, info

    def get_template_volume(self, file_name=None):
        """ 
        Read the template volume.  Download it first if it doesn't exist.

        Parameters
        ----------

        file_name: string
            File name to store the template volume.  If it already exists, 
            it will be read from this file.  If file_name is None, the 
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.TEMPLATE_KEY,
                                        self.resolution)

        if file_name is None:
            raise Exception("No save file provided for annotation volume.")

        if os.path.exists(file_name):
            annotation, info = nrrd.read(file_name)
        else:
            Manifest.safe_mkdir(os.path.dirname(file_name))

            annotation, info = self.api.download_template_volume(
                self.resolution, file_name)

        return annotation, info

    def get_projection_density(self, experiment_id, file_name=None):
        """ 
        Read a projection density volume for a single experiment.  Download it 
        first if it doesn't exist.  Projection density is the proportion of 
        of projecting pixels in a grid voxel in [0,1].
        
        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists, 
            it will be read from this file.  If file_name is None, the 
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.PROJECTION_DENSITY_KEY,
                                        experiment_id, self.resolution)

        if file_name is None:
            raise Exception("No file name to save volume.")

        if not os.path.exists(file_name):
            Manifest.safe_mkdir(os.path.dirname(file_name))
            self.api.download_projection_density(file_name, experiment_id,
                                                 self.resolution)

        return nrrd.read(file_name)

    def get_injection_density(self, experiment_id, file_name=None):
        """ 
        Read an injection density volume for a single experiment. Download it 
        first if it doesn't exist.  Injection density is the proportion of
        projecting pixels in a grid voxel only including pixels that are 
        part of the injection site in [0,1].
        
        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists, 
            it will be read from this file.  If file_name is None, the 
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.INJECTION_DENSITY_KEY,
                                        experiment_id, self.resolution)

        if file_name is None:
            raise Exception("No file name to save volume.")

        if not os.path.exists(file_name):
            Manifest.safe_mkdir(os.path.dirname(file_name))

            self.api.download_injection_density(file_name, experiment_id,
                                                self.resolution)

        return nrrd.read(file_name)

    def get_injection_fraction(self, experiment_id, file_name=None):
        """ 
        Read an injection fraction volume for a single experiment. Download it 
        first if it doesn't exist.  Injection fraction is the proportion of
        pixels in the injection site in a grid voxel in [0,1].
        
        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists, 
            it will be read from this file.  If file_name is None, the 
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.INJECTION_FRACTION_KEY,
                                        experiment_id, self.resolution)

        if file_name is None:
            raise Exception("No file name to save volume.")

        if not os.path.exists(file_name):
            Manifest.safe_mkdir(os.path.dirname(file_name))

            self.api.download_injection_fraction(file_name, experiment_id,
                                                 self.resolution)

        return nrrd.read(file_name)

    def get_data_mask(self, experiment_id, file_name=None):
        """ 
        Read a data mask volume for a single experiment. Download it 
        first if it doesn't exist.  Data mask is a binary mask of
        voxels that have valid data.  Only use valid data in analysis!
        
        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists, 
            it will be read from this file.  If file_name is None, the 
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.DATA_MASK_KEY,
                                        experiment_id, self.resolution)

        if file_name is None:
            raise Exception("No file name to save volume.")

        if not os.path.exists(file_name):
            Manifest.safe_mkdir(os.path.dirname(file_name))

            self.api.download_data_mask(file_name, experiment_id,
                                        self.resolution)

        return nrrd.read(file_name)

    def get_ontology(self, file_name=None):
        """ 
        Read the list of adult mouse structures and return an Ontology instance.

        Parameters
        ----------

        file_name: string
            File name to save/read the structures table.  If file_name is None, 
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.
        """

        return Ontology(self.get_structures(file_name))

    def get_structures(self, file_name=None):
        """ 
        Read the list of adult mouse structures and return a Pandas DataFrame.

        Parameters
        ----------

        file_name: string
            File name to save/read the structures table.  If file_name is None, 
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.
        """

        file_name = self.get_cache_path(file_name, self.STRUCTURES_KEY)

        if os.path.exists(file_name):
            structures = pd.DataFrame.from_csv(file_name)
        else:
            structures = OntologiesApi().get_structures(1)
            structures = pd.DataFrame(structures)

            if self.cache:
                Manifest.safe_mkdir(os.path.dirname(file_name))

                structures.to_csv(file_name)

        structures.set_index(['id'], inplace=True, drop=False)
        return structures

    def get_experiments(self,
                        dataframe=False,
                        file_name=None,
                        cre=None,
                        injection_structure_ids=None):
        """
        Read a list of experiments that match certain criteria.  If caching is enabled,
        this will save the whole (unfiltered) list of experiments to a file.

        Parameters
        ----------
        
        dataframe: boolean
            Return the list of experiments as a Pandas DataFrame.  If False,
            return a list of dictionaries.  Default False. 

        file_name: string
            File name to save/read the structures table.  If file_name is None, 
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.

        """

        file_name = self.get_cache_path(file_name, self.EXPERIMENTS_KEY)

        if os.path.exists(file_name):
            experiments = json_utilities.read(file_name)
        else:
            experiments = self.api.experiment_source_search(
                injection_structures='root')

            # removing these elements because they are specific to a particular resolution
            for e in experiments:
                del e['num-voxels']
                del e['injection-volume']
                del e['sum']
                del e['name']

            if self.cache:
                Manifest.safe_mkdir(os.path.dirname(file_name))

                json_utilities.write(file_name, experiments)

        # filter the read/downloaded list of experiments
        experiments = self.filter_experiments(experiments, cre,
                                              injection_structure_ids)

        if dataframe:
            experiments = pd.DataFrame(experiments)
            experiments.set_index(['id'], inplace=True, drop=False)

        return experiments

    def filter_experiments(self,
                           experiments,
                           cre=None,
                           injection_structure_ids=None):
        """ 
        Take a list of experiments and filter them by cre status and injection structure.

        Parameters
        ----------

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.
        """

        if cre == True:
            experiments = [e for e in experiments if e['transgenic-line']]
        elif cre == False:
            experiments = [e for e in experiments if not e['transgenic-line']]
        elif cre is not None:
            experiments = [
                e for e in experiments if e['transgenic-line'] in cre
            ]

        if injection_structure_ids is not None:
            descendant_ids = self.get_ontology().get_descendant_ids(
                injection_structure_ids)
            experiments = [
                e for e in experiments if e['structure-id'] in descendant_ids
            ]

        return experiments

    def get_experiment_structure_unionizes(self,
                                           experiment_id,
                                           file_name=None,
                                           is_injection=None,
                                           structure_ids=None,
                                           hemisphere_ids=None):
        """
        Retrieve the structure unionize data for a specific experiment.  Filter by 
        structure, injection status, and hemisphere.

        Parameters
        ----------
        
        experiment_id: int
            ID of the experiment of interest.  Corresponds to section_data_set_id in the API.

        file_name: string
            File name to save/read the experiments list.  If file_name is None, 
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.            

        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records that are inside a specific set of structures.
            If None, return all records. Default None.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all 
            records [1, 2, 3].  Default None.
            
        """

        file_name = self.get_cache_path(file_name,
                                        self.STRUCTURE_UNIONIZES_KEY,
                                        experiment_id)

        if os.path.exists(file_name):
            unionizes = pd.DataFrame.from_csv(file_name)
        else:
            unionizes = self.api.get_structure_unionizes([experiment_id])
            unionizes = pd.DataFrame(unionizes)

            # rename section_data_set_id column to experiment_id
            unionizes.columns = [
                'experiment_id' if c == 'section_data_set_id' else c
                for c in unionizes.columns
            ]

            if self.cache:
                Manifest.safe_mkdir(os.path.dirname(file_name))

                unionizes.to_csv(file_name)

        return self.filter_structure_unionizes(unionizes, is_injection,
                                               structure_ids, hemisphere_ids)

    def filter_structure_unionizes(self,
                                   unionizes,
                                   is_injection=None,
                                   structure_ids=None,
                                   hemisphere_ids=None):
        """
        Take a list of unionzes and return a subset of records filtered by injection status, structure, and
        hemisphere.

        Parameters
        ----------
        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records that are inside a specific set of structures.
            If None, return all records. Default None.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all 
            records [1, 2, 3].  Default None.
        """
        if is_injection is not None:
            unionizes = unionizes[unionizes.is_injection == is_injection]

        if structure_ids is not None:
            descendant_ids = self.get_ontology().get_descendant_ids(
                structure_ids)
            unionizes = unionizes[unionizes['structure_id'].isin(
                descendant_ids)]

        if hemisphere_ids is not None:
            unionizes = unionizes[unionizes['hemisphere_id'].isin(
                hemisphere_ids)]

        return unionizes

    def get_structure_unionizes(self,
                                experiment_ids,
                                is_injection=None,
                                structure_ids=None,
                                hemisphere_ids=None):
        """
        Get structure unionizes for a set of experiment IDs.  Filter the results by injection status, 
        structure, and hemisphere.

        Parameters
        ----------
        experiment_ids: list
            List of experiment IDs.  Corresponds to section_data_set_id in the API.
        
        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records that are inside a specific set of structures.
            If None, return all records. Default None.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all 
            records [1, 2, 3].  Default None.
        """

        unionizes = [
            self.get_experiment_structure_unionizes(
                eid,
                is_injection=is_injection,
                structure_ids=structure_ids,
                hemisphere_ids=hemisphere_ids) for eid in experiment_ids
        ]

        return pd.concat(unionizes, ignore_index=True)

    def get_projection_matrix(self,
                              experiment_ids,
                              projection_structure_ids,
                              hemisphere_ids=None,
                              parameter='projection_volume',
                              dataframe=False):

        unionizes = self.get_structure_unionizes(experiment_ids,
                                                 is_injection=False,
                                                 hemisphere_ids=hemisphere_ids)

        unionizes = unionizes[unionizes.structure_id.isin(
            projection_structure_ids)]

        projection_structure_ids = set(
            unionizes['structure_id'].values.tolist())
        hemisphere_ids = set(unionizes['hemisphere_id'].values.tolist())

        nrows = len(experiment_ids)
        ncolumns = len(projection_structure_ids) * len(hemisphere_ids)

        matrix = np.empty((nrows, ncolumns))
        matrix[:] = np.NAN

        row_lookup = {}
        for idx, e in enumerate(experiment_ids):
            row_lookup[e] = idx

        column_lookup = {}
        columns = []

        cidx = 0
        hlabel = {1: '-L', 2: '-R', 3: ''}

        o = self.get_ontology()

        for hid in hemisphere_ids:
            for sid in projection_structure_ids:
                column_lookup[(hid, sid)] = cidx
                label = o[sid].iloc[0]['acronym'] + hlabel[hid]
                columns.append({
                    'hemisphere_id': hid,
                    'structure_id': sid,
                    'label': label
                })
                cidx += 1

        for _, row in unionizes.iterrows():
            ridx = row_lookup[row['experiment_id']]
            k = (row['hemisphere_id'], row['structure_id'])
            cidx = column_lookup[k]
            matrix[ridx, cidx] = row[parameter]

        if dataframe:
            all_experiments = self.get_experiments(dataframe=True)

            rows_df = all_experiments.loc[experiment_ids]

            cols_df = pd.DataFrame(columns)

            return {'matrix': matrix, 'rows': rows_df, 'columns': cols_df}
        else:
            return {
                'matrix': matrix,
                'rows': experiment_ids,
                'columns': columns
            }

    def get_structure_mask(self,
                           structure_id,
                           file_name=None,
                           annotation_file_name=None):
        """
        Read a 3D numpy array shaped like the annotation volume that has non-zero values where 
        voxels belong to a particular structure.  This will take care of identifying substructures.

        Parameters
        ----------
        
        structure_id: int
            ID of a structure.  

        file_name: string
            File name to store the structure mask.  If it already exists, 
            it will be read from this file.  If file_name is None, the 
            file_name will be pulled out of the manifest.  Default is None.
        
        annotation_file_name: string
            File name to store the annotation volume.  If it already exists, 
            it will be read from this file.  If file_name is None, the 
            file_name will be pulled out of the manifest.  Default is None.            
        """

        file_name = self.get_cache_path(file_name, self.STRUCTURE_MASK_KEY,
                                        structure_id)

        if os.path.exists(file_name):
            return nrrd.read(file_name)
        else:
            ont = self.get_ontology()
            structure_ids = ont.get_descendant_ids([structure_id])
            annotation, _ = self.get_annotation_volume(annotation_file_name)
            mask = self.make_structure_mask(structure_ids, annotation)

            if self.cache:
                Manifest.safe_mkdir(os.path.dirname(file_name))
                nrrd.write(file_name, mask)

            return mask, None

    def make_structure_mask(self, structure_ids, annotation):
        """
        Look at an annotation volume and identify voxels that have values
        in a list of structure ids.

        Parameters
        ----------

        structure_ids: list
            List of IDs to look for in the annotation volume

        annotation: np.ndarray
            Numpy array filled with IDs.

        """

        m = np.zeros(annotation.shape, dtype=np.uint8)

        for _, sid in enumerate(structure_ids):
            m[annotation == sid] = 1

        return m

    def build_manifest(self, file_name):
        """
        Construct a manifest for this Cache class and save it in a file.
        
        Parameters
        ----------
        
        file_name: string
            File location to save the manifest.

        """

        manifest_builder = ManifestBuilder()
        manifest_builder.add_path('BASEDIR', '.')

        manifest_builder.add_path(self.EXPERIMENTS_KEY,
                                  'experiments.json',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.STRUCTURES_KEY,
                                  'structures.csv',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.STRUCTURE_UNIONIZES_KEY,
                                  'experiment_%d/structure_unionizes.csv',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.ANNOTATION_KEY,
                                  'annotation_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.TEMPLATE_KEY,
                                  'average_template_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.INJECTION_DENSITY_KEY,
                                  'experiment_%d/injection_density_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.INJECTION_FRACTION_KEY,
                                  'experiment_%d/injection_fraction_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.DATA_MASK_KEY,
                                  'experiment_%d/data_mask_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.PROJECTION_DENSITY_KEY,
                                  'experiment_%d/projection_density_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.STRUCTURE_MASK_KEY,
                                  'structure_masks/structure_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.write_json_file(file_name)
class MouseConnectivityCache(Cache):
    """
    Cache class for storing and accessing data related to the adult mouse
    Connectivity Atlas.  By default, this class will cache any downloaded
    metadata or files in well known locations defined in a manifest file.
    This behavior can be disabled.

    Attributes
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    api: MouseConnectivityApi instance
        Used internally to make API queries.

    Parameters
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    ccf_version: string
        Desired version of the Common Coordinate Framework.  This affects the annotation 
        volume (get_annotation_volume) and structure masks (get_structure_mask). 
        Must be one of (MouseConnectivityApi.CCF_2015, MouseConnectivityApi.CCF_2016). 
        Default: MouseConnectivityApi.CCF_2016

    cache: boolean
        Whether the class should save results of API queries to locations specified
        in the manifest file.  Queries for files (as opposed to metadata) must have a
        file location.  If caching is disabled, those locations must be specified
        in the function call (e.g. get_projection_density(file_name='file.nrrd')).

    manifest_file: string
        File name of the manifest to be read.  Default is "mouse_connectivity_manifest.json".

    """

    CCF_VERSION_KEY = "CCF_VERSION"
    ANNOTATION_KEY = "ANNOTATION"
    TEMPLATE_KEY = "TEMPLATE"
    PROJECTION_DENSITY_KEY = "PROJECTION_DENSITY"
    INJECTION_DENSITY_KEY = "INJECTION_DENSITY"
    INJECTION_FRACTION_KEY = "INJECTION_FRACTION"
    DATA_MASK_KEY = "DATA_MASK"
    STRUCTURE_UNIONIZES_KEY = "STRUCTURE_UNIONIZES"
    EXPERIMENTS_KEY = "EXPERIMENTS"
    STRUCTURES_KEY = "STRUCTURES"
    STRUCTURE_MASK_KEY = "STRUCTURE_MASK"

    def __init__(
        self,
        resolution=None,
        cache=True,
        manifest_file="mouse_connectivity_manifest.json",
        ccf_version=None,
        base_uri=None,
    ):
        super(MouseConnectivityCache, self).__init__(manifest=manifest_file, cache=cache)

        if resolution is None:
            self.resolution = MouseConnectivityApi.VOXEL_RESOLUTION_25_MICRONS
        else:
            self.resolution = resolution
        self.api = MouseConnectivityApi(base_uri=base_uri)

        if ccf_version is None:
            ccf_version = MouseConnectivityApi.CCF_VERSION_DEFAULT
        self.ccf_version = ccf_version

    def get_annotation_volume(self, file_name=None):
        """
        Read the annotation volume.  Download it first if it doesn't exist.

        Parameters
        ----------

        file_name: string
            File name to store the annotation volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.ANNOTATION_KEY, self.ccf_version, self.resolution)

        if file_name is None:
            raise Exception("No save file name provided for annotation volume.")

        if os.path.exists(file_name):
            annotation, info = nrrd.read(file_name)
        else:
            Manifest.safe_make_parent_dirs(file_name)

            annotation, info = self.api.download_annotation_volume(self.ccf_version, self.resolution, file_name)

        return annotation, info

    def get_template_volume(self, file_name=None):
        """
        Read the template volume.  Download it first if it doesn't exist.

        Parameters
        ----------

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.TEMPLATE_KEY, self.resolution)

        if file_name is None:
            raise Exception("No save file provided for annotation volume.")

        if os.path.exists(file_name):
            annotation, info = nrrd.read(file_name)
        else:
            Manifest.safe_make_parent_dirs(file_name)

            annotation, info = self.api.download_template_volume(self.resolution, file_name)

        return annotation, info

    def get_projection_density(self, experiment_id, file_name=None):
        """
        Read a projection density volume for a single experiment.  Download it
        first if it doesn't exist.  Projection density is the proportion of
        of projecting pixels in a grid voxel in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.PROJECTION_DENSITY_KEY, experiment_id, self.resolution)

        if file_name is None:
            raise Exception("No file name to save volume.")

        if not os.path.exists(file_name):
            Manifest.safe_make_parent_dirs(file_name)

            self.api.download_projection_density(file_name, experiment_id, self.resolution)

        return nrrd.read(file_name)

    def get_injection_density(self, experiment_id, file_name=None):
        """
        Read an injection density volume for a single experiment. Download it
        first if it doesn't exist.  Injection density is the proportion of
        projecting pixels in a grid voxel only including pixels that are
        part of the injection site in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.INJECTION_DENSITY_KEY, experiment_id, self.resolution)

        if file_name is None:
            raise Exception("No file name to save volume.")

        if not os.path.exists(file_name):
            Manifest.safe_make_parent_dirs(file_name)

            self.api.download_injection_density(file_name, experiment_id, self.resolution)

        return nrrd.read(file_name)

    def get_injection_fraction(self, experiment_id, file_name=None):
        """
        Read an injection fraction volume for a single experiment. Download it
        first if it doesn't exist.  Injection fraction is the proportion of
        pixels in the injection site in a grid voxel in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.INJECTION_FRACTION_KEY, experiment_id, self.resolution)

        if file_name is None:
            raise Exception("No file name to save volume.")

        if not os.path.exists(file_name):
            Manifest.safe_make_parent_dirs(file_name)

            self.api.download_injection_fraction(file_name, experiment_id, self.resolution)

        return nrrd.read(file_name)

    def get_data_mask(self, experiment_id, file_name=None):
        """
        Read a data mask volume for a single experiment. Download it
        first if it doesn't exist.  Data mask is a binary mask of
        voxels that have valid data.  Only use valid data in analysis!

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.DATA_MASK_KEY, experiment_id, self.resolution)

        if file_name is None:
            raise Exception("No file name to save volume.")

        if not os.path.exists(file_name):
            Manifest.safe_make_parent_dirs(file_name)

            self.api.download_data_mask(file_name, experiment_id, self.resolution)

        return nrrd.read(file_name)

    def get_ontology(self, file_name=None):
        """
        Read the list of adult mouse structures and return an Ontology instance.

        Parameters
        ----------

        file_name: string
            File name to save/read the structures table.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.
        """

        return Ontology(self.get_structures(file_name))

    def get_structures(self, file_name=None):
        """
        Read the list of adult mouse structures and return a Pandas DataFrame.

        Parameters
        ----------

        file_name: string
            File name to save/read the structures table.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.
        """

        file_name = self.get_cache_path(file_name, self.STRUCTURES_KEY)

        if os.path.exists(file_name):
            structures = pd.DataFrame.from_csv(file_name)
        else:
            structures = OntologiesApi(base_uri=self.api.api_url).get_structures(1)
            structures = pd.DataFrame(structures)

            if self.cache:
                Manifest.safe_make_parent_dirs(file_name)

                structures.to_csv(file_name)

        structures.set_index(["id"], inplace=True, drop=False)
        return structures

    def get_experiments(self, dataframe=False, file_name=None, cre=None, injection_structure_ids=None):
        """
        Read a list of experiments that match certain criteria.  If caching is enabled,
        this will save the whole (unfiltered) list of experiments to a file.

        Parameters
        ----------

        dataframe: boolean
            Return the list of experiments as a Pandas DataFrame.  If False,
            return a list of dictionaries.  Default False.

        file_name: string
            File name to save/read the structures table.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.

        """

        file_name = self.get_cache_path(file_name, self.EXPERIMENTS_KEY)

        if os.path.exists(file_name):
            experiments = json_utilities.read(file_name)
        else:
            experiments = self.api.experiment_source_search(injection_structures="root")

            # removing these elements because they are specific to a particular
            # resolution
            for e in experiments:
                del e["num-voxels"]
                del e["injection-volume"]
                del e["sum"]
                del e["name"]

            if self.cache:
                Manifest.safe_make_parent_dirs(file_name)

                json_utilities.write(file_name, experiments)

        # filter the read/downloaded list of experiments
        experiments = self.filter_experiments(experiments, cre, injection_structure_ids)

        if dataframe:
            experiments = pd.DataFrame(experiments)
            experiments.set_index(["id"], inplace=True, drop=False)

        return experiments

    def filter_experiments(self, experiments, cre=None, injection_structure_ids=None):
        """
        Take a list of experiments and filter them by cre status and injection structure.

        Parameters
        ----------

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.
        """

        if cre is True:
            experiments = [e for e in experiments if e["transgenic-line"]]
        elif cre is False:
            experiments = [e for e in experiments if not e["transgenic-line"]]
        elif cre is not None:
            experiments = [e for e in experiments if e["transgenic-line"] in cre]

        if injection_structure_ids is not None:
            descendant_ids = self.get_ontology().get_descendant_ids(injection_structure_ids)
            experiments = [e for e in experiments if e["structure-id"] in descendant_ids]

        return experiments

    def get_experiment_structure_unionizes(
        self,
        experiment_id,
        file_name=None,
        is_injection=None,
        structure_ids=None,
        include_descendants=False,
        hemisphere_ids=None,
    ):
        """
        Retrieve the structure unionize data for a specific experiment.  Filter by
        structure, injection status, and hemisphere.

        Parameters
        ----------

        experiment_id: int
            ID of the experiment of interest.  Corresponds to section_data_set_id in the API.

        file_name: string
            File name to save/read the experiments list.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.

        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a specific set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.

        """

        file_name = self.get_cache_path(file_name, self.STRUCTURE_UNIONIZES_KEY, experiment_id)

        if os.path.exists(file_name):
            unionizes = pd.DataFrame.from_csv(file_name)
        else:
            unionizes = self.api.get_structure_unionizes([experiment_id])
            unionizes = pd.DataFrame(unionizes)

            # rename section_data_set_id column to experiment_id
            unionizes.columns = ["experiment_id" if c == "section_data_set_id" else c for c in unionizes.columns]

            if self.cache:
                Manifest.safe_make_parent_dirs(file_name)

                unionizes.to_csv(file_name)

        return self.filter_structure_unionizes(
            unionizes, is_injection, structure_ids, include_descendants, hemisphere_ids
        )

    def filter_structure_unionizes(
        self, unionizes, is_injection=None, structure_ids=None, include_descendants=False, hemisphere_ids=None
    ):
        """
        Take a list of unionzes and return a subset of records filtered by injection status, structure, and
        hemisphere.

        Parameters
        ----------
        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.
        """
        if is_injection is not None:
            unionizes = unionizes[unionizes.is_injection == is_injection]

        if structure_ids is not None:
            if include_descendants:
                structure_ids = self.get_ontology().get_descendant_ids(structure_ids)
            else:
                structure_ids = set(structure_ids)

            unionizes = unionizes[unionizes["structure_id"].isin(structure_ids)]

        if hemisphere_ids is not None:
            unionizes = unionizes[unionizes["hemisphere_id"].isin(hemisphere_ids)]

        return unionizes

    def get_structure_unionizes(
        self, experiment_ids, is_injection=None, structure_ids=None, include_descendants=False, hemisphere_ids=None
    ):
        """
        Get structure unionizes for a set of experiment IDs.  Filter the results by injection status,
        structure, and hemisphere.

        Parameters
        ----------
        experiment_ids: list
            List of experiment IDs.  Corresponds to section_data_set_id in the API.

        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a specific set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.
        """

        unionizes = [
            self.get_experiment_structure_unionizes(
                eid,
                is_injection=is_injection,
                structure_ids=structure_ids,
                include_descendants=include_descendants,
                hemisphere_ids=hemisphere_ids,
            )
            for eid in experiment_ids
        ]

        return pd.concat(unionizes, ignore_index=True)

    def get_projection_matrix(
        self,
        experiment_ids,
        projection_structure_ids,
        hemisphere_ids=None,
        parameter="projection_volume",
        dataframe=False,
    ):

        unionizes = self.get_structure_unionizes(
            experiment_ids,
            is_injection=False,
            structure_ids=projection_structure_ids,
            include_descendants=False,
            hemisphere_ids=hemisphere_ids,
        )

        hemisphere_ids = set(unionizes["hemisphere_id"].values.tolist())

        nrows = len(experiment_ids)
        ncolumns = len(projection_structure_ids) * len(hemisphere_ids)

        matrix = np.empty((nrows, ncolumns))
        matrix[:] = np.NAN

        row_lookup = {}
        for idx, e in enumerate(experiment_ids):
            row_lookup[e] = idx

        column_lookup = {}
        columns = []

        cidx = 0
        hlabel = {1: "-L", 2: "-R", 3: ""}

        o = self.get_ontology()

        for hid in hemisphere_ids:
            for sid in projection_structure_ids:
                column_lookup[(hid, sid)] = cidx
                label = o[sid].iloc[0]["acronym"] + hlabel[hid]
                columns.append({"hemisphere_id": hid, "structure_id": sid, "label": label})
                cidx += 1

        for _, row in unionizes.iterrows():
            ridx = row_lookup[row["experiment_id"]]
            k = (row["hemisphere_id"], row["structure_id"])
            cidx = column_lookup[k]
            matrix[ridx, cidx] = row[parameter]

        if dataframe:
            all_experiments = self.get_experiments(dataframe=True)

            rows_df = all_experiments.loc[experiment_ids]

            cols_df = pd.DataFrame(columns)

            return {"matrix": matrix, "rows": rows_df, "columns": cols_df}
        else:
            return {"matrix": matrix, "rows": experiment_ids, "columns": columns}

    def get_structure_mask(self, structure_id, file_name=None, annotation_file_name=None):
        """
        Read a 3D numpy array shaped like the annotation volume that has non-zero values where
        voxels belong to a particular structure.  This will take care of identifying substructures.

        Parameters
        ----------

        structure_id: int
            ID of a structure.

        file_name: string
            File name to store the structure mask.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        annotation_file_name: string
            File name to store the annotation volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.
        """

        file_name = self.get_cache_path(file_name, self.STRUCTURE_MASK_KEY, structure_id)

        if os.path.exists(file_name):
            return nrrd.read(file_name)
        else:
            ont = self.get_ontology()
            structure_ids = ont.get_descendant_ids([structure_id])
            annotation, _ = self.get_annotation_volume(annotation_file_name)
            mask = self.make_structure_mask(structure_ids, annotation)

            if self.cache:
                Manifest.safe_make_parent_dirs(file_name)
                nrrd.write(file_name, mask)

            return mask, None

    def make_structure_mask(self, structure_ids, annotation):
        """
        Look at an annotation volume and identify voxels that have values
        in a list of structure ids.

        Parameters
        ----------

        structure_ids: list
            List of IDs to look for in the annotation volume

        annotation: np.ndarray
            Numpy array filled with IDs.

        """

        m = np.zeros(annotation.shape, dtype=np.uint8)

        for _, sid in enumerate(structure_ids):
            m[annotation == sid] = 1

        return m

    def build_manifest(self, file_name):
        """
        Construct a manifest for this Cache class and save it in a file.

        Parameters
        ----------

        file_name: string
            File location to save the manifest.

        """

        manifest_builder = ManifestBuilder()
        manifest_builder.add_path("BASEDIR", ".")

        manifest_builder.add_path(self.EXPERIMENTS_KEY, "experiments.json", parent_key="BASEDIR", typename="file")

        manifest_builder.add_path(self.STRUCTURES_KEY, "structures.csv", parent_key="BASEDIR", typename="file")

        manifest_builder.add_path(
            self.STRUCTURE_UNIONIZES_KEY, "experiment_%d/structure_unionizes.csv", parent_key="BASEDIR", typename="file"
        )

        manifest_builder.add_path(self.CCF_VERSION_KEY, "%s", parent_key="BASEDIR", typename="dir")

        manifest_builder.add_path(
            self.ANNOTATION_KEY, "annotation_%d.nrrd", parent_key=self.CCF_VERSION_KEY, typename="file"
        )

        manifest_builder.add_path(self.TEMPLATE_KEY, "average_template_%d.nrrd", parent_key="BASEDIR", typename="file")

        manifest_builder.add_path(
            self.INJECTION_DENSITY_KEY, "experiment_%d/injection_density_%d.nrrd", parent_key="BASEDIR", typename="file"
        )

        manifest_builder.add_path(
            self.INJECTION_FRACTION_KEY,
            "experiment_%d/injection_fraction_%d.nrrd",
            parent_key="BASEDIR",
            typename="file",
        )

        manifest_builder.add_path(
            self.DATA_MASK_KEY, "experiment_%d/data_mask_%d.nrrd", parent_key="BASEDIR", typename="file"
        )

        manifest_builder.add_path(
            self.PROJECTION_DENSITY_KEY,
            "experiment_%d/projection_density_%d.nrrd",
            parent_key="BASEDIR",
            typename="file",
        )

        manifest_builder.add_path(
            self.STRUCTURE_MASK_KEY, "structure_masks/structure_%d.nrrd", parent_key="BASEDIR", typename="file"
        )

        manifest_builder.write_json_file(file_name)
Beispiel #28
0
class ABA(Paths):
	"""
	This class handles interaction with the Allen Brain Atlas datasets and APIs to get structure trees,
	experimental metadata and results, tractography data etc. 
	"""
	
	# useful vars for analysis    
	excluded_regions = ["fiber tracts"]
	resolution = 25

	def __init__(self, projection_metric = "projection_energy", base_dir=None, **kwargs):
		""" 
		Set up file paths and Allen SDKs
		
		:param base_dir: path to directory to use for saving data (default value None)
		:param path_fiprojection_metricle: - str, metric to quantify the strength of projections from the Allen Connectome. (default value 'projection_energy')
		:param kwargs: can be used to pass path to individual data folders. See brainrender/Utils/paths_manager.py

		"""

		Paths.__init__(self, base_dir=base_dir, **kwargs)

		self.projection_metric = projection_metric

		# get mouse connectivity cache and structure tree
		self.mcc = MouseConnectivityCache(manifest_file=os.path.join(self.mouse_connectivity_cache, "manifest.json"))
		self.structure_tree = self.mcc.get_structure_tree()
		
		# get ontologies API and brain structures sets
		self.oapi = OntologiesApi()
		self.get_structures_sets()

		# get reference space
		self.space = ReferenceSpaceApi()
		self.spacecache = ReferenceSpaceCache(
			manifest=os.path.join(self.annotated_volume, "manifest.json"),  # downloaded files are stored relative to here
			resolution=self.resolution,
			reference_space_key="annotation/ccf_2017"  # use the latest version of the CCF
			)
		self.annotated_volume, _ = self.spacecache.get_annotation_volume()

		# mouse connectivity API [used for tractography]
		self.mca = MouseConnectivityApi()

		# Get tree search api
		self.tree_search = TreeSearchApi()

		# Get some metadata about experiments
		self.all_experiments = self.mcc.get_experiments(dataframe=True)
		self.strains = sorted([x for x in set(self.all_experiments.strain) if x is not None])
		self.transgenic_lines = sorted(set([x for x in set(self.all_experiments.transgenic_line) if x is not None]))

	####### GET EXPERIMENTS DATA
	def get_structures_sets(self):
		""" 
		Get the Allen's structure sets.
		"""
		summary_structures = self.structure_tree.get_structures_by_set_id([167587189])  # main summary structures
		summary_structures = [s for s in summary_structures if s["acronym"] not in self.excluded_regions]
		self.structures = pd.DataFrame(summary_structures)

		# Other structures sets
		try:
			all_sets = pd.DataFrame(self.oapi.get_structure_sets())
		except:
			print("Could not retrieve data, possibly because there is no internet connection.")
		else:
			sets = ["Summary structures of the pons", "Summary structures of the thalamus", 
						"Summary structures of the hypothalamus", "List of structures for ABA Fine Structure Search",
						"Structures representing the major divisions of the mouse brain", "Summary structures of the midbrain", "Structures whose surfaces are represented by a precomputed mesh"]
			self.other_sets = {}
			for set_name in sets:
				set_id = all_sets.loc[all_sets.description == set_name].id.values[0]
				self.other_sets[set_name] = pd.DataFrame(self.structure_tree.get_structures_by_set_id([set_id]))

			self.all_avaliable_meshes = sorted(self.other_sets["Structures whose surfaces are represented by a precomputed mesh"].acronym.values)

	def print_structures_list_to_text(self):
		""" 
		Saves the name of every brain structure for which a 3d mesh (.obj file) is available in a text file.
		"""
		s = self.other_sets["Structures whose surfaces are represented by a precomputed mesh"].sort_values('acronym')
		with open('all_regions.txt', 'w') as o:
			for acr, name in zip(s.acronym.values, s['name'].values):
				o.write("({}) -- {}\n".format(acr, name))

	def load_all_experiments(self, cre=False):
		"""
		This function downloads all the experimental data from the MouseConnectivityCache and saves the unionized results
		as pickled pandas dataframes. The process is slow, but the ammount of disk space necessary to save the data is small,
		so it's worth downloading all the experiments at once to speed up subsequent analysis.

		:param cre: Bool - data from either wild time or cre mice lines (Default value = False)

		"""
		
		if not cre: raise NotImplementedError("Only works for wild type sorry")
		# Downloads all experiments from allen brain atlas and saves the results as an easy to read pkl file
		for acronym in self.structures.acronym.values:
			print("Fetching experiments for : {}".format(acronym))

			structure = self.structure_tree.get_structures_by_acronym([acronym])[0]
			experiments = self.mcc.get_experiments(cre=cre, injection_structure_ids=[structure['id']])

			print("     found {} experiments".format(len(experiments)))

			try:
				structure_unionizes = self.mcc.get_structure_unionizes([e['id'] for e in experiments], 
															is_injection=False,
															structure_ids=self.structures.id.values,
															include_descendants=False)
			except: pass
			structure_unionizes.to_pickle(os.path.join(self.output_data, "{}.pkl".format(acronym)))
	
	def print_structures(self):
		""" 
		Prints the name of every structure in the structure tree to the console.
		"""
		acronyms, names = self.structures.acronym.values, self.structures['name'].values
		sort_idx = np.argsort(acronyms)
		acronyms, names = acronyms[sort_idx], names[sort_idx]
		[print("({}) - {}".format(a, n)) for a,n in zip(acronyms, names)]

	def experiments_source_search(self, SOI, *args, source=True,  **kwargs):
		"""
		Returns data about experiments whose injection was in the SOI, structure of interest

		:param SOI: str, structure of interest. Acronym of structure to use as seed for teh search
		:param *args: 
		:param source:  (Default value = True)
		:param **kwargs: 

		"""
		"""
			list of possible kwargs
				injection_structures : list of integers or strings
					Integer Structure.id or String Structure.acronym.
				target_domain : list of integers or strings, optional
					Integer Structure.id or String Structure.acronym.
				injection_hemisphere : string, optional
					'right' or 'left', Defaults to both hemispheres.
				target_hemisphere : string, optional
					'right' or 'left', Defaults to both hemispheres.
				transgenic_lines : list of integers or strings, optional
					Integer TransgenicLine.id or String TransgenicLine.name. Specify ID 0 to exclude all TransgenicLines.
				injection_domain : list of integers or strings, optional
					Integer Structure.id or String Structure.acronym.
				primary_structure_only : boolean, optional
				product_ids : list of integers, optional
					Integer Product.id
				start_row : integer, optional
					For paging purposes. Defaults to 0.
				num_rows : integer, optional
					For paging purposes. Defaults to 2000.

		"""
		transgenic_id = kwargs.pop('transgenic_id', 0) # id = 0 means use only wild type
		primary_structure_only = kwargs.pop('primary_structure_only', True)

		if not isinstance(SOI, list): SOI = [SOI]

		if source:
			injection_structures=SOI
			target_domain = None
		else:
			injection_structures = None
			target_domain = SOI

		return pd.DataFrame(self.mca.experiment_source_search(injection_structures=injection_structures,
											target_domain = target_domain,
											transgenic_lines=transgenic_id,
											primary_structure_only=primary_structure_only))

	def experiments_target_search(self, *args, **kwargs):
		"""

		:param *args: 
		:param **kwargs: 

		"""
		return self.experiments_source_search(*args, source=False, **kwargs)

	def fetch_experiments_data(self, experiments_id, *args, average_experiments=False, **kwargs):
		"""
		Get data and metadata for expeirments in the Allen Mouse Connectome project. 
	
		:param experiments_id: int, list, np.ndarray with ID of experiments whose data need to be fetched
		:param *args: 
		:param average_experiments:  (Default value = False)
		:param **kwargs: 

		"""
		if isinstance(experiments_id, np.ndarray):
			experiments_id = [int(x) for x in experiments_id]
		elif not isinstance(experiments_id, list): 
			experiments_id = [experiments_id]
		if [x for x in experiments_id if not isinstance(x, int)]:
			raise ValueError("Invalid experiments_id argument: {}".format(experiments_id))

		default_structures_ids = self.structures.id.values


		is_injection = kwargs.pop('is_injection', False) # Include only structures that are not injection
		structure_ids = kwargs.pop('structure_ids', default_structures_ids) # Pass IDs of structures of interest 
		hemisphere_ids= kwargs.pop('hemisphere_ids', None) # 1 left, 2 right, 3 both

		if not average_experiments:
			return pd.DataFrame(self.mca.get_structure_unionizes(experiments_id,
												is_injection = is_injection,
												structure_ids = structure_ids,
												hemisphere_ids = hemisphere_ids))
		else:
			raise NotImplementedError("Need to find a way to average across experiments")
			unionized = pd.DataFrame(self.mca.get_structure_unionizes(experiments_id,
												is_injection = is_injection,
												structure_ids = structure_ids,
												hemisphere_ids = hemisphere_ids))

		for regionid in list(set(unionized.structure_id)):
			region_avg = unionized.loc[unionized.structure_id == regionid].mean(axis=1)

	####### ANALYSIS ON EXPERIMENTAL DATA
	def analyze_efferents(self, ROI, projection_metric = None):
		"""
		Loads the experiments on ROI and looks at average statistics of efferent projections

		:param ROI: str, acronym of brain region of interest
		:param projection_metric: if None, the default projection metric is used, otherwise pass a string with metric to use (Default value = None)

		"""
		if projection_metric is None: 
			projection_metric = self.projection_metric

		experiment_data = pd.read_pickle(os.path.join(self.output_data, "{}.pkl".format(ROI)))
		experiment_data = experiment_data.loc[experiment_data.volume > self.volume_threshold]

		# Loop over all structures and get the injection density
		results = {"left":[], "right":[], "both":[], "id":[], "acronym":[], "name":[]}
		for target in self.structures.id.values:
			target_acronym = self.structures.loc[self.structures.id == target].acronym.values[0]
			target_name = self.structures.loc[self.structures.id == target].name.values[0]

			exp_target = experiment_data.loc[experiment_data.structure_id == target]

			exp_target_hemi = self.hemispheres(exp_target.loc[exp_target.hemisphere_id == 1], 
												exp_target.loc[exp_target.hemisphere_id == 2], 
												exp_target.loc[exp_target.hemisphere_id == 3])
			proj_energy = self.hemispheres(np.nanmean(exp_target_hemi.left[projection_metric].values),
											np.nanmean(exp_target_hemi.right[projection_metric].values),
											np.nanmean(exp_target_hemi.both[projection_metric].values)
			)


			for hemi in self.hemispheres_names:
				results[hemi].append(proj_energy._asdict()[hemi])
			results["id"].append(target)
			results["acronym"].append(target_acronym)
			results["name"].append(target_name)

		results = pd.DataFrame.from_dict(results).sort_values("right", na_position = "first")
		return results

	def analyze_afferents(self, ROI, projection_metric = None):
		"""[Loads the experiments on ROI and looks at average statistics of afferent projections]

		:param ROI: str, acronym of region of itnerest
		:param projection_metric: if None, the default projection metric is used, otherwise pass a string with metric to use (Default value = None)

		"""
		if projection_metric is None: 
			projection_metric = self.projection_metric
		ROI_id = self.structure_tree.get_structures_by_acronym([ROI])[0]["id"]

		# Loop over all strctures and get projection towards SOI
		results = {"left":[], "right":[], "both":[], "id":[], "acronym":[], "name":[]}

		for origin in self.structures.id.values:
			origin_acronym = self.structures.loc[self.structures.id == origin].acronym.values[0]
			origin_name = self.structures.loc[self.structures.id == origin].name.values[0]

			experiment_data = pd.read_pickle(os.path.join(self.output_data, "{}.pkl".format(origin_acronym)))
			experiment_data = experiment_data.loc[experiment_data.volume > self.volume_threshold]

			exp_target = experiment_data.loc[experiment_data.structure_id == SOI_id]
			exp_target_hemi = self.hemispheres(exp_target.loc[exp_target.hemisphere_id == 1], exp_target.loc[exp_target.hemisphere_id == 2], exp_target.loc[exp_target.hemisphere_id == 3])
			proj_energy = self.hemispheres(np.nanmean(exp_target_hemi.left[projection_metric].values),
											np.nanmean(exp_target_hemi.right[projection_metric].values),
											np.nanmean(exp_target_hemi.both[projection_metric].values)
			)
			for hemi in self.hemispheres_names:
				results[hemi].append(proj_energy._asdict()[hemi])
			results["id"].append(origin)
			results["acronym"].append(origin_acronym)
			results["name"].append(origin_name)

		results = pd.DataFrame.from_dict(results).sort_values("right", na_position = "first")
		return results

	####### GET TRACTOGRAPHY AND SPATIAL DATA
	def get_projection_tracts_to_target(self, p0=None, **kwargs):
		"""
		Gets tractography data for all experiments whose projections reach the brain region or location of iterest.
		
		:param p0: list of 3 floats with XYZ coordinates of point to be used as seed (Default value = None)
		:param **kwargs: 
		"""

		# check args
		if p0 is None:
			raise ValueError("Please pass coordinates")
		elif isinstance(p0, np.ndarray):
			p0 = list(p0)
		elif not isinstance(p0, (list, tuple)):
			raise ValueError("Invalid argument passed (p0): {}".format(p0))

		tract = self.mca.experiment_spatial_search(seed_point=p0, **kwargs)

		if isinstance(tract, str): 
			raise ValueError('Something went wrong with query, query error message:\n{}'.format(tract))
		else:
			return tract

	### OPERATIONS ON STRUCTURE TREES
	def get_structure_ancestors(self, regions, ancestors=True, descendants=False):
		"""
		Get's the ancestors of the region(s) passed as arguments

		:param regions: str, list of str with acronums of regions of interest
		:param ancestors: if True, returns the ancestors of the region  (Default value = True)
		:param descendants: if True, returns the descendants of the region (Default value = False)

		"""

		if not isinstance(regions, list):
			struct_id = self.structure_tree.get_structures_by_acronym([regions])[0]['id']
			return pd.DataFrame(self.tree_search.get_tree('Structure', struct_id, ancestors=ancestors, descendants=descendants))
		else:
			ancestors = []
			for region in regions:
				struct_id = self.structure_tree.get_structures_by_acronym([region])[0]['id']
				ancestors.append(pd.DataFrame(self.tree_search.get_tree('Structure', struct_id, ancestors=ancestors, descendants=descendants)))
			return ancestors

	def get_structure_descendants(self, regions):
		return self.get_structure_ancestors(regions, ancestors=False, descendants=True)

	def get_structure_from_coordinates(self, p0):
		"""
		Given a point in the Allen Mouse Brain reference space, returns the brain region that the point is in. 

		:param p0: list of floats with XYZ coordinates. 

		"""
		voxel = np.round(np.array(p0) / self.resolution).astype(int)
		try:
			structure_id = self.annotated_volume[voxel[0], voxel[1], voxel[2]]
		except:
			return None

		# Each voxel in the annotation volume is annotated as specifically as possible
		structure = self.structure_tree.get_structures_by_id([structure_id])[0]
		return structure
Beispiel #29
0
from allensdk.api.queries.mouse_connectivity_api import MouseConnectivityApi

mca = MouseConnectivityApi()

# get metadata for all non-Cre experiments
experiments = mca.experiment_source_search(injection_structures='root', transgenic_lines=0)

# download the projection density volume for one of the experiments
pd = mca.download_projection_density('example.nrrd', experiments[0]['id'], resolution=25)

Beispiel #30
0
class ABA(Atlas):
    """
    This class handles interaction with the Allen Brain Atlas datasets and APIs to get structure trees,
    experimental metadata and results, tractography data etc. 
    """
    ignore_regions = ['retina', 'brain', 'fiber tracts', 'grey'] # ignored when rendering

    # useful vars for analysis    
    excluded_regions = ["fiber tracts"]
    resolution = 25

    _root_bounds = [[-17, 13193], 
                   [ 134, 7564], 
                    [486, 10891]]

    _root_midpoint = [np.mean([-17, 13193]), 
                        np.mean([134, 7564]),
                        np.mean([486, 10891])]

    atlas_name = "ABA"
    mesh_format = 'obj'

    base_url = "https://neuroinformatics.nl/HBP/allen-connectivity-viewer/json/streamlines_NNN.json.gz"
    # Used for streamlines

    def __init__(self,  base_dir=None, **kwargs):
        """ 
        Set up file paths and Allen SDKs
        
        :param base_dir: path to directory to use for saving data (default value None)
        :param kwargs: can be used to pass path to individual data folders. See brainrender/Utils/paths_manager.py

        """

        Atlas.__init__(self, base_dir=base_dir, **kwargs)
        self.meshes_folder = self.mouse_meshes # where the .obj mesh for each region is saved

        # get mouse connectivity cache and structure tree
        self.mcc = MouseConnectivityCache(manifest_file=os.path.join(self.mouse_connectivity_cache, "manifest.json"))
        self.structure_tree = self.mcc.get_structure_tree()
        
        # get ontologies API and brain structures sets
        self.oapi = OntologiesApi()
        self.get_structures_sets()

        # get reference space
        self.space = ReferenceSpaceApi()
        self.spacecache = ReferenceSpaceCache(
            manifest=os.path.join(self.annotated_volume_fld, "manifest.json"),  # downloaded files are stored relative to here
            resolution=self.resolution,
            reference_space_key="annotation/ccf_2017"  # use the latest version of the CCF
            )
        self.annotated_volume, _ = self.spacecache.get_annotation_volume()

        # mouse connectivity API [used for tractography]
        self.mca = MouseConnectivityApi()

        # Get tree search api
        self.tree_search = TreeSearchApi()

        # Store all regions metadata [If there's internet connection]
        if self.other_sets is not None: 
            self.regions = self.other_sets["Structures whose surfaces are represented by a precomputed mesh"].sort_values('acronym')
            self.region_acronyms = list(self.other_sets["Structures whose surfaces are represented by a precomputed mesh"].sort_values(
                                                'acronym').acronym.values)

    # ---------------------------------------------------------------------------- #
    #                       Methods to support Scene creation                      #
    # ---------------------------------------------------------------------------- #
    """
        These methods are used by brainrender.scene to populate a scene using
        the Allen brain atlas meshes. They overwrite methods of the base atlas class
    """

    # ------------------------- Getting elements for scene ------------------------- #
    def get_brain_regions(self, brain_regions, VIP_regions=None, VIP_color=None,
                        add_labels=False,
                        colors=None, use_original_color=True, 
                        alpha=None, hemisphere=None, verbose=False, **kwargs):

        """
            Gets brain regions meshes for rendering
            Many parameters can be passed to specify how the regions should be rendered.
            To treat a subset of the rendered regions, specify which regions are VIP. 
            Use the kwargs to specify more detailes on how the regins should be rendered (e.g. wireframe look)

            :param brain_regions: str list of acronyms of brain regions
            :param VIP_regions: if a list of brain regions are passed, these are rendered differently compared to those in brain_regions (Default value = None)
            :param VIP_color: if passed, this color is used for the VIP regions (Default value = None)
            :param colors: str, color of rendered brian regions (Default value = None)
            :param use_original_color: bool, if True, the allen's default color for the region is used.  (Default value = False)
            :param alpha: float, transparency of the rendered brain regions (Default value = None)
            :param hemisphere: str (Default value = None)
            :param add_labels: bool (default False). If true a label is added to each regions' actor. The label is visible when hovering the mouse over the actor
            :param **kwargs: used to determine a bunch of thigs, including the look and location of lables from scene.add_labels
        """
        # Check that the atlas has brain regions data
        if self.region_acronyms is None:
            print(f"The atlas {self.atlas_name} has no brain regions data")
            return

        # Parse arguments
        if VIP_regions is None:
            VIP_regions = brainrender.DEFAULT_VIP_REGIONS
        if VIP_color is None:
            VIP_color = brainrender.DEFAULT_VIP_COLOR
        if alpha is None:
            _alpha = brainrender.DEFAULT_STRUCTURE_ALPHA
        else: _alpha = alpha

        # check that we have a list
        if not isinstance(brain_regions, list):
            brain_regions = [brain_regions]

        # check the colors input is correct
        if colors is not None:
            if isinstance(colors[0], (list, tuple)):
                if not len(colors) == len(brain_regions): 
                    raise ValueError("when passing colors as a list, the number of colors must match the number of brain regions")
                for col in colors:
                    if not check_colors(col): raise ValueError("Invalide colors in input: {}".format(col))
            else:
                if not check_colors(colors): raise ValueError("Invalide colors in input: {}".format(colors))
                colors = [colors for i in range(len(brain_regions))]

        # loop over all brain regions
        actors = {}
        for i, region in enumerate(brain_regions):
            self._check_valid_region_arg(region)

            if region in self.ignore_regions: continue
            if verbose: print("Rendering: ({})".format(region))

            # get the structure and check if we need to download the object file
            if region not in self.region_acronyms:
                print(f"The region {region} doesn't seem to belong to the atlas being used: {self.atlas_name}. Skipping")
                continue

            obj_file = os.path.join(self.meshes_folder, "{}.{}".format(region, self.mesh_format))
            if not self._check_obj_file(region, obj_file):
                print("Could not render {}, maybe we couldn't get the mesh?".format(region))
                continue

            # check which color to assign to the brain region
            if use_original_color:
                color = [x/255 for x in self.get_region_color(region)]
            else:
                if region in VIP_regions:
                    color = VIP_color
                else:
                    if colors is None:
                        color = brainrender.DEFAULT_STRUCTURE_COLOR
                    elif isinstance(colors, list):
                        color = colors[i]
                    else: 
                        color = colors

            if region in VIP_regions:
                alpha = 1
            else:
                alpha = _alpha

            # Load the object file as a mesh and store the actor
            if hemisphere is not None:
                if hemisphere.lower() == "left" or hemisphere.lower() == "right":
                    obj = self.get_region_unilateral(region, hemisphere=hemisphere, color=color, alpha=alpha)
                else:
                    raise ValueError(f'Invalid hemisphere argument: {hemisphere}')
            else:
                obj = load(obj_file, c=color, alpha=alpha)

            if obj is not None:
                actors_funcs.edit_actor(obj, **kwargs)

                actors[region] = obj
            else:
                print(f"Something went wrong while loading mesh data for {region}")

        return actors

    def get_neurons(self, neurons, color=None, display_axon=True, display_dendrites=True,
                alpha=1, neurite_radius=None):
        """
        Gets rendered morphological data of neurons reconstructions downloaded from the
        Mouse Light project at Janelia (or other sources). 
        Accepts neurons argument as:
            - file(s) with morphological data
            - vtkplotter mesh actor(s) of entire neurons reconstructions
            - dictionary or list of dictionary with actors for different neuron parts

        :param neurons: str, list, dict. File(s) with neurons data or list of rendered neurons.
        :param display_axon, display_dendrites: if set to False the corresponding neurite is not rendered
        :param color: default None. Can be:
                - None: each neuron is given a random color
                - color: rbg, hex etc. If a single color is passed all neurons will have that color
                - cmap: str with name of a colormap: neurons are colored based on their sequential order and cmap
                - dict: a dictionary specifying a color for soma, dendrites and axon actors, will be the same for all neurons
                - list: a list of length = number of neurons with either a single color for each neuron
                        or a dictionary of colors for each neuron
        :param alpha: float in range 0,1. Neurons transparency
        :param neurite_radius: float > 0 , radius of tube actor representing neurites
        """

        if not isinstance(neurons, (list, tuple)):
            neurons = [neurons]

        # ------------------------------ Prepare colors ------------------------------ #
        N = len(neurons)
        colors = dict(
            soma = None,
            axon = None,
            dendrites = None,
        )

        # If no color is passed, get random colors
        if color is None:
            cols = get_random_colors(N)
            colors = dict(
                soma = cols.copy(),
                axon = cols.copy(),
                dendrites = cols.copy(),)
        else:
            if isinstance(color, str):
                # Deal with a a cmap being passed
                if color in _mapscales_cmaps:
                    cols = [colorMap(n, name=color, vmin=-2, vmax=N+2) for n in np.arange(N)]
                    colors = dict(
                        soma = cols.copy(),
                        axon = cols.copy(),
                        dendrites = cols.copy(),)

                else:
                    # Deal with a single color being passed
                    cols = [getColor(color) for n in np.arange(N)]
                    colors = dict(
                        soma = cols.copy(),
                        axon = cols.copy(),
                        dendrites = cols.copy(),)
            elif isinstance(color, dict):
                # Deal with a dictionary with color for each component
                if not 'soma' in color.keys():
                    raise ValueError(f"When passing a dictionary as color argument, \
                                                soma should be one fo the keys: {color}")
                dendrites_color = color.pop('dendrites', color['soma'])
                axon_color = color.pop('axon', color['soma'])

                colors = dict(
                        soma = [color['soma'] for n in np.arange(N)],
                        axon = [axon_color for n in np.arange(N)],
                        dendrites = [dendrites_color for n in np.arange(N)],)
                        
            elif isinstance(color, (list, tuple)):
                # Check that the list content makes sense
                if len(color) != N:
                    raise ValueError(f"When passing a list of color arguments, the list length"+
                                f" ({len(color)}) should match the number of neurons ({N}).")
                if len(set([type(c) for c in color])) > 1:
                    raise ValueError(f"When passing a list of color arguments, all list elements"+
                                " should have the same type (e.g. str or dict)")

                if isinstance(color[0], dict):
                    # Deal with a list of dictionaries
                    soma_colors, dendrites_colors, axon_colors = [], [], []

                    for col in colors:
                        if not 'soma' in col.keys():
                            raise ValueError(f"When passing a dictionary as col argument, \
                                                        soma should be one fo the keys: {col}")
                        dendrites_colors.append(col.pop('dendrites', col['soma']))
                        axon_colors.append(col.pop('axon', col['soma']))
                        soma_colors.append(col['soma'])

                    colors = dict(
                        soma = soma_colors,
                        axon = axon_colors,
                        dendrites = dendrites_colors,)

                else:
                    # Deal with a list of colors
                    colors = dict(
                        soma = color.copy(),
                        axon = color.copy(),
                        dendrites = color.copy(),)
            else:
                raise ValueError(f"Color argument passed is not valid. Should be a \
                                        str, dict, list or None, not {type(color)}:{color}")

        # Check colors, if everything went well we should have N colors per entry
        for k,v in colors.items():
            if len(v) != N:
                raise ValueError(f"Something went wrong while preparing colors. Not all \
                                entries have right length. We got: {colors}")



        # ---------------------------------- Render ---------------------------------- #
        _neurons_actors = []
        for neuron in neurons:
            neuron_actors = {'soma':None, 'dendrites':None, 'axon': None}
            
            # Deal with neuron as filepath
            if isinstance(neuron, str):
                if os.path.isfile(neuron):
                    if neuron.endswith('.swc'):
                        neuron_actors, _ = get_neuron_actors_with_morphapi(swcfile=neuron, neurite_radius=neurite_radius)
                    else:
                        raise NotImplementedError('Currently we can only parse morphological reconstructions from swc files')
                else:
                    raise ValueError(f"Passed neruon {neuron} is not a valid input. Maybe the file doesn't exist?")
            
            # Deal with neuron as single actor
            elif isinstance(neuron, Actor):
                # A single actor was passed, maybe it's the entire neuron
                neuron_actors['soma'] = neuron # store it as soma anyway
                pass

            # Deal with neuron as dictionary of actor
            elif isinstance(neuron, dict):
                neuron_actors['soma'] = neuron.pop('soma', None)
                neuron_actors['axon'] = neuron.pop('axon', None)

                # Get dendrites actors
                if 'apical_dendrites' in neuron.keys() or 'basal_dendrites' in neuron.keys():
                    if 'apical_dendrites' not in neuron.keys():
                        neuron_actors['dendrites'] = neuron['basal_dendrites']
                    elif 'basal_dendrites' not in neuron.keys():
                        neuron_actors['dendrites'] = neuron['apical_dendrites']
                    else:
                        neuron_ctors['dendrites'] = merge(neuron['apical_dendrites'], neuron['basal_dendrites'])
                else:
                    neuron_actors['dendrites'] = neuron.pop('dendrites', None)
            
            # Deal with neuron as instance of Neuron from morphapi
            elif isinstance(neuron, Neuron):
                neuron_actors, _ = get_neuron_actors_with_morphapi(neuron=neuron)                
            # Deal with other inputs
            else:
                raise ValueError(f"Passed neuron {neuron} is not a valid input")

            # Check that we don't have anything weird in neuron_actors
            for key, act in neuron_actors.items():
                if act is not None:
                    if not isinstance(act, Actor):
                        raise ValueError(f"Neuron actor {key} is {act.__type__} but should be a vtkplotter Mesh. Not: {act}")

            if not display_axon:
                neuron_actors['axon'] = None
            if not display_dendrites:
                neuron_actors['dendrites'] = None
            _neurons_actors.append(neuron_actors)

        # Color actors
        for n, neuron in enumerate(_neurons_actors):
            if neuron['axon'] is not None:
                neuron['axon'].c(colors['axon'][n])
            neuron['soma'].c(colors['soma'][n])
            if neuron['dendrites'] is not None:
                neuron['dendrites'].c(colors['dendrites'][n])

        # Return
        if len(_neurons_actors) == 1:
            return _neurons_actors[0], None
        elif not _neurons_actors:
            return None, None
        else:
            return _neurons_actors, None

    def get_tractography(self, tractography, color=None,  color_by="manual", others_alpha=1, verbose=True,
                        VIP_regions=[], VIP_color=None, others_color="white", include_all_inj_regions=False,
                        extract_region_from_inj_coords=False, display_injection_volume=True):
        """
        Renders tractography data and adds it to the scene. A subset of tractography data can receive special treatment using the  with VIP regions argument:
        if the injection site for the tractography data is in a VIP regions, this is colored differently.

        :param tractography: list of dictionaries with tractography data
        :param color: color of rendered tractography data

        :param color_by: str, specifies which criteria to use to color the tractography (Default value = "manual")
        :param others_alpha: float (Default value = 1)
        :param verbose: bool (Default value = True)
        :param VIP_regions: list of brain regions with VIP treatement (Default value = [])
        :param VIP_color: str, color to use for VIP data (Default value = None)
        :param others_color: str, color for not VIP data (Default value = "white")
        :param include_all_inj_regions: bool (Default value = False)
        :param extract_region_from_inj_coords: bool (Default value = False)
        :param display_injection_volume: float, if True a spehere is added to display the injection coordinates and volume (Default value = True)
        """

        # check argument
        if not isinstance(tractography, list):
            if isinstance(tractography, dict):
                tractography = [tractography]
            else:
                raise ValueError("the 'tractography' variable passed must be a list of dictionaries")
        else:
            if not isinstance(tractography[0], dict):
                raise ValueError("the 'tractography' variable passed must be a list of dictionaries")

        if not isinstance(VIP_regions, list):
            raise ValueError("VIP_regions should be a list of acronyms")

        # check coloring mode used and prepare a list COLORS to use for coloring stuff
        if color_by == "manual":
            # check color argument
            if color is None:
                color = TRACT_DEFAULT_COLOR
                COLORS = [color for i in range(len(tractography))]
            elif isinstance(color, list):
                if not len(color) == len(tractography):
                    raise ValueError("If a list of colors is passed, it must have the same number of items as the number of tractography traces")
                else:
                    for col in color:
                        if not check_colors(col): raise ValueError("Color variable passed to tractography is invalid: {}".format(col))

                    COLORS = color
            else:
                if not check_colors(color):
                    raise ValueError("Color variable passed to tractography is invalid: {}".format(color))
                else:
                    COLORS = [color for i in range(len(tractography))]

        elif color_by == "region":
            COLORS = [self.get_region_color(t['structure-abbrev']) for t in tractography]

        elif color_by == "target_region":
            if VIP_color is not None:
                if not check_colors(VIP_color) or not check_colors(others_color):
                    raise ValueError("Invalid VIP or other color passed")
                try:
                    if include_all_inj_regions:
                        COLORS = [VIP_color if is_any_item_in_list( [x['abbreviation'] for x in t['injection-structures']], VIP_regions)\
                            else others_color for t in tractography]
                    else:
                        COLORS = [VIP_color if t['structure-abbrev'] in VIP_regions else others_color for t in tractography]
                except:
                    raise ValueError("Something went wrong while getting colors for tractography")
            else:
                COLORS = [self.get_region_color(t['structure-abbrev']) if t['structure-abbrev'] in VIP_regions else others_color for t in tractography]
        else:
            raise ValueError("Unrecognised 'color_by' argument {}".format(color_by))

        # add actors to represent tractography data
        actors, structures_acronyms = [], []
        if VERBOSE and verbose:
            print("Structures found to be projecting to target: ")

        # Loop over injection experiments
        for i, (t, color) in enumerate(zip(tractography, COLORS)):
            # Use allen metadata
            if include_all_inj_regions:
                inj_structures = [x['abbreviation'] for x in t['injection-structures']]
            else:
                inj_structures = [self.get_structure_parent(t['structure-abbrev'])['acronym']]

            if VERBOSE and verbose and not is_any_item_in_list(inj_structures, structures_acronyms):
                print("     -- ({})".format(t['structure-abbrev']))
                structures_acronyms.append(t['structure-abbrev'])

            # get tractography points and represent as list
            if color_by == "target_region" and not is_any_item_in_list(inj_structures, VIP_regions):
                alpha = others_alpha
            else:
                alpha = TRACTO_ALPHA

            if alpha == 0:
                continue # skip transparent ones

            # check if we need to manually check injection coords
            if extract_region_from_inj_coords:
                try:
                    region = self.get_structure_from_coordinates(t['injection-coordinates'], 
                                                            just_acronym=False)
                    if region is None: continue
                    inj_structures = [self.get_structure_parent(region['acronym'])['acronym']]
                except:
                    raise ValueError(self.get_structure_from_coordinates(t['injection-coordinates'], 
                                                            just_acronym=False))
                if inj_structures is None: continue
                elif isinstance(extract_region_from_inj_coords, list):
                    # check if injection coord are in one of the brain regions in list, otherwise skip
                    if not is_any_item_in_list(inj_structures, extract_region_from_inj_coords):
                        continue

            # represent injection site as sphere
            if display_injection_volume:
                actors.append(shapes.Sphere(pos=t['injection-coordinates'],
                                c=color, r=INJECTION_VOLUME_SIZE*t['injection-volume'], alpha=TRACTO_ALPHA))

            points = [p['coord'] for p in t['path']]
            actors.append(shapes.Tube(points, r=TRACTO_RADIUS, c=color, alpha=alpha, res=TRACTO_RES))

        return actors

    def get_streamlines(self, sl_file, *args, colorby=None, color_each=False,  **kwargs):
        """
        Render streamline data downloaded from https://neuroinformatics.nl/HBP/allen-connectivity-viewer/streamline-downloader.html

        :param sl_file: path to JSON file with streamliens data [or list of files]
        :param colorby: str,  criteria for how to color the streamline data (Default value = None)
        :param color_each: bool, if True, the streamlines for each injection is colored differently (Default value = False)
        :param *args:
        :param **kwargs:

        """
        color = None
        if not color_each:
            if colorby is not None:
                try:
                    color = self.structure_tree.get_structures_by_acronym([colorby])[0]['rgb_triplet']
                    if "color" in kwargs.keys():
                        del kwargs["color"]
                except:
                    raise ValueError("Could not extract color for region: {}".format(colorby))
        else:
            if colorby is not None:
                color = kwargs.pop("color", None)
                try:
                    get_n_shades_of(color, 1)
                except:
                    raise ValueError("Invalide color argument: {}".format(color))

        if not isinstance(sl_file, (list, tuple)):
            sl_file = [sl_file]

        actors = []
        if isinstance(sl_file[0], (str, pd.DataFrame)): # we have a list of files to add
            for slf in tqdm(sl_file):
                if not color_each:
                    if color is not None:
                        if isinstance(slf, str):
                            streamlines = parse_streamline(filepath=slf, *args, color=color, **kwargs)
                        else:
                            streamlines = parse_streamline(data=slf, *args, color=color, **kwargs)
                    else:
                        if isinstance(slf, str):
                            streamlines = parse_streamline(filepath=slf, *args, **kwargs)
                        else:
                            streamlines = parse_streamline(data=slf,  *args, **kwargs)
                else:
                    if color is not None:
                        col = get_n_shades_of(color, 1)[0]
                    else:
                        col = get_random_colors(n_colors=1)
                    if isinstance(slf, str):
                        streamlines = parse_streamline(filepath=slf, color=col, *args, **kwargs)
                    else:
                        streamlines = parse_streamline(data= slf, color=col, *args, **kwargs)
                actors.extend(streamlines)
        else:
            raise ValueError("unrecognized argument sl_file: {}".format(sl_file))

        return actors
     
    def get_injection_sites(self, experiments, color=None):
        """
        Creates Spherse at the location of injections with a volume proportional to the injected volume

        :param experiments: list of dictionaries with tractography data
        :param color:  (Default value = None)

        """
        # check arguments
        if not isinstance(experiments, list):
            raise ValueError("experiments must be a list")
        if not isinstance(experiments[0], dict):
            raise ValueError("experiments should be a list of dictionaries")

        #c= cgeck color
        if color is None:
            color = INJECTION_DEFAULT_COLOR

        injection_sites = []
        for exp in experiments:
            injection_sites.append(shapes.Sphere(pos=(exp["injection_x"], exp["injection_y"], exp["injection_z"]),
                    r = INJECTION_VOLUME_SIZE*exp["injection_volume"]*3,
                    c=color
                    ))

        return injection_sites

        
    # ---------------------------------------------------------------------------- #
    #                          STRUCTURE TREE INTERACTION                          #
    # ---------------------------------------------------------------------------- #

    # ------------------------- Get/Print structures sets ------------------------ #

    def get_structures_sets(self):
        """ 
        Get the Allen's structure sets.
        """
        summary_structures = self.structure_tree.get_structures_by_set_id([167587189])  # main summary structures
        summary_structures = [s for s in summary_structures if s["acronym"] not in self.excluded_regions]
        self.structures = pd.DataFrame(summary_structures)

        # Other structures sets
        try:
            all_sets = pd.DataFrame(self.oapi.get_structure_sets())
        except:
            print("Could not retrieve data, possibly because there is no internet connection. Limited functionality available.")
        else:
            sets = ["Summary structures of the pons", "Summary structures of the thalamus", 
                        "Summary structures of the hypothalamus", "List of structures for ABA Fine Structure Search",
                        "Structures representing the major divisions of the mouse brain", "Summary structures of the midbrain", "Structures whose surfaces are represented by a precomputed mesh"]
            self.other_sets = {}
            for set_name in sets:
                set_id = all_sets.loc[all_sets.description == set_name].id.values[0]
                self.other_sets[set_name] = pd.DataFrame(self.structure_tree.get_structures_by_set_id([set_id]))

            self.all_avaliable_meshes = sorted(self.other_sets["Structures whose surfaces are represented by a precomputed mesh"].acronym.values)

    def print_structures_list_to_text(self):
        """ 
        Saves the name of every brain structure for which a 3d mesh (.obj file) is available in a text file.
        """
        s = self.other_sets["Structures whose surfaces are represented by a precomputed mesh"].sort_values('acronym')
        with open('all_regions.txt', 'w') as o:
            for acr, name in zip(s.acronym.values, s['name'].values):
                o.write("({}) -- {}\n".format(acr, name))

    def print_structures(self):
        """ 
        Prints the name of every structure in the structure tree to the console.
        """
        acronyms, names = self.structures.acronym.values, self.structures['name'].values
        sort_idx = np.argsort(acronyms)
        acronyms, names = acronyms[sort_idx], names[sort_idx]
        [print("({}) - {}".format(a, n)) for a,n in zip(acronyms, names)]

    # -------------------------- Parents and descendants ------------------------- #
    def get_structure_ancestors(self, regions, ancestors=True, descendants=False):
        """
        Get's the ancestors of the region(s) passed as arguments

        :param regions: str, list of str with acronums of regions of interest
        :param ancestors: if True, returns the ancestors of the region  (Default value = True)
        :param descendants: if True, returns the descendants of the region (Default value = False)

        """

        if not isinstance(regions, list):
            struct_id = self.structure_tree.get_structures_by_acronym([regions])[0]['id']
            return pd.DataFrame(self.tree_search.get_tree('Structure', struct_id, ancestors=ancestors, descendants=descendants))
        else:
            ancestors = []
            for region in regions:
                struct_id = self.structure_tree.get_structures_by_acronym([region])[0]['id']
                ancestors.append(pd.DataFrame(self.tree_search.get_tree('Structure', struct_id, ancestors=ancestors, descendants=descendants)))
            return ancestors

    def get_structure_descendants(self, regions):
        return self.get_structure_ancestors(regions, ancestors=False, descendants=True)

    def get_structure_parent(self, acronyms):
        """
        Gets the parent of a brain region (or list of regions) from the hierarchical structure of the
        Allen Brain Atals.

        :param acronyms: list of acronyms of brain regions.

        """
        if not isinstance(acronyms, list):
            self._check_valid_region_arg(acronyms)
            s = self.structure_tree.get_structures_by_acronym([acronyms])[0]
            if s['id'] in self.structures.id.values:
                return s
            else:
                return self.get_structure_ancestors(s['acronym']).iloc[-1]
        else:
            parents = []
            for region in acronyms:
                self._check_valid_region_arg(region)
                s = self.structure_tree.get_structures_by_acronym(acronyms)[0]

                if s['id'] in self.structures.id.values:
                    parents.append(s)
                parents.append(self.get_structure_ancestors(s['acronym']).iloc[-1])
            return parents

    
    # ---------------------------------------------------------------------------- #
    #                                     UTILS                                    #
    # ---------------------------------------------------------------------------- #
    def get_hemisphere_from_point(self, point):
        if point[2] < self._root_midpoint[2]:
            return 'left'
        else:
            return 'right'

    def mirror_point_across_hemispheres(self, point):
        delta = point[2] - self._root_midpoint[2]
        point[2] = self._root_midpoint[2] - delta
        return point

    def get_region_color(self, regions):
        """
        Gets the RGB color of a brain region from the Allen Brain Atlas.

        :param regions:  list of regions acronyms.

        """
        if not isinstance(regions, list):
            return self.structure_tree.get_structures_by_acronym([regions])[0]['rgb_triplet']
        else:
            return [self.structure_tree.get_structures_by_acronym([r])[0]['rgb_triplet'] for r in regions]

    def _check_obj_file(self, region, obj_file):
        """
        If the .obj file for a brain region hasn't been downloaded already, this function downloads it and saves it.

        :param region: string, acronym of brain region
        :param obj_file: path to .obj file to save downloaded data.

        """
        # checks if the obj file has been downloaded already, if not it takes care of downloading it
        if not os.path.isfile(obj_file):
            try:
                if isinstance(region, dict):
                    region = region['acronym']
                structure = self.structure_tree.get_structures_by_acronym([region])[0]
            except Exception as e:
                raise ValueError(f'Could not find region with name {region}, got error: {e}')

            try:
                self.space.download_structure_mesh(structure_id = structure["id"],
                                                    ccf_version ="annotation/ccf_2017",
                                                    file_name=obj_file)
                return True
            except:
                print("Could not get mesh for: {}".format(obj_file))
                return False
        else: return True

    def _get_structure_mesh(self, acronym,   **kwargs):
        """
        Fetches the mesh for a brain region from the Allen Brain Atlas SDK.

        :param acronym: string, acronym of brain region
        :param **kwargs:

        """
        structure = self.structure_tree.get_structures_by_acronym([acronym])[0]
        obj_path = os.path.join(self.mouse_meshes, "{}.obj".format(acronym))

        if self._check_obj_file(structure, obj_path):
            mesh = load_mesh_from_file(obj_path, **kwargs)
            return mesh
        else:
            return None

    def get_region_unilateral(self, region, hemisphere="both", color=None, alpha=None):
        """
        Regions meshes are loaded with both hemispheres' meshes by default.
        This function splits them in two.

        :param region: str, actors of brain region
        :param hemisphere: str, which hemisphere to return ['left', 'right' or 'both'] (Default value = "both")
        :param color: color of each side's mesh. (Default value = None)
        :param alpha: transparency of each side's mesh.  (Default value = None)

        """
        if color is None: color = ROOT_COLOR
        if alpha is None: alpha = ROOT_ALPHA
        bilateralmesh = self._get_structure_mesh(region, c=color, alpha=alpha)

        if bilateralmesh is None:
            print(f'Failed to get mesh for {region}, returning None')
            return None

        com = bilateralmesh.centerOfMass()   # this will always give a point that is on the midline
        cut = bilateralmesh.cutWithPlane(origin=com, normal=(0, 0, 1))

        right = bilateralmesh.cutWithPlane( origin=com, normal=(0, 0, 1))
        
        # left is the mirror right # WIP
        com = self.get_region_CenterOfMass('root', unilateral=False)[2]
        left = actors_funcs.mirror_actor_at_point(right.clone(), com, axis='x')

        if hemisphere == "both":
            return left, right
        elif hemisphere == "left": 
            return left 
        else:
            return right


    @staticmethod
    def _check_valid_region_arg(region):
        """
        Check that the string passed is a valid brain region name.

        :param region: string, acronym of a brain region according to the Allen Brain Atlas.

        """
        if not isinstance(region, int) and not isinstance(region, str):
            raise ValueError("region must be a list, integer or string, not: {}".format(type(region)))
        else:
            return True

    def get_hemispere_from_point(self, p0):
        if p0[2] > self._root_midpoint[2]:
            return 'right'
        else:
            return 'left'

    def get_structure_from_coordinates(self, p0, just_acronym=True):
        """
        Given a point in the Allen Mouse Brain reference space, returns the brain region that the point is in. 

        :param p0: list of floats with XYZ coordinates. 

        """
        voxel = np.round(np.array(p0) / self.resolution).astype(int)
        try:
            structure_id = self.annotated_volume[voxel[0], voxel[1], voxel[2]]
        except:
            return None

        # Each voxel in the annotation volume is annotated as specifically as possible
        structure = self.structure_tree.get_structures_by_id([structure_id])[0]
        if structure is not None:
            if just_acronym:
                return structure['acronym']
        return structure
    
    def get_colors_from_coordinates(self, p0):
        """
            Given a point or a list of points returns a list of colors where 
            each item is the color of the brain region each point is in
        """
        if isinstance(p0[0], (float, int)):
            struct = self.get_structure_from_coordinates(p0, just_acronym=False)
            if struct is not None:
                return struct['rgb_triplet']
            else:
                return None
        else:
            structures = [self.get_structure_from_coordinates(p, just_acronym=False) for p in p0]
            colors = [struct['rgb_triplet'] if struct is not None else None 
                            for struct in structures]
            return colors 


    # ---------------------------------------------------------------------------- #
    #                             TRACTOGRAPHY FETCHING                            #
    # ---------------------------------------------------------------------------- #
    def get_projection_tracts_to_target(self, p0=None, **kwargs):
        """
        Gets tractography data for all experiments whose projections reach the brain region or location of iterest.
        
        :param p0: list of 3 floats with XYZ coordinates of point to be used as seed (Default value = None)
        :param **kwargs: 
        """

        # check args
        if p0 is None:
            raise ValueError("Please pass coordinates")
        elif isinstance(p0, np.ndarray):
            p0 = list(p0)
        elif not isinstance(p0, (list, tuple)):
            raise ValueError("Invalid argument passed (p0): {}".format(p0))
        
        p0 = [np.int(p) for p in p0]
        tract = self.mca.experiment_spatial_search(seed_point=p0, **kwargs)

        if isinstance(tract, str): 
            raise ValueError('Something went wrong with query, query error message:\n{}'.format(tract))
        else:
            return tract


    # ---------------------------------------------------------------------------- #
    #                             STREAMLINES FETCHING                             #
    # ---------------------------------------------------------------------------- #
    def download_streamlines_for_region(self, region, *args, **kwargs):
        """
            Using the Allen Mouse Connectivity data and corresponding API, this function finds expeirments whose injections
            were targeted to the region of interest and downloads the corresponding streamlines data. By default, experiements
            are selected for only WT mice and onl when the region was the primary injection target. Look at "ABA.experiments_source_search"
            to see how to change this behaviour.

            :param region: str with region to use for research
            :param *args: arguments for ABA.experiments_source_search
            :param **kwargs: arguments for ABA.experiments_source_search

        """
        # Get experiments whose injections were targeted to the region
        region_experiments = experiments_source_search(self.mca, region, *args, **kwargs)
        try:
            return download_streamlines(region_experiments.id.values, streamlines_folder=self.streamlines_cache)
        except:
            print(f"Could not download streamlines for region {region}")
            return [], [] # <- there were no experiments in the target region 

    def download_streamlines_to_region(self, p0, *args,  mouse_line = "wt", **kwargs):
        """
            Using the Allen Mouse Connectivity data and corresponding API, this function finds injection experiments
            which resulted in fluorescence being found in the target point, then downloads the streamlines data.

            :param p0: list of floats with XYZ coordinates
            :param mouse_line: str with name of the mouse line to use(Default value = "wt")
            :param *args: 
            :param **kwargs: 

        """
        experiments = pd.DataFrame(self.get_projection_tracts_to_target(p0=p0))
        if mouse_line == "wt":
            experiments = experiments.loc[experiments["transgenic-line"] == ""]
        else:
            if not isinstance(mouse_line, list):
                experiments = experiments.loc[experiments["transgenic-line"] == mouse_line]
            else:
                raise NotImplementedError("ops, you've found a bug!. For now you can only pass one mouse line at the time, sorry.")
        return download_streamlines(experiments.id.values, streamlines_folder=self.streamlines_cache)
Beispiel #31
0
from allensdk.core.mouse_connectivity_cache import MouseConnectivityCache
from allensdk.api.queries.mouse_connectivity_api import MouseConnectivityApi
import pdb
mca = MouseConnectivityApi()
print mca
# get metadata for all non-Cre experiments
#experiments = mca.experiment_source_search(injection_structures='root', transgenic_lines=0)
# download the projection density volume for one of the experiments
#pd = mca.download_projection_density('example.nrrd', experiments[0]['id'], resolution=25)

#enter selected projection numbers here, e.g. 167794131, 297231636, 287495026
id1 = 167794131
id2 = 297231636
id3 = 272736450
# import allensdk python api
from allensdk.api.queries.mouse_connectivity_api import MouseConnectivityApi
mca = MouseConnectivityApi()
# get metadata for all experiments
experiments = mca.experiment_source_search(injection_structures=['VIS','PTLp','RSP'])
# find selected experiments and format filenames, **Use the %paste magic function if pasting into ipython
for i in range(len(experiments)):
    if (experiments[i]['id'] == id1):
        fn1 = str(experiments[i]['injection-structures'][0]['abbreviation']) + '_' + str(experiments[i]['id']) + '_' + str(experiments[i]['transgenic-line'])
        print fn1
    if (experiments[i]['id'] == id2):
        fn2 = str(experiments[i]['injection-structures'][0]['abbreviation']) + '_' + str(experiments[i]['id']) + '_' + str(experiments[i]['transgenic-line'])
        print fn2
    if (experiments[i]['id'] == id3):
        fn3 = str(experiments[i]['injection-structures'][0]['abbreviation']) + '_' + str(experiments[i]['id']) + '_' + str(experiments[i]['transgenic-line'])
        print fn3
# download selected experiment projection density files at 25 um resolution
Beispiel #32
0
class ExperimentSectionData(object):
    def __init__(self,
                 mcc,
                 experiment_id,
                 output_dir,
                 anno,
                 meta,
                 rsp,
                 logger,
                 zoom=8,
                 remove_transform_data=True):
        self.remove_transform_data = remove_transform_data
        self.output_dir = output_dir
        os.makedirs(output_dir, exist_ok=True)
        self.mcc = mcc
        self.mapi = MouseConnectivityApi()
        self.anno, self.meta = anno, meta
        self.rsp = rsp
        self.zoom = 8 - zoom
        self.id = experiment_id
        assert zoom >= 0
        self.details = self.mapi.get_experiment_detail(self.id)
        image_resolution = self.details[0]['sub_images'][0]['resolution']
        self.two_d = 1.0 / image_resolution
        self.size = self.mcc.resolution * self.two_d / (2**self.zoom)
        self.dims = (self.details[0]['sub_images'][0]['height'] //
                     (2**self.zoom),
                     self.details[0]['sub_images'][0]['width'] //
                     (2**self.zoom))
        self.root_points = np.array(np.where(self.anno != 0)).T
        self.logger = logger
        self.logger.info(
            f"Initializing displacement transform data for {self.id}...")
        self.__init_transform__()
        self.logger.info(
            f"Performing displacement transformation for {self.id}...")
        self.__init_transformed_points__()

    def __init_transform__(self):
        temp = sitk.ReadImage(f'{self.output_dir}/dfmfld.mhd',
                              sitk.sitkVectorFloat64)
        dfmfld_transform = sitk.DisplacementFieldTransform(temp)

        temp = self.mcc.get_affine_parameters(
            self.id,
            direction='trv',
            file_name=f'{self.output_dir}/aff_param.txt')
        aff_trans = sitk.AffineTransform(3)
        aff_trans.SetParameters(temp.flatten())

        self.transform = sitk.Transform(3, sitk.sitkComposite)
        self.transform.AddTransform(aff_trans)
        self.transform.AddTransform(dfmfld_transform)

    def __init_transformed_points__(self):
        self.transformed_points = self.__transform_points__(
            self.transform,
            self.root_points.astype(float) * self.mcc.resolution)
        self.transformed_points[..., :2] *= self.two_d / (2**self.zoom)
        self.transformed_points[..., 2] /= 100
        self.next_points = self.transformed_points.copy()
        self.next_points[..., :2] += self.size
        self.transformed_points = np.round(self.transformed_points).astype(int)
        self.next_points = np.round(self.next_points).astype(int)

    @staticmethod
    def __transform_points__(composite_transform, points):
        return np.array(list(map(composite_transform.TransformPoint, points)))

    def create_section_data(self):
        first_section = np.min(self.transformed_points[..., -1])
        last_section = np.max(self.transformed_points[..., -1])
        result = np.zeros((*self.dims, last_section + 1), dtype=np.int32)

        self.logger.info(f"Transferring segmentation data for {self.id}...")
        transformed_indices = tuple(
            self.transformed_points.squeeze().T.tolist())
        next_indices = tuple(self.next_points.squeeze().T.tolist())
        original_indices = tuple(self.root_points.squeeze().T.tolist())
        result[transformed_indices[1], transformed_indices[0],
               transformed_indices[2]] = self.anno[original_indices]
        result[next_indices[1], next_indices[0],
               next_indices[2]] = self.anno[original_indices]

        structures = np.unique(result).tolist()
        structures = list(set(structures).difference({0}))
        sorted_indx = np.argsort(
            np.array(
                list(
                    map(lambda x: len(x),
                        self.rsp.structure_tree.ancestor_ids(structures)))))
        structures = np.array(structures)[sorted_indx].tolist()

        new_result = np.zeros_like(result)

        self.logger.info(f"Filling holes for {self.id}...")
        for i, struct in enumerate(structures):
            mask = result == struct
            mask = ndi.binary_closing(
                ndi.binary_fill_holes(mask).astype(np.int32)).astype(np.int32)
            new_result[mask != 0] = struct

        self.logger.info(f"Saving segmentation data for {self.id}...")
        np.savez_compressed(f"{self.output_dir}/{self.id}-sections",
                            new_result)

    def cleanup(self):
        if self.remove_transform_data:
            os.remove(f'{self.output_dir}/dfmfld.mhd')
            os.remove(f'{self.output_dir}/dfmfld.raw')
            os.remove(f'{self.output_dir}/aff_param.txt')
Beispiel #33
0
class MouseConnectivityCache(ReferenceSpaceCache):
    """
    Cache class for storing and accessing data related to the adult mouse
    Connectivity Atlas.  By default, this class will cache any downloaded
    metadata or files in well known locations defined in a manifest file.
    This behavior can be disabled.

    Attributes
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    api: MouseConnectivityApi instance
        Used internally to make API queries.

    Parameters
    ----------

    resolution: int
        Resolution of grid data to be downloaded when accessing projection volume,
        the annotation volume, and the annotation volume.  Must be one of (10, 25,
        50, 100).  Default is 25.

    ccf_version: string
        Desired version of the Common Coordinate Framework.  This affects the annotation
        volume (get_annotation_volume) and structure masks (get_structure_mask).
        Must be one of (MouseConnectivityApi.CCF_2015, MouseConnectivityApi.CCF_2016).
        Default: MouseConnectivityApi.CCF_2016

    cache: boolean
        Whether the class should save results of API queries to locations specified
        in the manifest file.  Queries for files (as opposed to metadata) must have a
        file location.  If caching is disabled, those locations must be specified
        in the function call (e.g. get_projection_density(file_name='file.nrrd')).

    manifest_file: string
        File name of the manifest to be read.  Default is "mouse_connectivity_manifest.json".

    """

    PROJECTION_DENSITY_KEY = 'PROJECTION_DENSITY'
    INJECTION_DENSITY_KEY = 'INJECTION_DENSITY'
    INJECTION_FRACTION_KEY = 'INJECTION_FRACTION'
    DATA_MASK_KEY = 'DATA_MASK'
    STRUCTURE_UNIONIZES_KEY = 'STRUCTURE_UNIONIZES'
    EXPERIMENTS_KEY = 'EXPERIMENTS'
    DEFORMATION_FIELD_HEADER_KEY = 'DEFORMATION_FIELD_HEADER'
    DEFORMATION_FIELD_VOXEL_KEY = 'DEFORMATION_FIELD_VOXELS'
    ALIGNMENT3D_KEY = 'ALIGNMENT3D'

    MANIFEST_VERSION = 1.3

    SUMMARY_STRUCTURE_SET_ID = 167587189
    DEFAULT_STRUCTURE_SET_IDS = tuple([SUMMARY_STRUCTURE_SET_ID])

    DFMFLD_RESOLUTIONS = (25, )

    @property
    def default_structure_ids(self):

        if not hasattr(self, '_default_structure_ids'):
            tree = self.get_structure_tree()
            default_structures = tree.get_structures_by_set_id(
                MouseConnectivityCache.DEFAULT_STRUCTURE_SET_IDS)
            self._default_structure_ids = [
                st['id'] for st in default_structures
            ]

        return self._default_structure_ids

    def __init__(self,
                 resolution=None,
                 cache=True,
                 manifest_file=None,
                 ccf_version=None,
                 base_uri=None,
                 version=None):

        if manifest_file is None:
            manifest_file = get_default_manifest_file('mouse_connectivity')

        if version is None:
            version = self.MANIFEST_VERSION

        if resolution is None:
            resolution = MouseConnectivityApi.VOXEL_RESOLUTION_25_MICRONS

        if ccf_version is None:
            ccf_version = MouseConnectivityApi.CCF_VERSION_DEFAULT

        super(MouseConnectivityCache,
              self).__init__(resolution,
                             reference_space_key=ccf_version,
                             cache=cache,
                             manifest=manifest_file,
                             version=version)

        self.api = MouseConnectivityApi(base_uri=base_uri)

    def get_projection_density(self, experiment_id, file_name=None):
        """
        Read a projection density volume for a single experiment.  Download it
        first if it doesn't exist.  Projection density is the proportion of
        of projecting pixels in a grid voxel in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.PROJECTION_DENSITY_KEY,
                                        experiment_id, self.resolution)

        self.api.download_projection_density(file_name,
                                             experiment_id,
                                             self.resolution,
                                             strategy='lazy')

        return nrrd.read(file_name)

    def get_injection_density(self, experiment_id, file_name=None):
        """
        Read an injection density volume for a single experiment. Download it
        first if it doesn't exist.  Injection density is the proportion of
        projecting pixels in a grid voxel only including pixels that are
        part of the injection site in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.INJECTION_DENSITY_KEY,
                                        experiment_id, self.resolution)
        self.api.download_injection_density(file_name,
                                            experiment_id,
                                            self.resolution,
                                            strategy='lazy')

        return nrrd.read(file_name)

    def get_injection_fraction(self, experiment_id, file_name=None):
        """
        Read an injection fraction volume for a single experiment. Download it
        first if it doesn't exist.  Injection fraction is the proportion of
        pixels in the injection site in a grid voxel in [0,1].

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.INJECTION_FRACTION_KEY,
                                        experiment_id, self.resolution)
        self.api.download_injection_fraction(file_name,
                                             experiment_id,
                                             self.resolution,
                                             strategy='lazy')

        return nrrd.read(file_name)

    def get_data_mask(self, experiment_id, file_name=None):
        """
        Read a data mask volume for a single experiment. Download it
        first if it doesn't exist.  Data mask is a binary mask of
        voxels that have valid data.  Only use valid data in analysis!

        Parameters
        ----------

        experiment_id: int
            ID of the experiment to download/read.  This corresponds to
            section_data_set_id in the API.

        file_name: string
            File name to store the template volume.  If it already exists,
            it will be read from this file.  If file_name is None, the
            file_name will be pulled out of the manifest.  Default is None.

        """

        file_name = self.get_cache_path(file_name, self.DATA_MASK_KEY,
                                        experiment_id, self.resolution)
        self.api.download_data_mask(file_name,
                                    experiment_id,
                                    self.resolution,
                                    strategy='lazy')

        return nrrd.read(file_name)

    def get_experiments(self,
                        dataframe=False,
                        file_name=None,
                        cre=None,
                        injection_structure_ids=None):
        """
        Read a list of experiments that match certain criteria.  If caching is enabled,
        this will save the whole (unfiltered) list of experiments to a file.

        Parameters
        ----------

        dataframe: boolean
            Return the list of experiments as a Pandas DataFrame.  If False,
            return a list of dictionaries.  Default False.

        file_name: string
            File name to save/read the structures table.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.

        """

        file_name = self.get_cache_path(file_name, self.EXPERIMENTS_KEY)

        experiments = self.api.get_experiments_api(path=file_name,
                                                   strategy='lazy',
                                                   **Cache.cache_json())

        for e in experiments:
            # renaming id
            e['id'] = e['data_set_id']
            del e['data_set_id']

            # simplify trangsenic line
            tl = e.get('transgenic_line', None)
            if tl:
                e['transgenic_line'] = tl['name']

            # parse the injection structures
            injs = [int(i) for i in e['injection_structures'].split('/')]
            e['injection_structures'] = injs
            e['primary_injection_structure'] = injs[0]

            # remove storage dir
            del e['storage_directory']

        # filter the read/downloaded list of experiments
        experiments = self.filter_experiments(experiments, cre,
                                              injection_structure_ids)

        if dataframe:
            experiments = pd.DataFrame(experiments)
            experiments.set_index(['id'], inplace=True, drop=False)

        return experiments

    def filter_experiments(self,
                           experiments,
                           cre=None,
                           injection_structure_ids=None):
        """
        Take a list of experiments and filter them by cre status and injection structure.

        Parameters
        ----------

        cre: boolean or list
            If True, return only cre-positive experiments.  If False, return only
            cre-negative experiments.  If None, return all experients. If list, return
            all experiments with cre line names in the supplied list. Default None.

        injection_structure_ids: list
            Only return experiments that were injected in the structures provided here.
            If None, return all experiments.  Default None.
        """

        if cre is True:
            experiments = [e for e in experiments if e['transgenic_line']]
        elif cre is False:
            experiments = [e for e in experiments if not e['transgenic_line']]
        elif cre is not None:
            cre = [c.lower() for c in cre]
            experiments = [
                e for e in experiments if e['transgenic_line'] is not None
                and e['transgenic_line'].lower() in cre
            ]

        if injection_structure_ids is not None:
            structure_ids = MouseConnectivityCache.validate_structure_ids(
                injection_structure_ids)
            descendant_ids = set(
                reduce(
                    op.add,
                    self.get_structure_tree().descendant_ids(
                        injection_structure_ids)))

            experiments = [
                e for e in experiments if e['structure_id'] in descendant_ids
            ]

        return experiments

    def get_experiment_structure_unionizes(self,
                                           experiment_id,
                                           file_name=None,
                                           is_injection=None,
                                           structure_ids=None,
                                           include_descendants=False,
                                           hemisphere_ids=None):
        """
        Retrieve the structure unionize data for a specific experiment.  Filter by
        structure, injection status, and hemisphere.

        Parameters
        ----------

        experiment_id: int
            ID of the experiment of interest.  Corresponds to section_data_set_id in the API.

        file_name: string
            File name to save/read the experiments list.  If file_name is None,
            the file_name will be pulled out of the manifest.  If caching
            is disabled, no file will be saved. Default is None.

        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a specific set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.

        """

        file_name = self.get_cache_path(file_name,
                                        self.STRUCTURE_UNIONIZES_KEY,
                                        experiment_id)

        filter_fn = functools.partial(self.filter_structure_unionizes,
                                      is_injection=is_injection,
                                      structure_ids=structure_ids,
                                      include_descendants=include_descendants,
                                      hemisphere_ids=hemisphere_ids)

        col_rn = lambda x: pd.DataFrame(x).rename(
            columns={'section_data_set_id': 'experiment_id'})

        return self.api.get_structure_unionizes(
            [experiment_id],
            path=file_name,
            strategy='lazy',
            pre=col_rn,
            post=filter_fn,
            writer=lambda p, x: pd.DataFrame(x).to_csv(p),
            reader=lambda x: pd.read_csv(x, index_col=0, parse_dates=True))

    def rank_structures(self,
                        experiment_ids,
                        is_injection,
                        structure_ids=None,
                        hemisphere_ids=None,
                        rank_on='normalized_projection_volume',
                        n=5,
                        threshold=10**-2):
        '''Produces one or more (per experiment) ranked lists of brain structures, using a specified data field.

        Parameters
        ----------
        experiment_ids : list of int
            Obtain injection_structures for these experiments.
        is_injection : boolean
            Use data from only injection (or non-injection) unionizes.
        structure_ids : list of int, optional
            Consider only these structures. It is a good idea to make sure that these structures are not spatially
            overlapping; otherwise your results will contain redundant information. Defaults to the summary
            structures - a brain-wide list of nonoverlapping mid-level structures.
        hemisphere_ids : list of int, optional
            Consider only these hemispheres (1: left, 2: right, 3: both). Like with structures,
            you might get redundant results if you select overlapping options. Defaults to [1, 2].
        rank_on : str, optional
            Rank unionize data using this field (descending). Defaults to normalized_projection_volume.
        n : int, optional
            Return only the top n structures.
        threshold : float, optional
            Consider only records whose data value - specified by the rank_on parameter - exceeds this value.

        Returns
        -------
        list :
            Each element (1 for each input experiment) is a list of dictionaries. The dictionaries describe the top
            injection structures in descending order. They are specified by their structure and hemisphere id fields and
            additionally report the value specified by the rank_on parameter.

        '''

        output_keys = [
            'experiment_id', rank_on, 'hemisphere_id', 'structure_id'
        ]

        if hemisphere_ids is None:
            hemisphere_ids = [1, 2]
        if structure_ids is None:
            structure_ids = self.default_structure_ids

        unionizes = self.get_structure_unionizes(experiment_ids,
                                                 is_injection=is_injection,
                                                 structure_ids=structure_ids,
                                                 hemisphere_ids=hemisphere_ids,
                                                 include_descendants=False)
        unionizes = unionizes[unionizes[rank_on] > threshold]

        results = []
        for eid in experiment_ids:

            this_experiment_unionizes = unionizes[unionizes['experiment_id'] ==
                                                  eid]
            this_experiment_unionizes = this_experiment_unionizes.sort_values(
                by=rank_on, ascending=False)
            this_experiment_unionizes = this_experiment_unionizes.loc[:,
                                                                      output_keys]

            records = this_experiment_unionizes.to_dict('record')
            if len(records) > n:
                records = records[:n]
            results.append(records)

        return results

    def filter_structure_unionizes(self,
                                   unionizes,
                                   is_injection=None,
                                   structure_ids=None,
                                   include_descendants=False,
                                   hemisphere_ids=None):
        """
        Take a list of unionzes and return a subset of records filtered by injection status, structure, and
        hemisphere.

        Parameters
        ----------
        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.
        """
        if is_injection is not None:
            unionizes = unionizes[unionizes.is_injection == is_injection]

        if structure_ids is not None:
            structure_ids = MouseConnectivityCache.validate_structure_ids(
                structure_ids)

            if include_descendants:
                structure_ids = reduce(
                    op.add,
                    self.get_structure_tree().descendant_ids(structure_ids))
            else:
                structure_ids = set(structure_ids)

            unionizes = unionizes[unionizes['structure_id'].isin(
                structure_ids)]

        if hemisphere_ids is not None:
            unionizes = unionizes[unionizes['hemisphere_id'].isin(
                hemisphere_ids)]

        return unionizes

    def get_structure_unionizes(self,
                                experiment_ids,
                                is_injection=None,
                                structure_ids=None,
                                include_descendants=False,
                                hemisphere_ids=None):
        """
        Get structure unionizes for a set of experiment IDs.  Filter the results by injection status,
        structure, and hemisphere.

        Parameters
        ----------
        experiment_ids: list
            List of experiment IDs.  Corresponds to section_data_set_id in the API.

        is_injection: boolean
            If True, only return unionize records that disregard non-injection pixels.
            If False, only return unionize records that disregard injection pixels.
            If None, return all records.  Default None.

        structure_ids: list
            Only return unionize records for a specific set of structures.
            If None, return all records. Default None.

        include_descendants: boolean
            Include all descendant records for specified structures. Default False.

        hemisphere_ids: list
            Only return unionize records that disregard pixels outside of a hemisphere.
            or set of hemispheres. Left = 1, Right = 2, Both = 3.  If None, include all
            records [1, 2, 3].  Default None.
        """

        unionizes = [
            self.get_experiment_structure_unionizes(
                eid,
                is_injection=is_injection,
                structure_ids=structure_ids,
                include_descendants=include_descendants,
                hemisphere_ids=hemisphere_ids) for eid in experiment_ids
        ]

        return pd.concat(unionizes, ignore_index=True, sort=True)

    def get_projection_matrix(self,
                              experiment_ids,
                              projection_structure_ids=None,
                              hemisphere_ids=None,
                              parameter='projection_volume',
                              dataframe=False):

        if projection_structure_ids is None:
            projection_structure_ids = self.default_structure_ids

        unionizes = self.get_structure_unionizes(
            experiment_ids,
            is_injection=False,
            structure_ids=projection_structure_ids,
            include_descendants=False,
            hemisphere_ids=hemisphere_ids)

        hemisphere_ids = set(unionizes['hemisphere_id'].values.tolist())

        nrows = len(experiment_ids)
        ncolumns = len(projection_structure_ids) * len(hemisphere_ids)

        matrix = np.empty((nrows, ncolumns))
        matrix[:] = np.NAN

        row_lookup = {}
        for idx, e in enumerate(experiment_ids):
            row_lookup[e] = idx

        column_lookup = {}
        columns = []

        cidx = 0
        hlabel = {1: '-L', 2: '-R', 3: ''}

        acronym_map = self.get_structure_tree().value_map(
            lambda x: x['id'], lambda x: x['acronym'])

        for hid in hemisphere_ids:
            for sid in projection_structure_ids:
                column_lookup[(hid, sid)] = cidx
                label = acronym_map[sid] + hlabel[hid]
                columns.append({
                    'hemisphere_id': hid,
                    'structure_id': sid,
                    'label': label
                })
                cidx += 1

        for _, row in unionizes.iterrows():
            ridx = row_lookup[row['experiment_id']]
            k = (row['hemisphere_id'], row['structure_id'])
            cidx = column_lookup[k]
            matrix[ridx, cidx] = row[parameter]

        if dataframe:
            warnings.warn("dataframe argument is deprecated.")
            all_experiments = self.get_experiments(dataframe=True)

            rows_df = all_experiments.loc[experiment_ids]

            cols_df = pd.DataFrame(columns)

            return {'matrix': matrix, 'rows': rows_df, 'columns': cols_df}
        else:
            return {
                'matrix': matrix,
                'rows': experiment_ids,
                'columns': columns
            }

    def get_deformation_field(self,
                              section_data_set_id,
                              header_path=None,
                              voxel_path=None):
        ''' Extract the local alignment parameters for this dataset. This a 3D vector image (3 components) describing 
        a deformable local mapping from CCF voxels to this section data set's affine-aligned image stack.

        Parameters
        ----------
            section_data_set_id : int
                Download the deformation field for this data set
            header_path : str, optional
                If supplied, the deformation field header will be downloaded to this path.
            voxel_path : str, optiona
                If supplied, the deformation field voxels will be downloaded to this path.

        Returns
        -------
            numpy.ndarray : 
                3D X 3 component vector array (origin 0, 0, 0; 25-micron isometric resolution) defining a 
                deformable transformation from CCF-space to affine-transformed image space.

        '''

        if self.resolution not in self.DFMFLD_RESOLUTIONS:
            warnings.warn(
                'deformation fields are only available at {} isometric resolutions, but this is a '\
                '{}-micron cache'.format(self.DFMFLD_RESOLUTIONS, self.resolution)
            )

        header_path = self.get_cache_path(header_path,
                                          self.DEFORMATION_FIELD_HEADER_KEY,
                                          section_data_set_id)
        voxel_path = self.get_cache_path(voxel_path,
                                         self.DEFORMATION_FIELD_VOXEL_KEY,
                                         section_data_set_id)

        if not (os.path.exists(header_path) and os.path.exists(voxel_path)):
            Manifest.safe_make_parent_dirs(header_path)
            Manifest.safe_make_parent_dirs(voxel_path)
            self.api.download_deformation_field(section_data_set_id,
                                                header_path=header_path,
                                                voxel_path=voxel_path)

        return sitk.GetArrayFromImage(sitk.ReadImage(str(
            header_path)))  # TODO the str call here is only necessary in 2.7

    def get_affine_parameters(self,
                              section_data_set_id,
                              direction='trv',
                              file_name=None):
        ''' Extract the parameters of the 3D affine tranformation mapping this section data set's image-space stack to 
        CCF-space (or vice-versa).

        Parameters
        ----------
        section_data_set_id : int
            download the parameters for this data set.
        direction : str, optional
            Valid options are:
                trv : "transform from reference to volume". Maps CCF points to image space points. If you are 
                    resampling data into CCF, this is the direction you want.
                tvr : "transform from volume to reference". Maps image space points to CCF points.
        file_name : str
            If provided, store the downloaded file here.
 
        Returns
        -------
        alignment : numpy.ndarray
            4 X 3 matrix. In order to transform a point [X_1, X_2, X_3] run 
                np.dot([X_1, X_2, X_3, 1], alignment). In 
            to build a SimpleITK affine transform run:
                transform = sitk.AffineTransform(3)
                transform.SetParameters(alignment.flatten())

        '''

        if not direction in ('trv', 'tvr'):
            raise ArgumentError(
                'invalid direction: {}. direction must be one of tvr, trv'.
                format(direction))

        file_name = self.get_cache_path(file_name, self.ALIGNMENT3D_KEY)

        raw_alignment = self.api.download_alignment3d(
            strategy='lazy',
            path=file_name,
            section_data_set_id=section_data_set_id,
            **Cache.cache_json())

        alignment_re = re.compile('{}_(?P<index>\d+)'.format(direction))
        alignment = np.zeros((4, 3), dtype=float)

        for entry, value in raw_alignment.items():
            match = alignment_re.match(entry)
            if match is not None:
                alignment.flat[int(match.group('index'))] = value

        return alignment

    def add_manifest_paths(self, manifest_builder):
        """
        Construct a manifest for this Cache class and save it in a file.

        Parameters
        ----------

        file_name: string
            File location to save the manifest.

        """

        manifest_builder = super(MouseConnectivityCache,
                                 self).add_manifest_paths(manifest_builder)

        manifest_builder.add_path(self.EXPERIMENTS_KEY,
                                  'experiments.json',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.STRUCTURE_UNIONIZES_KEY,
                                  'experiment_%d/structure_unionizes.csv',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.INJECTION_DENSITY_KEY,
                                  'experiment_%d/injection_density_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.INJECTION_FRACTION_KEY,
                                  'experiment_%d/injection_fraction_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.DATA_MASK_KEY,
                                  'experiment_%d/data_mask_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.PROJECTION_DENSITY_KEY,
                                  'experiment_%d/projection_density_%d.nrrd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.DEFORMATION_FIELD_HEADER_KEY,
                                  'experiment_%d/dfmfld.mhd',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.DEFORMATION_FIELD_VOXEL_KEY,
                                  'experiment_%d/dfmfld.raw',
                                  parent_key='BASEDIR',
                                  typename='file')

        manifest_builder.add_path(self.ALIGNMENT3D_KEY,
                                  'experiment_%d/alignment3d.json',
                                  parent_key='BASEDIR',
                                  typename='file')

        return manifest_builder
Beispiel #34
0
def test_notebook(fn_temp_dir):

    # coding: utf-8

    # # Reference Space
    #
    # This notebook contains example code demonstrating the use of the StructureTree and ReferenceSpace classes. These classes provide methods for interacting with the 3d spaces to which Allen Institute data and atlases are registered.
    #
    # Unlike the AllenSDK cache classes, StructureTree and ReferenceSpace operate entirely in memory. We recommend using json files to store text and nrrd files to store volumetric images.
    #
    # The MouseConnectivityCache class has methods for downloading, storing, and constructing StructureTrees and ReferenceSpaces. Please see [here](https://alleninstitute.github.io/AllenSDK/_static/examples/nb/mouse_connectivity.html) for examples.

    # ## Constructing a StructureTree
    #
    # A StructureTree object is a wrapper around a structure graph - a list of dictionaries documenting brain structures and their containment relationships. To build a structure tree, you will first need to obtain a structure graph.
    #
    # For a list of atlases and corresponding structure graph ids, see [here](http://help.brain-map.org/display/api/Atlas+Drawings+and+Ontologies).

    # In[1]:

    from allensdk.api.queries.ontologies_api import OntologiesApi
    from allensdk.core.structure_tree import StructureTree

    oapi = OntologiesApi()
    structure_graph = oapi.get_structures_with_sets(
        [1])  # 1 is the id of the adult mouse structure graph

    # This removes some unused fields returned by the query
    structure_graph = StructureTree.clean_structures(structure_graph)

    tree = StructureTree(structure_graph)

    # In[2]:

    # now let's take a look at a structure
    tree.get_structures_by_name(['Dorsal auditory area'])

    # The fields are:
    #     * acronym: a shortened name for the structure
    #     * rgb_triplet: each structure is assigned a consistent color for visualizations
    #     * graph_id: the structure graph to which this structure belongs
    #     * graph_order: each structure is assigned a consistent position in the flattened graph
    #     * id: a unique integer identifier
    #     * name: the full name of the structure
    #     * structure_id_path: traces a path from the root node of the tree to this structure
    #     * structure_set_ids: the structure belongs to these predefined groups

    # ## Using a StructureTree

    # In[3]:

    # get a structure's parent
    tree.parent([1011])

    # In[4]:

    # get a dictionary mapping structure ids to names

    name_map = tree.get_name_map()
    name_map[247]

    # In[5]:

    # ask whether one structure is contained within another

    strida = 385
    stridb = 247

    is_desc = '' if tree.structure_descends_from(385, 247) else ' not'

    print('{0} is{1} in {2}'.format(name_map[strida], is_desc,
                                    name_map[stridb]))

    # In[6]:

    # build a custom map that looks up acronyms by ids
    # the syntax here is just a pair of node-wise functions.
    # The first one returns keys while the second one returns values

    acronym_map = tree.value_map(lambda x: x['id'], lambda y: y['acronym'])
    print(acronym_map[385])

    # ## Downloading an annotation volume
    #
    # This code snippet will download and store a nrrd file containing the Allen Common Coordinate Framework annotation. We have requested an annotation with 25-micron isometric spacing. The orientation of this space is:
    #     * Anterior -> Posterior
    #     * Superior -> Inferior
    #     * Left -> Right
    # This is the no-frills way to download an annotation volume. See the <a href='_static/examples/nb/mouse_connectivity.html#Manipulating-Grid-Data'>mouse connectivity</a> examples if you want to properly cache the downloaded data.

    # In[7]:

    import os
    import nrrd
    from allensdk.api.queries.mouse_connectivity_api import MouseConnectivityApi
    from allensdk.config.manifest import Manifest

    # the annotation download writes a file, so we will need somwhere to put it
    annotation_dir = 'annotation'
    Manifest.safe_mkdir(annotation_dir)

    annotation_path = os.path.join(annotation_dir, 'annotation.nrrd')

    mcapi = MouseConnectivityApi()
    mcapi.download_annotation_volume('annotation/ccf_2016', 25,
                                     annotation_path)

    annotation, meta = nrrd.read(annotation_path)

    # ## Constructing a ReferenceSpace

    # In[8]:

    from allensdk.core.reference_space import ReferenceSpace

    # build a reference space from a StructureTree and annotation volume, the third argument is
    # the resolution of the space in microns
    rsp = ReferenceSpace(tree, annotation, [25, 25, 25])

    # ## Using a ReferenceSpace

    # #### making structure masks
    #
    # The simplest use of a Reference space is to build binary indicator masks for structures or groups of structures.

    # In[9]:

    # A complete mask for one structure
    whole_cortex_mask = rsp.make_structure_mask([315])

    # view in coronal section

    # What if you want a mask for a whole collection of ontologically disparate structures? Just pass more structure ids to make_structure_masks:

    # In[10]:

    # This gets all of the structures targeted by the Allen Brain Observatory project
    brain_observatory_structures = rsp.structure_tree.get_structures_by_set_id(
        [514166994])
    brain_observatory_ids = [st['id'] for st in brain_observatory_structures]

    brain_observatory_mask = rsp.make_structure_mask(brain_observatory_ids)

    # view in horizontal section

    # You can also make and store a number of structure_masks at once:

    # In[11]:

    import functools

    # Define a wrapper function that will control the mask generation.
    # This one checks for a nrrd file in the specified base directory
    # and builds/writes the mask only if one does not exist
    mask_writer = functools.partial(ReferenceSpace.check_and_write,
                                    annotation_dir)

    # many_structure_masks is a generator - nothing has actrually been run yet
    mask_generator = rsp.many_structure_masks([385, 1097], mask_writer)

    # consume the resulting iterator to make and write the masks
    for structure_id in mask_generator:
        print('made mask for structure {0}.'.format(structure_id))

    os.listdir(annotation_dir)

    # #### Removing unassigned structures

    # A structure graph may contain structures that are not used in a particular reference space. Having these around can complicate use of the reference space, so we generally want to remove them.
    #
    # We'll try this using "Somatosensory areas, layer 6a" as a test case. In the 2016 ccf space, this structure is unused in favor of finer distinctions (e.g. "Primary somatosensory area, barrel field, layer 6a").

    # In[12]:

    # Double-check the voxel counts
    no_voxel_id = rsp.structure_tree.get_structures_by_name(
        ['Somatosensory areas, layer 6a'])[0]['id']
    print('voxel count for structure {0}: {1}'.format(
        no_voxel_id, rsp.total_voxel_map[no_voxel_id]))

    # remove unassigned structures from the ReferenceSpace's StructureTree
    rsp.remove_unassigned()

    # check the structure tree
    no_voxel_id in rsp.structure_tree.node_ids()

    # #### View a slice from the annotation

    # In[13]:

    import numpy as np

    # #### Downsample the space
    #
    # If you want an annotation at a resolution we don't provide, you can make one with the downsample method.

    # In[14]:

    import warnings

    target_resolution = [75, 75, 75]

    # in some versions of scipy, scipy.ndimage.zoom raises a helpful but distracting
    # warning about the method used to truncate integers.
    warnings.simplefilter('ignore')

    sf_rsp = rsp.downsample(target_resolution)

    # re-enable warnings
    warnings.simplefilter('default')

    print(rsp.annotation.shape)
    print(sf_rsp.annotation.shape)