Skip to content

Commit

Permalink
Merge pull request #29 from KrishnaswamyLab/dev
Browse files Browse the repository at this point in the history
graphtools v0.2.0
  • Loading branch information
scottgigante authored Nov 23, 2018
2 parents edc91ea + 76ba4f0 commit 9480f9a
Show file tree
Hide file tree
Showing 17 changed files with 670 additions and 195 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

sudo: required

cache: pip

addons:
apt:
packages:
Expand Down
25 changes: 16 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ graphtools
.. image:: https://img.shields.io/pypi/v/graphtools.svg
:target: https://pypi.org/project/graphtools/
:alt: Latest PyPi version
.. image:: https://anaconda.org/conda-forge/tasklogger/badges/version.svg
:target: https://anaconda.org/conda-forge/tasklogger/
:alt: Latest Conda version
.. image:: https://api.travis-ci.com/KrishnaswamyLab/graphtools.svg?branch=master
:target: https://travis-ci.com/KrishnaswamyLab/graphtools
:alt: Travis CI Build
Expand All @@ -28,7 +31,11 @@ Installation

graphtools is available on `pip`. Install by running the following in a terminal::

pip install --user graphtools
pip install --user graphtools

Alternatively, graphtools can be installed using `Conda <https://conda.io/docs/>`_ (most easily obtained via the `Miniconda Python distribution <https://conda.io/miniconda.html>`_)::

conda install -c conda-forge graphtools

Or, to install the latest version from github::

Expand All @@ -45,14 +52,14 @@ The `graphtools.Graph` class provides an all-in-one interface for k-nearest neig

Use it as follows::

from sklearn import datasets
import graphtools
digits = datasets.load_digits()
G = graphtools.Graph(digits['data'])
K = G.kernel
P = G.diff_op
G = graphtools.Graph(digits['data'], n_landmark=300)
L = G.landmark_op
from sklearn import datasets
import graphtools
digits = datasets.load_digits()
G = graphtools.Graph(digits['data'])
K = G.kernel
P = G.diff_op
G = graphtools.Graph(digits['data'], n_landmark=300)
L = G.landmark_op

Help
----
Expand Down
2 changes: 1 addition & 1 deletion graphtools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .api import Graph
from .api import Graph, from_igraph
from .version import __version__
51 changes: 43 additions & 8 deletions graphtools/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
import warnings
import tasklogger
from scipy import sparse

from . import base
from . import graphs
Expand All @@ -9,14 +10,15 @@
def Graph(data,
n_pca=None,
sample_idx=None,
adaptive_k='sqrt',
adaptive_k=None,
precomputed=None,
knn=5,
decay=10,
bandwidth=None,
distance='euclidean',
thresh=1e-4,
kernel_symm='+',
gamma=None,
theta=None,
n_landmark=None,
n_svd=100,
beta=1,
Expand Down Expand Up @@ -61,6 +63,11 @@ def Graph(data,
decay : `int` or `None`, optional (default: 10)
Rate of alpha decay to use. If `None`, alpha decay is not used.
bandwidth : `float`, list-like or `None`, optional (default: `None`)
Fixed bandwidth to use. If given, overrides `knn`. Can be a single
bandwidth or a list-like (shape=[n_samples]) of bandwidths for each
sample.
distance : `str`, optional (default: `'euclidean'`)
Any metric from `scipy.spatial.distance` can be used
distance metric for building kNN graph.
Expand All @@ -75,25 +82,25 @@ def Graph(data,
Defines method of MNN symmetrization.
'+' : additive
'*' : multiplicative
'gamma' : min-max
'theta' : min-max
'none' : no symmetrization
gamma: float (default: None)
Min-max symmetrization constant or matrix. Only used if kernel_symm='gamma'.
K = `gamma * min(K, K.T) + (1 - gamma) * max(K, K.T)`
theta: float (default: None)
Min-max symmetrization constant or matrix. Only used if kernel_symm='theta'.
K = `theta * min(K, K.T) + (1 - theta) * max(K, K.T)`
precomputed : {'distance', 'affinity', 'adjacency', `None`}, optional (default: `None`)
If the graph is precomputed, this variable denotes which graph
matrix is provided as `data`.
Only one of `precomputed` and `n_pca` can be set.
beta: float, optional(default: 1)
Multiply within - batch connections by(1 - beta)
Multiply between - batch connections by beta
sample_idx: array-like
Batch index for MNN kernel
adaptive_k : `{'min', 'mean', 'sqrt', 'none'}` (default: 'sqrt')
adaptive_k : `{'min', 'mean', 'sqrt', 'none'}` (default: None)
Weights MNN kernel adaptively using the number of cells in
each sample according to the selected method.
Expand Down Expand Up @@ -221,3 +228,31 @@ def Graph(data,
for key, value in params.items()
if key != "data"])))
return Graph(**params)


def from_igraph(G, **kwargs):
"""Convert an igraph.Graph to a graphtools.Graph
Creates a graphtools.graphs.TraditionalGraph with a
precomputed adjacency matrix
Parameters
----------
G : igraph.Graph
Graph to be converted
kwargs
keyword arguments for graphtools.Graph
Returns
-------
G : graphtools.graphs.TraditionalGraph
"""
if 'precomputed' in kwargs:
if kwargs['precomputed'] != 'adjacency':
warnings.warn(
"Cannot build graph from igraph with precomputed={}. "
"Use 'adjacency' instead.".format(kwargs['precomputed']),
UserWarning)
del kwargs['precomputed']
return Graph(sparse.coo_matrix(G.get_adjacency().data),
precomputed='adjacency', **kwargs)
109 changes: 77 additions & 32 deletions graphtools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,12 +311,12 @@ class BaseGraph(with_metaclass(abc.ABCMeta, Base)):
Defines method of MNN symmetrization.
'+' : additive
'*' : multiplicative
'gamma' : min-max
'theta' : min-max
'none' : no symmetrization
gamma: float (default: 0.5)
theta: float (default: 0.5)
Min-max symmetrization constant.
K = `gamma * min(K, K.T) + (1 - gamma) * max(K, K.T)`
K = `theta * min(K, K.T) + (1 - theta) * max(K, K.T)`
initialize : `bool`, optional (default : `True`)
if false, don't create the kernel matrix.
Expand All @@ -337,11 +337,20 @@ class BaseGraph(with_metaclass(abc.ABCMeta, Base)):
"""

def __init__(self, kernel_symm='+',
theta=None,
gamma=None,
initialize=True, **kwargs):
if gamma is not None:
warnings.warn("gamma is deprecated. "
"Setting theta={}".format(gamma), FutureWarning)
theta = gamma
if kernel_symm == 'gamma':
warnings.warn("kernel_symm='gamma' is deprecated. "
"Setting kernel_symm='theta'", FutureWarning)
kernel_symm = 'theta'
self.kernel_symm = kernel_symm
self.gamma = gamma
self._check_symmetrization(kernel_symm, gamma)
self.theta = theta
self._check_symmetrization(kernel_symm, theta)

if initialize:
tasklogger.log_debug("Initializing kernel...")
Expand All @@ -350,25 +359,25 @@ def __init__(self, kernel_symm='+',
tasklogger.log_debug("Not initializing kernel.")
super().__init__(**kwargs)

def _check_symmetrization(self, kernel_symm, gamma):
if kernel_symm not in ['+', '*', 'gamma', None]:
def _check_symmetrization(self, kernel_symm, theta):
if kernel_symm not in ['+', '*', 'theta', None]:
raise ValueError(
"kernel_symm '{}' not recognized. Choose from "
"'+', '*', 'gamma', or 'none'.".format(kernel_symm))
elif kernel_symm != 'gamma' and gamma is not None:
warnings.warn("kernel_symm='{}' but gamma is not None. "
"Setting kernel_symm='gamma'.".format(kernel_symm))
self.kernel_symm = kernel_symm = 'gamma'

if kernel_symm == 'gamma':
if gamma is None:
warnings.warn("kernel_symm='gamma' but gamma not given. "
"Defaulting to gamma=0.5.")
self.gamma = gamma = 0.5
elif not isinstance(gamma, numbers.Number) or \
gamma < 0 or gamma > 1:
raise ValueError("gamma {} not recognized. Expected "
"a float between 0 and 1".format(gamma))
"'+', '*', 'theta', or 'none'.".format(kernel_symm))
elif kernel_symm != 'theta' and theta is not None:
warnings.warn("kernel_symm='{}' but theta is not None. "
"Setting kernel_symm='theta'.".format(kernel_symm))
self.kernel_symm = kernel_symm = 'theta'

if kernel_symm == 'theta':
if theta is None:
warnings.warn("kernel_symm='theta' but theta not given. "
"Defaulting to theta=0.5.")
self.theta = theta = 0.5
elif not isinstance(theta, numbers.Number) or \
theta < 0 or theta > 1:
raise ValueError("theta {} not recognized. Expected "
"a float between 0 and 1".format(theta))

def _build_kernel(self):
"""Private method to build kernel matrix
Expand Down Expand Up @@ -400,26 +409,26 @@ def symmetrize_kernel(self, K):
elif self.kernel_symm == "*":
tasklogger.log_debug("Using multiplication symmetrization.")
K = K.multiply(K.T)
elif self.kernel_symm == 'gamma':
elif self.kernel_symm == 'theta':
tasklogger.log_debug(
"Using gamma symmetrization (gamma = {}).".format(self.gamma))
K = self.gamma * elementwise_minimum(K, K.T) + \
(1 - self.gamma) * elementwise_maximum(K, K.T)
"Using theta symmetrization (theta = {}).".format(self.theta))
K = self.theta * elementwise_minimum(K, K.T) + \
(1 - self.theta) * elementwise_maximum(K, K.T)
elif self.kernel_symm is None:
tasklogger.log_debug("Using no symmetrization.")
pass
else:
# this should never happen
raise ValueError(
"Expected kernel_symm in ['+', '*', 'gamma' or None]. "
"Got {}".format(self.gamma))
"Expected kernel_symm in ['+', '*', 'theta' or None]. "
"Got {}".format(self.theta))
return K

def get_params(self):
"""Get parameters from this object
"""
return {'kernel_symm': self.kernel_symm,
'gamma': self.gamma}
'theta': self.theta}

def set_params(self, **params):
"""Set parameters on this object
Expand All @@ -429,7 +438,7 @@ def set_params(self, **params):
Valid parameters:
Invalid parameters: (these would require modifying the kernel matrix)
- kernel_symm
- gamma
- theta
Parameters
----------
Expand All @@ -439,8 +448,8 @@ def set_params(self, **params):
-------
self
"""
if 'gamma' in params and params['gamma'] != self.gamma:
raise ValueError("Cannot update gamma. Please create a new graph")
if 'theta' in params and params['theta'] != self.theta:
raise ValueError("Cannot update theta. Please create a new graph")
if 'kernel_symm' in params and \
params['kernel_symm'] != self.kernel_symm:
raise ValueError(
Expand Down Expand Up @@ -535,6 +544,42 @@ def build_kernel(self):
"""
raise NotImplementedError

def to_pygsp(self, **kwargs):
"""Convert to a PyGSP graph
For use only when the user means to create the graph using
the flag `use_pygsp=True`, and doesn't wish to recompute the kernel.
Creates a graphtools.graphs.TraditionalGraph with a precomputed
affinity matrix which also inherits from pygsp.graphs.Graph.
Parameters
----------
kwargs
keyword arguments for graphtools.Graph
Returns
-------
G : graphtools.base.PyGSPGraph, graphtools.graphs.TraditionalGraph
"""
from . import api
if 'precomputed' in kwargs:
if kwargs['precomputed'] != 'affinity':
warnings.warn(
"Cannot build PyGSPGraph with precomputed={}. "
"Using 'affinity' instead.".format(kwargs['precomputed']),
UserWarning)
del kwargs['precomputed']
if 'use_pygsp' in kwargs:
if kwargs['use_pygsp'] is not True:
warnings.warn(
"Cannot build PyGSPGraph with use_pygsp={}. "
"Use True instead.".format(kwargs['use_pygsp']),
UserWarning)
del kwargs['use_pygsp']
return api.Graph(self.K,
precomputed="affinity", use_pygsp=True,
**kwargs)


class PyGSPGraph(with_metaclass(abc.ABCMeta, pygsp.graphs.Graph, Base)):
"""Interface between BaseGraph and PyGSP.
Expand Down
Loading

0 comments on commit 9480f9a

Please sign in to comment.