Skip to the content.

NeuMapper

NeuMapper is a Matlab implementation of a scalable Mapper algorithm designed specifically for neuroimaging data analysis.

Developed with neuroimaging data analysis in mind, NeuMapper implements a novel landmark-based intrinsic binning strategy that eliminates the need for dimensionality reduction. Rather than projecting the high-dimensional data to a low-dimensional embedding, NeuMapper stays in high-dimensional space and performs the binning directly on a reciprocal kNN graph. By using geodesic distances, NeuMapper is able to better capture the high-dimensional, non-linear structure underlying the dynamical landscape of brain activity.

NeuMapper was designed specifically for working with complex, high-dimensional neuroimaging data and produces a shape graph representation that can be annotated with meta-information and further examined using network science tools. These shape graphs can be visualized using DyNeuSR, a Python visualization library that provides a custom web interface for exploring and interacting with shape graphs, and several other tools for anchoring these representations back to neurophysiology and behavior. To see how NeuMapper and DyNeuSR can be used together to create beautiful visualizations of high-dimensional data, check out the examples folder. See below for a demo of the interactive visualization interface.

For more details about NeuMapper see “NeuMapper: A Scalable Computational Framework for Multiscale Exploration of the Brain’s Dynamical Organization” (Geniesse et al., 2022). For the original Mapper algorithm and related applications to neuroimaging data, see “Generating dynamical neuroimaging spatiotemporal representations (DyNeuSR) using topological data analysis” (Geniesse et al., 2019) and “Towards a new approach to reveal dynamical organization of the brain using topological data analysis” (Saggar et al., 2018). Check out this blog post for more about the initial work that inspired the development of NeuMapper.

Setup

Dependencies

MATLAB R2020b

Demos

Examples

This repository includes several examples that introduce NeuMapper’s core functions and highlight different options.

Trefoil knot (examples/trefoil_knot)

The code below walks through a simple example using NeuMapper to visualize the shape of data sampled from a trefoil knot.

%% Configure paths
addpath(genpath('../../code/'));


%% Load the data
X = create_trefoil_knot(1000,'euclidean');


%% Use default options
options = struct();
options.resolution = 40;


%% Run NeuMapper
res = neumapper(X, options);


%% Save outputs
save('trefoil_knot_neumapper.mat','res');
saveas(gcf, 'trefoil_knot_neumapper.png');

Haxby fMRI data (examples/haxby_decoding)

First, let’s fetch some data from the Haxby fMRI visual decoding dataset, using Nilearn’s fetch_haxby function. We can then save the data and timing labels, so that we can read these variables into Matlab before running NeuMapper.

import numpy as np 
import pandas as pd
from nilearn.datasets import fetch_haxby
from nilearn.input_data import NiftiMasker

# Fetch dataset, extract time-series from ventral temporal (VT) mask
dataset = fetch_haxby(subjects=[2])
masker = NiftiMasker(
    dataset.mask_vt[0], 
    standardize=True, detrend=True, smoothing_fwhm=4.0,
    low_pass=0.09, high_pass=0.008, t_r=2.5,
    memory="nilearn_cache")
X = masker.fit_transform(dataset.func[0])

# Encode labels as integers
df = pd.read_csv(dataset.session_target[0], sep=" ")
target, labels = pd.factorize(df.labels.values)
timing = pd.DataFrame().assign(task=target, task_name=labels[target])
timing_onehot = pd.DataFrame({l:1*(target==i) for i,l in enumerate(labels)})

# Save X and y
np.save('SBJ02_mask_vt.npy', X)
timing.to_csv('SBJ02_timing_labels.tsv', sep='\t', index=0)
timing_onehot.to_csv('SBJ02_timing_onehot.tsv', sep='\t', index=0)

Now we can simply load the data into Matlab, and run NeuMapper.

%% Configure paths
addpath(genpath('../../code/'));


%% Load the data
X = readNPY('SBJ02_mask_vt.npy');
timing = readtable('SBJ02_timing_labels.tsv','FileType','text','Delimiter','\t');
colors = timing.task;
labels = string(timing.task_name);


%% Configure options
options = struct();
options.metric = 'correlation';
options.k = 30;
options.resolution = 400;
options.gain = 40;
options.labels = timing.task + 1; %reindex to start from 1


%% Run NeuMapper
[c,X_] = pca(X,'NumComponents',50); % Preprocess with PCA
res = neumapper(X_, options);


%% Save outputs
save('haxby_decoding_neumapper.mat','res');
saveas(gcf, 'haxby_decoding_neumapper.png');

While NeuMapper provides a basic visualization of the shape graph, to create a more interactive visualization, we can go back to Python and use the DyNeuSR visualization library. Compared to the simpler visualizations produced by NeuMapper, where each node is represented by the average coloring, the visualizations produced by DyNeuSR represent each node as a pie-chart, colored by the relative proportion of each label associated with the node.

Note, after loading the result file in Python, a few additional steps are required to extract the relevant node/link information from the memberMat matrix stored in the res structure returned by NeuMapper.

import numpy as np 
import pandas as pd
import networkx as nx
import scipy as sp
from sklearn.utils import Bunch
from scipy.io import loadmat
from dyneusr.core import DyNeuGraph  
from collections import defaultdict


## Load the NeuMapper result
mat = loadmat('haxby_decoding_neumapper.mat')
res = mat['res'][0][0]
res = Bunch(**{k:res[i] for i,k in enumerate(res.dtype.names)})
res = res.get('res', res.get('var', res))

# load one-hot encoding matrix of timing labels 
timing_onehot = pd.read_csv('SBJ02_timing_onehot.tsv', sep='\t') 


## Convert to KeplerMapper format
membership = res.clusterBins
adjacency = membership @ membership.T
np.fill_diagonal(adjacency, 0)
adjacency = (adjacency > 0).astype(int)

# get node link data 
G = nx.Graph(adjacency)
graph = nx.node_link_data(G)

# update format of nodes  e.g. {node: [row_i, ...]}
nodes = defaultdict(list) 
for n, node in enumerate(membership):
    nodes[n] = node.nonzero()[0].tolist()

# update format of links  e.g. {source: [target, ...]}
links = defaultdict(list) 
for link in graph['links']:
    u, v = link['source'], link['target']
    if u != v:
        links[u].append(v)

# update graph data
graph['nodes'] = nodes
graph['links'] = links


## Visualize the shape graph using DyNeuSR's DyNeuGraph
dG = DyNeuGraph(G=graph, y=timing_onehot)
dG.visualize('haxby_decoding_neumapper_dyneusr.html')

References

If you find NeuMapper useful, please consider citing:

Geniesse, C., Chowdhury, S., & Saggar, M. (2022). NeuMapper: A Scalable Computational Framework for Multiscale Exploration of the Brain’s Dynamical Organization. Network Neuroscience, Advance publication. doi:10.1162/netn_a_00229

For more information about DyNeuSR, please see:

Geniesse, C., Sporns, O., Petri, G., & Saggar, M. (2019). Generating dynamical neuroimaging spatiotemporal representations (DyNeuSR) using topological data analysis. Network Neuroscience, 3(3). doi:10.1162/netn_a_00093

For more information about the Mapper approach, please see:

Saggar, M., Sporns, O., Gonzalez-Castillo, J., Bandettini, P.A., Carlsson, G., Glover, G., & Reiss, A.L. (2018). Towards a new approach to reveal dynamical organization of the brain using topological data analysis. Nature Communications, 9(1). doi:10.1038/s41467-018-03664-4