├── .gitignore ├── .gitmodules ├── LICENSE ├── MANIFEST.in ├── README.md ├── bin └── locale_alg ├── data └── zachary.mtx ├── docker ├── Dockerfile ├── build.sh └── run.sh ├── images └── locale.png ├── notebooks └── SDP_Community_Detection.ipynb ├── sdp_clustering ├── __init__.py └── models.py ├── setup.py └── src ├── cluster.cpp ├── cluster.h └── pybind.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | *.so 4 | build 5 | dist 6 | *.egg-info 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/pybind11"] 2 | ignore = dirty 3 | path = third_party/pybind11 4 | url = https://github.com/pybind/pybind11.git 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CMU Locus Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | include src/*.h* 3 | recursive-include third_party/pybind11/include *.h 4 | recursive-include third_party/pybind11/pybind11 *.py 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SDP Clustering • [![PyPi][pypi-image]][pypi] [![colab][colab-image]][colab] [![License][license-image]][license] 2 | 3 | [license-image]: https://img.shields.io/badge/License-MIT-yellow.svg 4 | [license]: LICENSE 5 | 6 | [pypi-image]: https://img.shields.io/pypi/v/sdp-clustering.svg 7 | [pypi]: https://pypi.python.org/pypi/sdp-clustering 8 | 9 | [colab-image]: https://colab.research.google.com/assets/colab-badge.svg 10 | [colab]: https://colab.research.google.com/drive/16d06iAViZHJ58S-RmwAzKR_TyWi5FE-V#offline=true&sandboxMode=true 11 | 12 | * Community detection using fast low-cardinality semidefinite programming * 13 | 14 | This repository contains the source code to reproduce the experiments in the NeurIPS'20 paper [Community detection using fast low-cardinality semidefinite programming](https://arxiv.org/abs/2012.02676) by [Po-Wei Wang](https://powei.tw/) and [J. Zico Kolter](http://zicokolter.com/). 15 | 16 | ## What the package provides 17 | It detect communities (that is, clustering with unknown number of clusters) via maximizing a metric called modularity. 18 | Further, it provides sparse embeddings for nodes in a graph. 19 | 20 | #### How it works 21 | We relax the (combinatorial) modularity maximization problem to a smooth semidefinite program (SDP) by converting the Kronecker delta into a dot-product. 22 | By further controlling the cardinality (sparsity) in the dot-product space, 23 | we develop a efficient optimization algorithm that scales linearly with the number of data entries. See the paper for more details. 24 | ![Conversion](images/locale.png) 25 | 26 | ## Installation 27 | 28 | ### Via pip 29 | ```bash 30 | pip install sdp-clustering 31 | ``` 32 | 33 | ### From source 34 | ```bash 35 | git clone --recursive https://github.com/locuslab/sdp_clustering 36 | cd sdp_clustering && python setup.py install 37 | ``` 38 | 39 | #### Package Dependencies 40 | ``` 41 | conda install -c numpy scipy 42 | ``` 43 | 44 | ## Running experiments 45 | After installation, the package provides a command-line utility **locale_alg** accepting matrix-market format. 46 | For example, to detect communities in Zachary Karate Club and output the result in *labels.txt*, run 47 | ```bash 48 | locale_alg data/zachary.mtx --out labels.txt 49 | ``` 50 | To obtain the low-cardinality embedding (without rounding) with cardinality ≤2, run 51 | ```bash 52 | locale_alg data/zachary.mtx --out emb.txt --embedding --k=2 53 | ``` 54 | 55 | ### Experiment parameters 56 | All experiments can be replicated by the default parameters (k=8), except that the Amazon data requires k=16. 57 | 58 | ## API 59 | See **bin/locale_alg** for the example usage. 60 | Mainly, the package provides 3 functions 61 | ```python 62 | locale_embedding: obtain embeddings from the continuous optimization algorithm 63 | leiden_locale: obtain comminity assignments by the hierarchical Leiden-Locale algorithm 64 | init_random_seed: set random seed 65 | ``` 66 | For more details, see *sdp_clustering/models.py*. 67 | For even more details, see the Cpp implementation in the *src/* folder. 68 | -------------------------------------------------------------------------------- /bin/locale_alg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import argparse 5 | import numpy as np 6 | from scipy.io import mmread 7 | from sdp_clustering import init_random_seed, leiden_locale, locale_embedding 8 | 9 | def main(): 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument('graph_input', type=str, 12 | help='Inputting_graph in symmetric matrix market format') 13 | parser.add_argument('--k', type=int, default=8, 14 | help='Cadinality for embeddings (int)') 15 | parser.add_argument('--eps', type=float, default=1e-6, 16 | help='Stopping criterion for optimization problem (float)') 17 | parser.add_argument('--max_outer', type=int, default=10, 18 | help='Maximum number of outer iterations (int)') 19 | parser.add_argument('--max_lv', type=int, default=10, 20 | help='Maximum number of levels in an outer iteration (int)') 21 | parser.add_argument('--max_inner', type=int, default=2, 22 | help='Maximum number of inner iters for optimization (int)') 23 | parser.add_argument('--seed', type=int, default=1234, 24 | help='random seed (int)') 25 | parser.add_argument('--verbose', type=int, default=1, 26 | help='Verbosity') 27 | parser.add_argument('--embedding', action='store_true', 28 | help='Output embedding instead of labels') 29 | parser.add_argument('--out', type=str, default=None, 30 | help='Output clustering labels or embeddings (default no output)') 31 | args = parser.parse_args() 32 | 33 | 34 | graph = mmread(args.graph_input) 35 | 36 | init_random_seed(args.seed) 37 | if args.embedding: 38 | E = locale_embedding(graph, args.k, args.eps, args.max_inner, args.verbose) 39 | if args.out: 40 | E.savetxt(args.out) 41 | else: 42 | labels = leiden_locale(graph, args.k, args.eps, args.max_outer, args.max_lv, args.max_inner, args.verbose) 43 | if args.out: 44 | np.savetxt(args.out, labels, fmt='%d', delimiter='\n') 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /data/zachary.mtx: -------------------------------------------------------------------------------- 1 | %%MatrixMarket matrix coordinate pattern symmetric 2 | % Zachary karate club from http://konect.cc/networks/ucidata-zachary/ 3 | 34 34 78 4 | 1 2 5 | 1 3 6 | 2 3 7 | 1 4 8 | 2 4 9 | 3 4 10 | 1 5 11 | 1 6 12 | 1 7 13 | 5 7 14 | 6 7 15 | 1 8 16 | 2 8 17 | 3 8 18 | 4 8 19 | 1 9 20 | 3 9 21 | 3 10 22 | 1 11 23 | 5 11 24 | 6 11 25 | 1 12 26 | 1 13 27 | 4 13 28 | 1 14 29 | 2 14 30 | 3 14 31 | 4 14 32 | 6 17 33 | 7 17 34 | 1 18 35 | 2 18 36 | 1 20 37 | 2 20 38 | 1 22 39 | 2 22 40 | 24 26 41 | 25 26 42 | 3 28 43 | 24 28 44 | 25 28 45 | 3 29 46 | 24 30 47 | 27 30 48 | 2 31 49 | 9 31 50 | 1 32 51 | 25 32 52 | 26 32 53 | 29 32 54 | 3 33 55 | 9 33 56 | 15 33 57 | 16 33 58 | 19 33 59 | 21 33 60 | 23 33 61 | 24 33 62 | 30 33 63 | 31 33 64 | 32 33 65 | 9 34 66 | 10 34 67 | 14 34 68 | 15 34 69 | 16 34 70 | 19 34 71 | 20 34 72 | 21 34 73 | 23 34 74 | 24 34 75 | 27 34 76 | 28 34 77 | 29 34 78 | 30 34 79 | 31 34 80 | 32 34 81 | 33 34 82 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pytorch/pytorch:1.6.0-cuda10.1-cudnn7-devel 2 | 3 | ARG USER_ID 4 | ARG GROUP_ID 5 | ARG USER_NAME 6 | ARG HOME_DIR 7 | 8 | #RUN addgroup --gid ${GROUP_ID} ${USER_NAME} || groupmod -n ${USER_NAME} $(getent group ${GROUP_ID}) 9 | RUN apt-get -q update; apt-get -q -y install sudo vim 10 | #RUN conda install -y jupyter matplotlib line_profiler scipy 11 | #RUN python -m pip install pytorch_memlab setproctitle termcolor 12 | #RUN python -m pip install cvxpy 13 | #RUN conda install -y -q pytorch=1.3.1 -c pytorch 14 | RUN adduser --quiet --disabled-password --system --no-create-home --uid ${USER_ID} --gid ${GROUP_ID} --gecos '' --shell /bin/bash ${USER_NAME} 15 | RUN usermod -d ${HOME_DIR} ${USER_NAME} 16 | RUN adduser --quiet ${USER_NAME} sudo ; echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 17 | 18 | RUN mkdir -p /data 19 | WORKDIR /data 20 | 21 | USER ${USER_NAME} 22 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | docker image build \ 2 | --build-arg USER_ID=$(id -u ${USER}) \ 3 | --build-arg GROUP_ID=$(id -g ${USER}) \ 4 | --build-arg USER_NAME=$(whoami) \ 5 | --build-arg HOME_DIR=$HOME \ 6 | -t cluster . 7 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | DATA_VOLUME="-v $(pwd)/..:/data" 2 | HOME_VOLUME="-v $HOME:$HOME" 3 | docker run --rm --privileged --runtime=nvidia -it --net=host --ipc=host ${DATA_VOLUME} ${HOME_VOLUME} -v /etc/localtime:/etc/localtime:ro cluster bash -l 4 | -------------------------------------------------------------------------------- /images/locale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locuslab/sdp_clustering/446a2bbae2b15c7c9fac15af74414be8950e867f/images/locale.png -------------------------------------------------------------------------------- /notebooks/SDP_Community_Detection.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "SDP Community Detection.ipynb", 7 | "provenance": [] 8 | }, 9 | "kernelspec": { 10 | "name": "python3", 11 | "display_name": "Python 3" 12 | } 13 | }, 14 | "cells": [ 15 | { 16 | "cell_type": "markdown", 17 | "metadata": { 18 | "id": "PffV6Yr7PQbp" 19 | }, 20 | "source": [ 21 | "# Community detection using fast low-cardinality semidefinite programming\n", 22 | "Welcome! This is the Colab for our [SDP community detection paper](https://arxiv.org/abs/2012.02676) in NeurIPS'20.\n", 23 | "\n", 24 | "In this tutorial, we will demonstrate how to use the sdp_clustering package.\n", 25 | "## Installation\n", 26 | "The following command will download the code repository and compile it." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "metadata": { 32 | "id": "D5xvysRyxNpD" 33 | }, 34 | "source": [ 35 | "%%capture\n", 36 | "!rm -rf sdp_clustering\n", 37 | "!git clone --recursive https://github.com/locuslab/sdp_clustering sdp_clustering\n", 38 | "!cd sdp_clustering && python setup.py develop" 39 | ], 40 | "execution_count": 1, 41 | "outputs": [] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "metadata": { 46 | "colab": { 47 | "base_uri": "https://localhost:8080/" 48 | }, 49 | "id": "4rYMD-1cAP67", 50 | "outputId": "70bc0e82-fc28-4179-8599-d2742ec34347" 51 | }, 52 | "source": [ 53 | "cd sdp_clustering/" 54 | ], 55 | "execution_count": 2, 56 | "outputs": [ 57 | { 58 | "output_type": "stream", 59 | "text": [ 60 | "/content/sdp_clustering\n" 61 | ], 62 | "name": "stdout" 63 | } 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": { 69 | "id": "VDXv4Qd4P6Bj" 70 | }, 71 | "source": [ 72 | "## Demonstration: Zachary Karate club\n", 73 | "In the following session, we will demo the package by showing results for the famous example from Zachary Karate Club.\n", 74 | "\n", 75 | "First, we load the graph and plot it." 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "metadata": { 81 | "id": "aOYd2q79KVGm" 82 | }, 83 | "source": [ 84 | "data = 'data/zachary.mtx'" 85 | ], 86 | "execution_count": 3, 87 | "outputs": [] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "metadata": { 92 | "colab": { 93 | "base_uri": "https://localhost:8080/", 94 | "height": 319 95 | }, 96 | "id": "-89lXvnLK3Mg", 97 | "outputId": "b37bc8d4-d086-4b16-8d4a-02b54944a02a" 98 | }, 99 | "source": [ 100 | "import matplotlib.pyplot as plt\n", 101 | "import networkx as nx\n", 102 | "import numpy as np\n", 103 | "from scipy.io import mmread\n", 104 | "graph = mmread(data)\n", 105 | "gx = nx.convert_matrix.from_numpy_matrix(graph.todense())\n", 106 | "nx.draw(gx)" 107 | ], 108 | "execution_count": 4, 109 | "outputs": [ 110 | { 111 | "output_type": "display_data", 112 | "data": { 113 | "image/png": "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FOjp6dG3b19u375Nv3796NWrF127duXWrVs5tqGrbFJTadHfJ83JwIED2b17N6mpqWqfW96kBKNca1Av+SbdKzxnRR8nRrnWyHIvs1SpUvj7+zNnzhyCg4PzbO/QoUOULl2a1q1bq9WPJk2aiBGfIBRTH23gg/ejg169ehEREUFCQgKOjo45ThcWhszpthbVS6FIT1V7CQe8L702YsQI7t69i5ubG+7u7gwYMIDY2Ngsx70vCK7dr05qoIedWSmt2igoNWrUwNbWVqusyJSUFKRSaa7P29jYsG3bNvr27cuDBw9yPEahULB48WK+/fZbtZNU6tevT3R0NMnJyWqdJwhCwfuoA1+m8uXLs3XrVnx8fBg+fDjDhw8nIaHw16k5WpSh7uuLtE06x2T3Wng6mdPWrlK26bb8UuilUimTJk3i7t271K5dGxcXF0aNGkV8/PsdCXo6W2jdVwXQs4H27RSUzB0bNCWTyfIMfADt27fH29ubbt268e7du2zPnzt3jsTERLp27ar26xsbG1OnTh1CQ0PVPlcQhIL1rwh8mTp27MitW7coUaIE9vb2HDp0qND7sGvXLgb37cEo1xqs6OPExsGNcpxuU0WpUqX47rvviImJoWzZstSrV4+pU6eiSE7UavE2igxcbcoXu6UMH+rduzenTp3i9evXGp2f34gv0+TJk7G3t2fYsGHZkl0WL17MtGnTNN65QyS4CELx9K8KfACmpqasWbOGnTt34u3tTZ8+fXj+XPuyYqqIjIzk1atXtGzZUqftlitXjsWLFxMREYFMJsPOzg7DmLOU0Nfs16enyOD3dd9x7do1nfZTl8qWLYu7uzt79+7V6HxVA59EImH9+vXcu3cvS/3N69evExUVxYABAzR6fRAJLoJQXP3rAl8mV1dXbt68SfXq1XFwcGDHjh0qp69rateuXfTt21fjEUJ+zMzMWL16NdeuXePto9v8N3gzBqi3iN3YUI+5nvWYO3E4Hh4e/PDDDxolkRQGbaY7VQ188H5a8uDBg/zyyy8cO3YMgCVLljBlyhSMjDRfNiISXAShePqoFrBr6tq1awwbNgxLS0t8fX2pVq2azl9DoVBgY2PD3r171VrvpY3bt2/zn5928KC88/+v68t97jOnguBPnjzhq6++4vHjx/z222/Y29sXSr9VlZqairm5OVeuXMHa2lqtc1u1asXc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\n", 114 | "text/plain": [ 115 | "
" 116 | ] 117 | }, 118 | "metadata": { 119 | "tags": [] 120 | } 121 | } 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": { 127 | "id": "QKUOvlKvR9fp" 128 | }, 129 | "source": [ 130 | "### Community detection\n", 131 | "The goal of **community detection** is to find the hidden community / clusters inside a network. That is, it is a clustering problem with unknown number of clusters.\n", 132 | "\n", 133 | "One way to solve the community detection problem by maximizing a metric function called **modularity**, which can be defined as\n", 134 | "\n", 135 | "$Q(c):=\\frac{1}{2m}\\sum_{ij} \\left[a_{ij}-\\frac{d_id_j}{2m}\\right]\\mathbb{1}(c_i=c_j),$\n", 136 | "\n", 137 | "where $a_{ij}$ is the edge weight connecting nodes $i$ and $j$, \n", 138 | "$d_i=\\sum_j a_{ij}$ is the degree for node $i$, $m=\\sum_{ij}a_{ij}/2$ is the sum of edge weights,\n", 139 | "and $c_i\\in [r]$ is the community assignment for node $i$ among the $r$ possible communities. The higher the modularity, the more organized the community assignment. Thus, we can maximize the metric function to obtain a good community assignment. However, the maximization problem is NP-complete and highly.\n", 140 | "Thus, we proposed a smooth semidefinite relaxation to approximate the problem.\n", 141 | "\n", 142 | "### The locale relaxation\n", 143 | "The core idea of the Locale relaxation is that we can transform a Kronecker delta into a dot product\n", 144 | "\n", 145 | "$\\mathbb{1}(c_i=c_j) = v_i^T v_j,\\;\\text{where }\\; v_i\\in\\{e(1),e(2),\\ldots\\}$, and the set $\\{e(t)\\}$ are the standard basis. \n", 146 | "\n", 147 | "For example, if two nodes are in the same cluster, we can assign both of them as $e(1)$, so $\\mathbb{1}(c_i=c_j) = e(1)^Te(1) = 1$, and zero otherwise. \n", 148 | "\n", 149 | "The new encoding is exact and is still NP-complete, but we can further relax the standard basis into a smooth and continuous set containing the basis.\n", 150 | "![Alt text](https://raw.githubusercontent.com/locuslab/sdp_clustering/main/images/locale.png)\n", 151 | "Further, we can control the cardinality / sparsity of the relaxation to control the smoothness of the relaxed problem.\n", 152 | "We called the relaxation the **Lo**w-**ca**rdina**l**ity **e**mbedding, that is, the **Locale** relxation.\n", 153 | "\n", 154 | "## The API\n", 155 | "Here, we show the embedding obtained from the demo graph (*zachary.mtx*).\n", 156 | "The embeddings are sparse and presented as\n", 157 | "\n", 158 | "$\\text{index}_1:\\text{value}_1\\;\\;\\;\\;\\text{index}_2:\\text{value}_2 \\ldots$ \n", 159 | "\n", 160 | "for the 34 nodes in the graph." 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "metadata": { 166 | "colab": { 167 | "base_uri": "https://localhost:8080/" 168 | }, 169 | "id": "espamqlIK5q8", 170 | "outputId": "6152178c-9e83-4bda-87e6-e7064b6cecbd" 171 | }, 172 | "source": [ 173 | "from sdp_clustering import init_random_seed, leiden_locale, locale_embedding\n", 174 | "\n", 175 | "init_random_seed(1234)\n", 176 | "# obtain the embedding\n", 177 | "E = locale_embedding(graph, k=2, eps=1e-3, max_inner=10, verbose=0)\n", 178 | "print(E)" 179 | ], 180 | "execution_count": 5, 181 | "outputs": [ 182 | { 183 | "output_type": "stream", 184 | "text": [ 185 | "(1) 2:0.86\t7:0.52\t\n", 186 | "(2) 2:0.98\t8:0.21\t\n", 187 | "(3) 8:0.84\t2:0.54\t\n", 188 | "(4) 2:0.91\t8:0.41\t\n", 189 | "(5) 7:1.00\t6:0.10\t\n", 190 | "(6) 7:0.99\t6:0.11\t\n", 191 | "(7) 7:0.99\t6:0.11\t\n", 192 | "(8) 2:0.90\t8:0.43\t\n", 193 | "(9) 9:0.99\t8:0.13\t\n", 194 | "(10) 8:0.92\t9:0.39\t\n", 195 | "(11) 7:0.99\t6:0.10\t\n", 196 | "(12) 2:0.83\t7:0.56\t\n", 197 | "(13) 2:0.98\t8:0.18\t\n", 198 | "(14) 2:0.90\t8:0.44\t\n", 199 | "(15) 9:0.88\t10:0.48\t\n", 200 | "(16) 9:0.88\t10:0.48\t\n", 201 | "(17) 7:0.99\t6:0.14\t\n", 202 | "(18) 2:0.99\t7:0.16\t\n", 203 | "(19) 9:0.88\t10:0.48\t\n", 204 | "(20) 2:1.00\t7:0.03\t\n", 205 | "(21) 9:0.88\t10:0.48\t\n", 206 | "(22) 2:0.99\t7:0.16\t\n", 207 | "(23) 9:0.88\t10:0.48\t\n", 208 | "(24) 10:0.73\t24:0.69\t\n", 209 | "(25) 24:0.83\t32:0.55\t\n", 210 | "(26) 24:0.79\t32:0.61\t\n", 211 | "(27) 10:0.79\t9:0.62\t\n", 212 | "(28) 24:0.91\t10:0.42\t\n", 213 | "(29) 32:0.84\t8:0.54\t\n", 214 | "(30) 10:0.84\t9:0.55\t\n", 215 | "(31) 9:0.99\t10:0.13\t\n", 216 | "(32) 32:0.83\t24:0.55\t\n", 217 | "(33) 9:0.89\t10:0.46\t\n", 218 | "(34) 9:0.84\t10:0.54\t\n", 219 | "\n" 220 | ], 221 | "name": "stdout" 222 | } 223 | ] 224 | }, 225 | { 226 | "cell_type": "markdown", 227 | "metadata": { 228 | "id": "9SzIf1hbZapb" 229 | }, 230 | "source": [ 231 | "We show that the embeddings captures the cluster structure by showing their covariance matrix. The optimal assignment of the demo graph has 4 clusters, \n", 232 | "so correspondingly the optimal covariance graph has 4 blocks. " 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "metadata": { 238 | "colab": { 239 | "base_uri": "https://localhost:8080/", 240 | "height": 283 241 | }, 242 | "id": "nUs-47aALBZX", 243 | "outputId": "3eb6edf4-8591-4d93-b329-43be303675f0" 244 | }, 245 | "source": [ 246 | "# obtain clustering result\n", 247 | "labels = leiden_locale(graph)\n", 248 | "# re-order the embedding\n", 249 | "order = sorted(list(range(len(labels))), key=lambda x: labels[x])\n", 250 | "V = E.to_scipy()[order]\n", 251 | "# create the covariance matrix\n", 252 | "C = (V@V.T).todense()\n", 253 | "plt.imshow(C)" 254 | ], 255 | "execution_count": 6, 256 | "outputs": [ 257 | { 258 | "output_type": "execute_result", 259 | "data": { 260 | "text/plain": [ 261 | "" 262 | ] 263 | }, 264 | "metadata": { 265 | "tags": [] 266 | }, 267 | "execution_count": 6 268 | }, 269 | { 270 | "output_type": "display_data", 271 | "data": { 272 | "image/png": "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\n", 273 | "text/plain": [ 274 | "
" 275 | ] 276 | }, 277 | "metadata": { 278 | "tags": [], 279 | "needs_background": "light" 280 | } 281 | } 282 | ] 283 | }, 284 | { 285 | "cell_type": "markdown", 286 | "metadata": { 287 | "id": "JHnBnJacNLdl" 288 | }, 289 | "source": [ 290 | "## Command line utilities\n", 291 | "We can also use the command-line utilities **locale_alg** provided by the package to perform clustering and embedding. For example, to detect communities in Zachary Karate Club and output the result in labels.txt, run" 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "metadata": { 297 | "colab": { 298 | "base_uri": "https://localhost:8080/" 299 | }, 300 | "id": "rSpww8unxSiV", 301 | "outputId": "b5a322e6-1977-4b94-dc87-8c573c7424bd" 302 | }, 303 | "source": [ 304 | "!locale_alg {data} --out labels.txt --max_outer=3" 305 | ], 306 | "execution_count": 7, 307 | "outputs": [ 308 | { 309 | "output_type": "stream", 310 | "text": [ 311 | "iter 1(1)\topt fval 0.39981914\tn_comm 5\n", 312 | "iter 1(1)\trnd fval 0.21186720\tn_comm 12\n", 313 | "iter 1(2)\topt fval 0.41978961\tn_comm 4\n", 314 | "iter 1(2)\trnd fval 0.36875412\tn_comm 5\n", 315 | "iter 1(3)\topt fval 0.41978961\tn_comm 4\n", 316 | "iter 1(3)\trnd fval 0.41978961\tn_comm 4\n", 317 | "iter 1(4)\topt fval 0.41978961\tn_comm 4\n", 318 | "iter 1(4)\trnd fval 0.41978961\tn_comm 4\n", 319 | "\n", 320 | "iter 2(1)\topt fval 0.41978955\tn_comm 4\n", 321 | "iter 2(1)\trnd fval 0.23134451\tn_comm 11\n", 322 | "iter 2(2)\topt fval 0.41978961\tn_comm 4\n", 323 | "iter 2(2)\trnd fval 0.41978961\tn_comm 4\n", 324 | "iter 2(3)\topt fval 0.41978961\tn_comm 4\n", 325 | "iter 2(3)\trnd fval 0.41978961\tn_comm 4\n", 326 | "\n", 327 | "iter 3(1)\topt fval 0.41978961\tn_comm 4\n", 328 | "iter 3(1)\trnd fval 0.25969756\tn_comm 10\n", 329 | "iter 3(2)\topt fval 0.41978958\tn_comm 4\n", 330 | "iter 3(2)\trnd fval 0.41978961\tn_comm 4\n", 331 | "iter 3(3)\topt fval 0.41978961\tn_comm 4\n", 332 | "iter 3(3)\trnd fval 0.41978961\tn_comm 4\n", 333 | "\n" 334 | ], 335 | "name": "stdout" 336 | } 337 | ] 338 | }, 339 | { 340 | "cell_type": "markdown", 341 | "metadata": { 342 | "id": "eYqQxvEAbzbq" 343 | }, 344 | "source": [ 345 | "The output shows the modularity (fval) and number of clusters (n_comm) for each hierarchical iterations. Note that opt means the modularity after relaxation, and rnd is the result after rounding. We can see that the final modularity is 0.4198, which is optimal.\n", 346 | "\n", 347 | "To obtain embeddings, run" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "metadata": { 353 | "colab": { 354 | "base_uri": "https://localhost:8080/" 355 | }, 356 | "id": "XwZMS2AvxU42", 357 | "outputId": "41d191bb-6e3f-4220-c8e3-28d571353813" 358 | }, 359 | "source": [ 360 | "!locale_alg {data} --out emb.txt --embedding --verbose=0 --max_inner=10 --k=2\n", 361 | "!cat emb.txt" 362 | ], 363 | "execution_count": 8, 364 | "outputs": [ 365 | { 366 | "output_type": "stream", 367 | "text": [ 368 | "2:0.8434904217720032\t7:0.5371441841125488\t\n", 369 | "2:0.9853476881980896\t8:0.1705576628446579\t\n", 370 | "8:0.7674331068992615\t2:0.6411290764808655\t\n", 371 | "2:0.9597187042236328\t8:0.2809625267982483\t\n", 372 | "7:0.9988347291946411\t6:0.04826069995760918\t\n", 373 | "7:0.9983335137367249\t6:0.05770694091916084\t\n", 374 | "7:0.9985255599021912\t6:0.05428372323513031\t\n", 375 | "2:0.9383329749107361\t8:0.34573298692703247\t\n", 376 | "9:0.955811619758606\t10:0.2939799726009369\t\n", 377 | "8:0.9194113612174988\t9:0.39329734444618225\t\n", 378 | "7:0.9986326098442078\t6:0.05227864533662796\t\n", 379 | "2:0.8082109093666077\t7:0.5888931751251221\t\n", 380 | "2:0.9845686554908752\t7:0.1749989092350006\t\n", 381 | "2:0.9362026453018188\t8:0.35146087408065796\t\n", 382 | "9:0.8369048833847046\t10:0.5473482608795166\t\n", 383 | "9:0.8372026085853577\t10:0.5468928813934326\t\n", 384 | "7:0.997382640838623\t6:0.07230319827795029\t\n", 385 | "2:0.9853029847145081\t7:0.17081592977046967\t\n", 386 | "9:0.8374990820884705\t10:0.5464388132095337\t\n", 387 | "2:0.999264121055603\t7:0.03835659846663475\t\n", 388 | "9:0.8377942442893982\t10:0.5459861755371094\t\n", 389 | "2:0.9853131175041199\t7:0.17075775563716888\t\n", 390 | "9:0.8380880355834961\t10:0.5455349087715149\t\n", 391 | "24:0.7275087833404541\t10:0.6860983967781067\t\n", 392 | "24:0.8944594860076904\t32:0.44714900851249695\t\n", 393 | "24:0.8783431649208069\t32:0.47803062200546265\t\n", 394 | "10:0.7863637208938599\t9:0.617763876914978\t\n", 395 | "24:0.9432942867279053\t10:0.3319576382637024\t\n", 396 | "32:0.8080738186836243\t8:0.5890812277793884\t\n", 397 | "10:0.8409537076950073\t9:0.5411071181297302\t\n", 398 | "9:0.949730634689331\t10:0.31306809186935425\t\n", 399 | "32:0.7668662071228027\t24:0.641806960105896\t\n", 400 | "9:0.8350643515586853\t10:0.5501522421836853\t\n", 401 | "9:0.8096125721931458\t10:0.5869647264480591\t\n" 402 | ], 403 | "name": "stdout" 404 | } 405 | ] 406 | }, 407 | { 408 | "cell_type": "markdown", 409 | "metadata": { 410 | "id": "lqIll033cmvf" 411 | }, 412 | "source": [ 413 | "That's all! For more details, please refer to our [paper](https://arxiv.org/abs/2012.02676) and [github repo](https://github.com/locuslab/sdp_clustering)." 414 | ] 415 | }, 416 | { 417 | "cell_type": "code", 418 | "metadata": { 419 | "id": "dQlivnZ9c04z" 420 | }, 421 | "source": [ 422 | "" 423 | ], 424 | "execution_count": null, 425 | "outputs": [] 426 | } 427 | ] 428 | } -------------------------------------------------------------------------------- /sdp_clustering/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import locale_embedding, leiden_locale, init_random_seed 2 | 3 | __all__ = ['locale_embedding', 'leiden_locale', 'init_random_seed'] 4 | -------------------------------------------------------------------------------- /sdp_clustering/models.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.sparse import csr_matrix 3 | import sdp_clustering._cpp as _cpp 4 | 5 | class SparseMat(object): 6 | def __init__(self, indptr, indices, data): 7 | self.indptr = indptr 8 | self.indices = indices 9 | self.data = data 10 | 11 | def copy(self): 12 | return SparseMat(self.indptr.copy(), self.indices.copy(), self.data.copy()) 13 | 14 | @classmethod 15 | def from_scipy(cls, A): 16 | if not A is csr_matrix: 17 | A = A.tocsr() 18 | indptr = A.indptr 19 | indices = A.indices 20 | data = np.asarray(A.data, dtype=np.float32) 21 | 22 | return cls(indptr, indices, data) 23 | 24 | def to_scipy(self): 25 | return csr_matrix((self.data, self.indices, self.indptr)) 26 | 27 | @classmethod 28 | def zeros(cls, n, k): 29 | indptr = np.arange(0, n*k+1, k, dtype=np.int32) 30 | indices = np.zeros(n*k, dtype=np.int32) 31 | data = np.zeros(n*k, dtype=np.float32) 32 | 33 | return cls(indptr, indices, data) 34 | 35 | @classmethod 36 | def zeros_like(cls, mat, n=None): 37 | if n is None: n = mat.indptr.shape[0]-1 38 | indptr = np.zeros(n+1, dtype=np.int32) 39 | indices = np.zeros_like(mat.indices, dtype=np.int32) 40 | data = np.zeros_like(mat.data) 41 | 42 | return cls(indptr, indices, data) 43 | 44 | def __str__(self): 45 | n = self.indptr.shape[0]-1 46 | s = [] 47 | for i in range(n): 48 | s.append(f'({i+1}) ') 49 | for p in range(self.indptr[i], self.indptr[i+1]): 50 | if self.data[p].item() == 0.0: continue 51 | s.append(f'{self.indices[p].item()+1}:{self.data[p].item():1.2f}\t') 52 | s.append('\n') 53 | return ''.join(s) 54 | 55 | def savetxt(self, fname): 56 | f = open(fname, 'w') 57 | n = self.indptr.shape[0]-1 58 | for i in range(n): 59 | for p in range(self.indptr[i], self.indptr[i+1]): 60 | idx, val = self.indices[p].item()+1, self.data[p].item() 61 | if val == 0.0: continue 62 | f.write(f'{idx}:{val}\t') 63 | f.write('\n') 64 | f.close() 65 | 66 | def init_random_seed(seed): 67 | _cpp.init_random_seed(seed) 68 | 69 | def solve_locale(A, Adiag, k, comm=None, n_comm=None, max_iter=100, eps=1e-3, shrink=0, comm_init=0, rnd_card=1, verbose=False): 70 | n = A.indptr.shape[0]-1 71 | if k>n: k=n 72 | k_ = max(10, k) # preallocate for increased rank 73 | 74 | V = SparseMat.zeros(n,k) 75 | buf = np.zeros(n*k_*2) 76 | d, s, g = np.zeros(n), np.zeros(n*k_), np.zeros(n*k_) 77 | 78 | queue = np.zeros(n, dtype=np.int32) 79 | is_in = np.zeros(n, dtype=np.int32) 80 | 81 | if comm is None: comm = np.zeros(n, dtype=np.int32) 82 | else: comm = comm.copy() 83 | if n_comm is None: n_comm = np.zeros(1, dtype=np.int32) 84 | else: n_comm = np.array([n_comm], dtype=np.int32) 85 | 86 | fval = _cpp.solve_locale( 87 | max_iter, eps, 88 | A.indptr, A.indices, A.data, Adiag, 89 | V.indptr, V.indices, V.data, 90 | buf, s, d, g, 91 | queue, is_in, 92 | comm, n_comm, 93 | shrink, comm_init, rnd_card, verbose) 94 | 95 | return fval, comm, n_comm.item(), V 96 | 97 | def aggregate_clusters(A, Adiag, comm, n_comm): 98 | G = SparseMat.zeros_like(A, n_comm) 99 | Gdiag = np.zeros(n_comm) 100 | 101 | #print(A.indptr.shape, A.indices.shape, A.data.shape, Adiag.shape) 102 | #print(G.indptr.shape, G.indices.shape, G.data.shape, Gdiag.shape) 103 | #print(comm.shape) 104 | 105 | _cpp.aggregate_clusters( 106 | A.indptr, A.indices, A.data, Adiag, 107 | G.indptr, G.indices, G.data, Gdiag, 108 | comm) 109 | 110 | return G, Gdiag 111 | 112 | def merge_clusters(comm, comm_next, new_comm): 113 | #for i, ic in enumerate(new_comm.tolist()): 114 | # comm_next[ic] = comm[i] 115 | _cpp.merge(comm, comm_next, new_comm) 116 | 117 | def split_clusters(comm, comm_next): 118 | #for i, ic in enumerate(comm.tolist()): 119 | # comm[i] = comm_next[ic] 120 | _cpp.split(comm, comm_next) 121 | 122 | def locale_embedding(A, k=8, eps=1e-6, max_inner=10, verbose=False): 123 | A = SparseMat.from_scipy(A) 124 | n = len(A.indptr)-1 125 | Adiag = np.zeros(n) 126 | fval, _, _, V = solve_locale(A, Adiag, k, comm=None, eps=eps, max_iter=max_inner, comm_init=False, rnd_card=0, verbose=verbose) 127 | return V 128 | 129 | def leiden_locale(A, k=8, eps=1e-6, max_outer=10, max_lv=10, max_inner=2, verbose=0): 130 | A = SparseMat.from_scipy(A) 131 | n = len(A.indptr)-1 132 | Adiag = np.zeros(n) 133 | comm_init = None 134 | for it in range(max_outer): 135 | comms = [] 136 | G, Gdiag = A.copy(), Adiag.copy() 137 | for lv in range(max_lv): 138 | # LocaleEmbedding and LocaleRounding 139 | fval, comm, n_comm, V = solve_locale(G, Gdiag, k, comm=comm_init, eps=eps, max_iter=max_inner, comm_init=comm_init is not None, verbose=verbose) 140 | if verbose: print(f'iter {it+1}({lv+1})\topt fval {fval:.8f}\tn_comm {n_comm}') 141 | if 1: # LeidenRefine 142 | fval, new_comm, new_n_comm, V = solve_locale(G, Gdiag, k, comm=comm, n_comm=n_comm, eps=1e-4, max_iter=1, shrink=1, verbose=verbose) 143 | if verbose: print(f'iter {it+1}({lv+1})\trnd fval {fval:.8f}\tn_comm {new_n_comm}') 144 | else: # If k=1, this branch equals the Louvain algorithm 145 | new_comm, new_n_comm = comm.copy(), n_comm 146 | 147 | if new_n_comm == len(comm): break 148 | 149 | comm_init = np.zeros(new_n_comm, dtype=np.int32) 150 | merge_clusters(comm, comm_init, new_comm) 151 | 152 | # Aggregrate 153 | comms.append(new_comm.copy()) 154 | G, Gdiag = aggregate_clusters(G, Gdiag, new_comm, new_n_comm) 155 | 156 | for lv in reversed(range(len(comms)-1)): 157 | split_clusters(comms[lv], comms[lv+1]) 158 | comm_init = comms[0].copy() 159 | if verbose: print() 160 | 161 | return comm_init 162 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | DIR = os.path.abspath(os.path.dirname(__file__)) 4 | sys.path.append(os.path.join(DIR, "third_party", "pybind11")) 5 | print(sys.path) 6 | 7 | from glob import glob 8 | from setuptools import setup 9 | import pybind11 10 | from pybind11.setup_helpers import Pybind11Extension, build_ext # noqa: E402 11 | 12 | del sys.path[-1] 13 | 14 | pkg_name = 'sdp_clustering' 15 | ext_name = '_cpp' 16 | __version__ = "0.0.3" 17 | 18 | ext_modules = [ 19 | Pybind11Extension(pkg_name+'.'+ext_name, 20 | include_dirs = ['./src'], 21 | sources = sorted(glob('src/*.c*')), 22 | define_macros = [('EXTENSION_NAME', ext_name)], 23 | extra_compile_args = ['-O3', '-Wall', '-g'], 24 | ), 25 | ] 26 | 27 | setup( 28 | name=pkg_name, 29 | version=__version__, 30 | install_requires=['numpy', 'scipy'], 31 | author="Po-Wei Wang", 32 | author_email="poweiw@cs.cmu.edu", 33 | url="https://github.com/locuslab/sdp_clustering", 34 | description="SDP-based clustering by maximum modularity", 35 | long_description="", 36 | scripts=['bin/locale_alg'], 37 | ext_modules=ext_modules, 38 | extras_require={"test": "pytest"}, 39 | cmdclass={"build_ext": build_ext}, 40 | packages=[pkg_name], 41 | zip_safe=False, 42 | classifiers=[ 43 | "License :: OSI Approved :: MIT License", 44 | ], 45 | ) 46 | -------------------------------------------------------------------------------- /src/cluster.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #ifndef __unix__ 11 | #include 12 | #endif 13 | 14 | #include "cluster.h" 15 | 16 | #define MEPS 1e-20 17 | 18 | inline int min(int x, int y) { return (x<=y)?x:y; } 19 | 20 | #define NS_PER_SEC 1000000000 21 | int64_t wall_clock_ns() 22 | { 23 | #ifdef __unix__ 24 | struct timespec tspec; 25 | int r = clock_gettime(CLOCK_MONOTONIC, &tspec); 26 | assert(r==0); 27 | return tspec.tv_sec*NS_PER_SEC + tspec.tv_nsec; 28 | #else 29 | struct timeval tv; 30 | int r = gettimeofday( &tv, NULL ); 31 | assert(r==0); 32 | return tv.tv_sec*NS_PER_SEC + tv.tv_usec*1000; 33 | #endif 34 | } 35 | 36 | double wall_time_diff(int64_t ed, int64_t st) 37 | { 38 | return (double)(ed-st)/(double)NS_PER_SEC; 39 | } 40 | 41 | // ------------- Sparse BLAS-like utils -------------------- 42 | 43 | // perform y += a * xi for sparse matrix X 44 | void axipy(float *__restrict__ y, const float a, SparseMat X, int i) 45 | { 46 | int p = X.indptr[i]; 47 | const int ed = X.indptr[i+1]; 48 | const int *__restrict__ indices = X.indices; 49 | const float *__restrict__ data = X.data; 50 | 51 | for (; pval, vy = ((SparsePair*)py)->val; 121 | 122 | if (vx < vy) return 1; 123 | else if (vx > vy) return -1; 124 | else return 0; 125 | } 126 | #else 127 | bool val_cmp(const SparsePair &x, const SparsePair &y) 128 | { 129 | return x.val > y.val; 130 | } 131 | #endif 132 | 133 | int idx_cmp(const void *px, const void *py) 134 | { 135 | int vx = ((SparsePair*)px)->idx, vy = ((SparsePair*)py)->idx; 136 | 137 | if (vx < vy) return -1; 138 | else if (vx > vy) return 1; 139 | else return 0; 140 | } 141 | 142 | // ------------- randomization utils ------------------- 143 | 144 | void randperm(int *perm, int k) 145 | { 146 | for (int i=0; iis_in[x]) return; 192 | Q->queue[Q->rear] = x; 193 | Q->rear = (Q->rear + 1) % Q->cap; 194 | Q->len++; 195 | Q->is_in[x] = 1; 196 | } 197 | 198 | int ring_pop(Ring *Q) 199 | { 200 | int x = Q->queue[Q->front]; 201 | Q->front = (Q->front + 1) % Q->cap; 202 | Q->len--; 203 | Q->is_in[x] = 0; 204 | 205 | return x; 206 | } 207 | 208 | void ring_reset(Ring *Q) 209 | { 210 | //for (int i=0; icap; i++) Q->queue[i] = i; 211 | randperm(Q->queue, Q->cap); 212 | Q->len = Q->cap; 213 | Q->front = Q->rear = 0; 214 | for (int i=0; icap; i++) Q->is_in[i] = 1; 215 | } 216 | 217 | /* -------------- main algorihtm ------------------------*/ 218 | 219 | float solve_locale(int max_iter, float eps, 220 | int n, SparseMat A, float *Adiag, SparseMat V, 221 | SparsePair *buf, float *__restrict__ s, float *__restrict__ d, float *__restrict__ g, 222 | Ring *Q, int *comm, int *n_comm, int shrink, int comm_init, int rnd_card, int verbose) 223 | { 224 | int64_t time_st = wall_clock_ns(); 225 | if (verbose>1) fprintf(stderr, "n_comm %d\n", *n_comm); 226 | double fval = 0; 227 | double m = 0; 228 | for (int i=0; i1) fprintf(stderr, "k = %d m %lf\n", k, m); 263 | if (verbose>1) fprintf(stderr, "inner 0 fval %f time %.4e\n", fval, wall_time_diff(time_now, time_st)); 264 | 265 | int iter=0, first=0, nvisited=0; 266 | double delta = 0; 267 | int rank = n; 268 | 269 | ring_reset(Q); 270 | while (1) { 271 | if (nvisited >= n || Q->len == 0) { 272 | iter ++; 273 | fval += delta/(2*m); 274 | time_now = wall_clock_ns(); 275 | if (verbose>1) fprintf(stderr, "inner %d fval %.8e delta %.2e %s %s qlen %d time %.4e\n", iter, fval, delta/(2*m), shrink?"shrink":"", first?"first":"", Q->len, wall_time_diff(time_now, time_st)); 276 | double scaled_delta = fabs(delta/(2*m)); 277 | if ((!shrink && (scaled_delta < eps || iter>=max_iter)) || (shrink && (scaled_delta < eps || Q->len==0 || iter>=15))) { 278 | if (shrink) break; 279 | if (rnd_card == 0) return fval; 280 | shrink ^= 1; 281 | first = shrink; 282 | ring_reset(Q); 283 | } else { 284 | first = 0; 285 | } 286 | 287 | delta = 0, nvisited = 0; 288 | } 289 | 290 | int i = ring_pop(Q); 291 | int ic = comm[i]; 292 | nvisited++; 293 | 294 | if (A.indptr[i]==A.indptr[i+1]) continue; 295 | if (*n_comm) { // avoid singleton 296 | int nonsingleton = 0; 297 | for (int p=A.indptr[i]; pk)? k : nbuf; 329 | std::partial_sort(buf, buf+mid, buf+nbuf, val_cmp); 330 | int npos = nbuf; 331 | for (int q=nbuf-1; q>=0; q--) 332 | if (buf[q].val <= 0) npos--; 333 | 334 | float gv, gnrm; 335 | if (npos == 0) { // gv=0 at best 336 | if (old_gv > -MEPS) { // vi'g = 0 -> g[indices of vi] = 0 337 | buf[0] = {V.indices[V.indptr[i]], V.data[V.indptr[i]]}; 338 | } else if (buf[0].val == 0) { // g0 = 0 339 | buf[0].val = 1; 340 | } else { // increase rank 341 | buf[0] = {rank++, 1}; 342 | if(rank > n*10) {fprintf(stderr, "rank explode\n"); exit(0);} 343 | } 344 | gnrm = 1, gv = 0, npos = 1; 345 | } else { // update 346 | //if (shrink) randpick(buf, npos, g, first?-100:old_gv); 347 | npos = min(npos, V.indptr[i+1]-V.indptr[i]); 348 | if (shrink) npos = min(npos, rnd_card); 349 | gv = gnrm = snrm2_pairs(buf, npos); 350 | } 351 | 352 | if ( (gv - old_gv <= MEPS) && !first ) { // if not increasing, continue 353 | axipy(s, d[i], V, i); 354 | continue; 355 | } 356 | 357 | if (gnrm <= MEPS) { 358 | fprintf(stderr, "nbuf %d npos %d v0 %e v1 %e buf0 %g %d\n", nbuf, npos, V.data[V.indptr[i]], V.data[V.indptr[i]+1], buf[0].val, buf[0].val<0); 359 | fprintf(stderr, "i=%d gnrm %e gv %e old_gv %e gv-old_gv %e first %d shrink %d\n", i, gnrm, gv, old_gv, gv-old_gv, first, shrink); 360 | exit(0); 361 | } 362 | // copy vector from buf 363 | for (int p=V.indptr[i], q=0; p= npos) buf[q] = {0, 0}; 365 | V.indices[p] = buf[q].idx; 366 | V.data[p] = buf[q].val / gnrm; 367 | } 368 | axipy(s, d[i], V, i); 369 | 370 | delta += (gv - old_gv)*2; 371 | 372 | /* Update one block in Locale: End */ 373 | 374 | // push neighbors to queue 375 | for (int p=A.indptr[i]; p=0; i--) { 424 | G.indptr[i+1] = G.indptr[i]; 425 | } 426 | G.indptr[0] = 0; 427 | 428 | #if 0 429 | double mA = 0; 430 | for (int i=0; i 2 | #include 3 | 4 | #include "cluster.h" 5 | 6 | namespace py = pybind11; 7 | 8 | using arr = py::array; 9 | float *fptr(arr& a) { return (float*) a.mutable_data(); } 10 | int *iptr(arr& a) { return (int*) a.mutable_data(); } 11 | 12 | static int has_inited = 0; 13 | 14 | void init_random_seed(long int seed) 15 | { 16 | has_inited = 1; 17 | srand48(seed); 18 | srandom(seed); 19 | } 20 | 21 | SparseMat SparseMat_init(arr indptr, arr indices, arr data) 22 | { 23 | SparseMat X; 24 | X.indptr = iptr(indptr); 25 | X.indices = iptr(indices); 26 | X.data = fptr(data); 27 | return X; 28 | } 29 | 30 | float py_solve_locale(int max_iter, float eps, 31 | arr Aindptr, arr Aindices, arr Adata, arr Adiag, 32 | arr Vindptr, arr Vindices, arr Vdata, 33 | arr buf, arr s, arr d, arr g, 34 | arr queue, arr is_in, 35 | arr comm, arr n_comm, 36 | int shrink, int comm_init, 37 | int rnd_card, int verbose) 38 | { 39 | if (!has_inited) init_random_seed((long int) time(NULL)); 40 | SparseMat A = SparseMat_init(Aindptr, Aindices, Adata); 41 | SparseMat V = SparseMat_init(Vindptr, Vindices, Vdata); 42 | 43 | int n = Aindptr.shape(0)-1; 44 | Ring Q = {0, 0, 0, n, iptr(queue), iptr(is_in)}; 45 | 46 | float fval = solve_locale(max_iter, eps, 47 | n, A, fptr(Adiag), V, 48 | (SparsePair*)buf.mutable_data(), fptr(s), fptr(d), fptr(g), 49 | &Q, 50 | iptr(comm), iptr(n_comm), 51 | shrink, comm_init, 52 | rnd_card, verbose); 53 | return fval; 54 | } 55 | 56 | void py_aggregate_clusters( 57 | arr Aindptr, arr Aindices, arr Adata, arr Adiag, 58 | arr Gindptr, arr Gindices, arr Gdata, arr Gdiag, 59 | arr comm) 60 | { 61 | SparseMat A = SparseMat_init(Aindptr, Aindices, Adata); 62 | SparseMat G = SparseMat_init(Gindptr, Gindices, Gdata); 63 | 64 | int nA = Aindptr.shape(0)-1; 65 | int nG = Gindptr.shape(0)-1; 66 | aggregate_clusters( 67 | nA, A, fptr(Adiag), 68 | nG, G, fptr(Gdiag), 69 | iptr(comm)); 70 | } 71 | 72 | void py_merge(arr comm, arr comm_next, arr new_comm) 73 | { 74 | int n = comm.shape(0); 75 | merge(n, iptr(comm), iptr(comm_next), iptr(new_comm)); 76 | } 77 | 78 | void py_split(arr comm, arr comm_next) 79 | { 80 | int n = comm.shape(0); 81 | split(n, iptr(comm), iptr(comm_next)); 82 | } 83 | 84 | PYBIND11_MODULE(EXTENSION_NAME, m) { 85 | m.def("init_random_seed", &init_random_seed, "Init random seed"); 86 | m.def("solve_locale", &py_solve_locale, "Solve locale optimization"); 87 | m.def("aggregate_clusters", &py_aggregate_clusters, "Form hypergraph"); 88 | m.def("merge", &py_merge, "Merge (cpu)"); 89 | m.def("split", &py_split, "Split (cpu)"); 90 | } 91 | --------------------------------------------------------------------------------