├── .gitignore ├── LICENSE ├── README.md ├── cvxgraphalgs ├── __init__.py ├── algorithms │ ├── __init__.py │ ├── independent_set.py │ └── max_cut.py ├── generators │ ├── __init__.py │ ├── planted_models.py │ └── stochastic_block.py └── structures │ ├── __init__.py │ └── cut.py ├── docs └── notebooks │ ├── 1-goemans-williamson.ipynb │ └── 2-semirandom-independent-set.ipynb └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX generated files 2 | .DS_Store 3 | .DS_Store? 4 | ._* 5 | .Spotlight-V100 6 | .Trashes 7 | ehthumbs.db 8 | Thumbs.db 9 | 10 | # Pycharm files 11 | .idea/ 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # celery beat schedule file 91 | celerybeat-schedule 92 | 93 | # SageMath parsed files 94 | *.sage.py 95 | 96 | # Environments 97 | .env 98 | .venv 99 | env/ 100 | venv/ 101 | ENV/ 102 | env.bak/ 103 | venv.bak/ 104 | 105 | # Spyder project settings 106 | .spyderproject 107 | .spyproject 108 | 109 | # Rope project settings 110 | .ropeproject 111 | 112 | # mkdocs documentation 113 | /site 114 | 115 | # mypy 116 | .mypy_cache/ 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hermish 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVX Graph Algorithms 2 | 3 | ## Introduction 4 | 5 | > Modern convex optimization-based graph algorithms. 6 | 7 | Convex optimization presents an exciting new direction in designing exact and 8 | approximate graph algorithms. However, these algorithms are often overlooked in 9 | practice due to limitations in solving large convex programs quickly. 10 | Convex optimization-based graph algorithms nonetheless achieve impressive 11 | theoretical performance, and often provide a beautiful geometric interpretation. 12 | This package implements some of these algorithms and provides corresponding 13 | graph generators to test performance---hopefully highlighting how simple, 14 | elegant and effective these can be for many real-world problems. 15 | 16 | ## Details 17 | 18 | In this package, we provide implementations of the following algorithms. Note 19 | featured convex optimization-based algorithms are in bold and references are 20 | provided when available. 21 | 22 | This package also provides functions to generate graphs drawn from the planted 23 | independent set distribution and stochastic block model. 24 | 25 | 1. Maximum Cut Problem 26 | 1. **Goemans-Williamson MAX-CUT Algorithm** [1] 27 | 2. Random MAX-CUT Algorithm 28 | 3. Greedy MAX-CUT Algorithm 29 | 2. Independent Set Algorithm 30 | 1. **Crude SDP-based Independent Set** [2] 31 | 2. Greedy Independent Set Algorithm 32 | 3. Spectral Algorithm for Independent Set 33 | 34 | ## Install and Usage 35 | 36 | You can install this directly from the Python Package Index (PyPI). 37 | 38 | ``` 39 | pip install cvxgraphalgs 40 | ``` 41 | 42 | Below, we show how to run the Goemans-Williamson MAX-CUT Algorithm on a graph 43 | drawn from the stochastic block model distribution. For more examples, explore 44 | the jupyter notebooks available with the package documentation available 45 | [here](https://github.com/hermish/cvx-graph-algorithms/). 46 | 47 | ``` 48 | >>> import cvxgraphalgs as cvxgr 49 | >>> graph, _ = cvxgr.generators.bernoulli_planted_independent( 50 | ... size=50, independent_size=15, probability=0.5 51 | ... ) 52 | >>> recovered = cvxgr.algorithms.crude_sdp_independent_set(graph) 53 | >>> len(recovered) 54 | 15 55 | ``` 56 | 57 | ## References 58 | [1]: Goemans, Michel X., and David P. Williamson. "Improved approximation 59 | algorithms for maximum cut and satisfiability problems using semidefinite 60 | programming." *Journal of the ACM (JACM)* 42, no. 6 (1995): 1115-1145. 61 | 62 | [2]: McKenzie, Theo, Hermish Mehta, and Luca Trevisan. "A New Algorithm for the 63 | Robust Semi-random Independent Set Problem." *arXiv:1808.03633* (2018). 64 | -------------------------------------------------------------------------------- /cvxgraphalgs/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['algorithms', 'generators', 'structures'] 2 | __version__ = '0.1.2' 3 | __author__ = 'Hermish Mehta' 4 | 5 | import cvxgraphalgs.algorithms 6 | import cvxgraphalgs.generators 7 | import cvxgraphalgs.structures 8 | -------------------------------------------------------------------------------- /cvxgraphalgs/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | from cvxgraphalgs.algorithms.independent_set import * 2 | from cvxgraphalgs.algorithms.max_cut import * 3 | -------------------------------------------------------------------------------- /cvxgraphalgs/algorithms/independent_set.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cvxpy as cp 3 | import networkx as nx 4 | from scipy import linalg 5 | 6 | 7 | def greedy_independent_set(graph): 8 | """ 9 | :param graph: (nx.classes.graph.Graph) An undirected graph with no 10 | self-loops or multiple edges. The graph can either be weighted or 11 | unweighted, although the problem only differentiates between zero and 12 | non-zero weight edges. 13 | :return: (set) The independent set the algorithm outputs, represented as 14 | a set of vertices. 15 | """ 16 | independent = set() 17 | for vertex in graph.nodes: 18 | if not any(graph.has_edge(vertex, element) for element in independent): 19 | independent.add(vertex) 20 | return independent 21 | 22 | 23 | def crude_sdp_independent_set(graph): 24 | """ 25 | :param graph: (nx.classes.graph.Graph) An undirected graph with no 26 | self-loops or multiple edges. The graph can either be weighted or 27 | unweighted, although the problem only differentiates between zero and 28 | non-zero weight edges. 29 | :return: (set) The independent set the algorithm outputs, represented as 30 | a set of vertices. 31 | """ 32 | solution = _solve_vector_program(graph) 33 | labels = list(graph.nodes) 34 | candidates = _get_vector_clusters(labels, solution, 1.0) 35 | best = max(candidates, key=lambda cluster: len(cluster)) 36 | return best 37 | 38 | 39 | def _solve_vector_program(graph): 40 | """ 41 | :param graph: (nx.classes.graph.Graph) An undirected graph with no 42 | self-loops or multiple edges. The graph can either be weighted or 43 | unweighted, although the problem only differentiates between zero and 44 | non-zero weight edges. 45 | :return: (np.ndarray) A matrix whose columns represents the vectors assigned 46 | to each vertex to maximize the crude semi-definite program (C-SDP) 47 | objective. 48 | """ 49 | size = len(graph) 50 | products = cp.Variable((size, size), PSD=True) 51 | 52 | objective_matrix = size * np.eye(size) - np.ones((size, size)) 53 | objective = cp.Minimize(cp.trace(objective_matrix @ products)) 54 | 55 | adjacency = nx.linalg.adjacency_matrix(graph) 56 | adjacency = adjacency.toarray() 57 | constraints = [ 58 | cp.diag(products) == 1, 59 | products >= 0, 60 | cp.multiply(products, adjacency) == 0 61 | ] 62 | 63 | problem = cp.Problem(objective, constraints) 64 | problem.solve() 65 | assert problem.status == 'optimal' 66 | 67 | eigenvalues, eigenvectors = np.linalg.eigh(products.value) 68 | eigenvalues = np.maximum(eigenvalues, 0) 69 | diagonal_root = np.diag(np.sqrt(eigenvalues)) 70 | assignment = diagonal_root @ eigenvectors.T 71 | return assignment 72 | 73 | 74 | def _get_vector_clusters(labels, vectors, threshold): 75 | """ 76 | :param labels: (list) A list of labels. 77 | :param vectors: (np.ndarray) A matrix whose columns are the vectors 78 | corresponding to each label. Therefore, the label LABELS[i] references 79 | the vector VECTORS[:,i]; both lengths must be exactly the same, so 80 | len(VECTORS.T) == len(LABELS). 81 | :param threshold: (float | int) The closeness threshold. 82 | :return: (list) Return a list of sets. For each vector, this list includes a 83 | set which contains the labels of all vectors within a THRESHOLD-ball 84 | of the original. The list will contain exactly len(LABELS) entries, 85 | in the same order as LABELS. 86 | """ 87 | total = len(labels) 88 | clusters = [] 89 | 90 | for current in range(total): 91 | output = set() 92 | for other in range(total): 93 | if np.linalg.norm(vectors[:,current] - vectors[:,other]) <= threshold: 94 | output.add(labels[other]) 95 | clusters.append(output) 96 | return clusters 97 | 98 | 99 | def planted_spectral_algorithm(graph): 100 | """ 101 | :param graph: (nx.classes.graph.Graph) An undirected graph with no 102 | self-loops or multiple edges. The graph can either be weighted or 103 | unweighted, although the problem only differentiates between zero and 104 | non-zero weight edges. 105 | :return: (set) The independent set the algorithm outputs, represented as 106 | a set of vertices. 107 | """ 108 | size = len(graph) 109 | labels = list(graph.nodes) 110 | adjacency = nx.linalg.adjacency_matrix(graph) 111 | adjacency = adjacency.toarray() 112 | co_adjacency = 1 - adjacency 113 | 114 | ones_matrix = np.ones((size, size)) 115 | normalized = co_adjacency - 0.5 * ones_matrix 116 | _, eigenvector = linalg.eigh(normalized, eigvals=(size - 1, size - 1)) 117 | 118 | indices = list(range(size)) 119 | indices.sort(key=lambda num: eigenvector[num], reverse=True) 120 | 121 | output = set() 122 | for index in indices: 123 | vertex = labels[index] 124 | if not any(graph.has_edge(vertex, element) for element in output): 125 | output.add(vertex) 126 | return output 127 | -------------------------------------------------------------------------------- /cvxgraphalgs/algorithms/max_cut.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cvxpy as cp 3 | import networkx as nx 4 | 5 | from cvxgraphalgs.structures.cut import Cut 6 | 7 | 8 | def greedy_max_cut(graph): 9 | """ 10 | Runs a greedy MAX-CUT approximation algorithm to partition the vertices of 11 | the graph into two sets. This greedy approach delivers an approximation 12 | ratio of 0.5. 13 | 14 | :param graph: (nx.classes.graph.Graph) An undirected graph with no 15 | self-loops or multiple edges. The graph can either be weighted or 16 | unweighted, where each edge present is assigned an equal weight of 1. 17 | :return: (structures.cut.Cut) The cut returned by the algorithm as two 18 | sets, where each corresponds to a different side of the cut. Together, 19 | both sets contain all vertices in the graph, and each vertex is in 20 | exactly one of the two sets. 21 | """ 22 | cut = Cut(set(), set()) 23 | for vertex in graph.nodes: 24 | l_neighbors = sum((adj in cut.left) for adj in graph.neighbors(vertex)) 25 | r_neighbors = sum((adj in cut.right) for adj in graph.neighbors(vertex)) 26 | if l_neighbors < r_neighbors: 27 | cut.left.add(vertex) 28 | else: 29 | cut.right.add(vertex) 30 | return cut 31 | 32 | 33 | def random_cut(graph, probability): 34 | """ 35 | :param graph: (nx.classes.graph.Graph) A NetworkX graph. 36 | :param probability: (float) A number in [0, 1] which gives the probability 37 | each vertex lies on the right side of the cut. 38 | :return: (structures.cut.Cut) The random cut which results from randomly 39 | assigning vertices to either side independently at random according 40 | to the probability given above. 41 | """ 42 | size = len(graph) 43 | sides = np.random.binomial(1, probability, size) 44 | 45 | nodes = list(graph.nodes) 46 | left = {vertex for side, vertex in zip(sides, nodes) if side == 0} 47 | right = {vertex for side, vertex in zip(sides, nodes) if side == 1} 48 | return Cut(left, right) 49 | 50 | 51 | def goemans_williamson_weighted(graph): 52 | """ 53 | Runs the Goemans-Williamson randomized 0.87856-approximation algorithm for 54 | MAX-CUT on the graph instance, returning the cut. 55 | 56 | :param graph: (nx.classes.graph.Graph) An undirected graph with no 57 | self-loops or multiple edges. The graph can either be weighted or 58 | unweighted, where each edge present is assigned an equal weight of 1. 59 | :return: (structures.cut.Cut) The cut returned by the algorithm as two 60 | sets, where each corresponds to a different side of the cut. Together, 61 | both sets contain all vertices in the graph, and each vertex is in 62 | exactly one of the two sets. 63 | """ 64 | adjacency = nx.linalg.adjacency_matrix(graph) 65 | adjacency = adjacency.toarray() 66 | solution = _solve_cut_vector_program(adjacency) 67 | sides = _recover_cut(solution) 68 | 69 | nodes = list(graph.nodes) 70 | left = {vertex for side, vertex in zip(sides, nodes) if side < 0} 71 | right = {vertex for side, vertex in zip(sides, nodes) if side >= 0} 72 | return Cut(left, right) 73 | 74 | 75 | def _solve_cut_vector_program(adjacency): 76 | """ 77 | :param adjacency: (np.ndarray) A square matrix representing the adjacency 78 | matrix of an undirected graph with no self-loops. Therefore, the matrix 79 | must be symmetric with zeros along its diagonal. 80 | :return: (np.ndarray) A matrix whose columns represents the vectors assigned 81 | to each vertex to maximize the MAX-CUT semi-definite program (SDP) 82 | objective. 83 | """ 84 | size = len(adjacency) 85 | ones_matrix = np.ones((size, size)) 86 | products = cp.Variable((size, size), PSD=True) 87 | cut_size = 0.5 * cp.sum(cp.multiply(adjacency, ones_matrix - products)) 88 | 89 | objective = cp.Maximize(cut_size) 90 | constraints = [cp.diag(products) == 1] 91 | problem = cp.Problem(objective, constraints) 92 | problem.solve() 93 | 94 | eigenvalues, eigenvectors = np.linalg.eigh(products.value) 95 | eigenvalues = np.maximum(eigenvalues, 0) 96 | diagonal_root = np.diag(np.sqrt(eigenvalues)) 97 | assignment = diagonal_root @ eigenvectors.T 98 | return assignment 99 | 100 | 101 | def _recover_cut(solution): 102 | """ 103 | :param solution: (np.ndarray) A vector assignment of vertices, where each 104 | SOLUTION[:,i] corresponds to the vector associated with vertex i. 105 | :return: (np.ndarray) The cut from probabilistically rounding the 106 | solution, where -1 signifies left, +1 right, and 0 (which occurs almost 107 | surely never) either. 108 | """ 109 | size = len(solution) 110 | partition = np.random.normal(size=size) 111 | projections = solution.T @ partition 112 | 113 | sides = np.sign(projections) 114 | return sides 115 | -------------------------------------------------------------------------------- /cvxgraphalgs/generators/__init__.py: -------------------------------------------------------------------------------- 1 | from cvxgraphalgs.generators.stochastic_block import * 2 | from cvxgraphalgs.generators.planted_models import * 3 | -------------------------------------------------------------------------------- /cvxgraphalgs/generators/planted_models.py: -------------------------------------------------------------------------------- 1 | import random 2 | import itertools 3 | import numpy as np 4 | import networkx as nx 5 | 6 | 7 | def bernoulli_planted_independent(size, independent_size, probability): 8 | """ 9 | :param size: (int) The number of total vertices in the graph. 10 | :param independent_size: (int) The size of the planted independent set. 11 | :param probability: (float) The probability each "allowed" edge exists; 12 | must be between 0 and 1 inclusive. 13 | :return: (nx.classes.graph.Graph, set) A tuple consisting of a graph 14 | drawn from the planted independent set distribution on vertices 15 | {0, ..., SIZE - 1} and corresponding independent set, represented as 16 | a set of vertex labels. No edges exist between any vertices in the 17 | independent set. All other edges are present independently with 18 | probability PROBABILITY. 19 | """ 20 | vertices = list(range(size)) 21 | isolated = np.random.choice(size, independent_size, replace=False) 22 | isolated = set(isolated) 23 | 24 | graph = nx.Graph() 25 | graph.add_nodes_from(vertices) 26 | for start, end in itertools.combinations(graph.nodes, 2): 27 | if start not in isolated or end not in isolated: 28 | if random.random() < probability: 29 | graph.add_edge(start, end) 30 | return graph, isolated 31 | -------------------------------------------------------------------------------- /cvxgraphalgs/generators/stochastic_block.py: -------------------------------------------------------------------------------- 1 | import random 2 | import itertools 3 | import networkx as nx 4 | 5 | 6 | def stochastic_block_on_cut(cut, within, between): 7 | """ 8 | Returns a graph drawn from the Stochastic Block Model, on the vertices 9 | in CUT. Every edge between pairs of vertices in CUT.LEFT and CUT.RIGHT is 10 | present independently with probability WITHIN; edges between sides are 11 | similarly present independently with probability BETWEEN. 12 | 13 | :param cut: (structures.cut.Cut) A cut which represents the vertices in 14 | each of the two communities. Traditionally, the size of each side is 15 | exactly half the total number of vertices in the graph, denoted n. 16 | :param within: (float) The probability an edge exists between two vertices 17 | in the same community, denoted p. Must be between 0 and 1 inclusive. 18 | :param between: (float) The probability of each edge between two vertices 19 | in different communities, denoted q. Must be between 0 and 1 inclusive. 20 | :return: (nx.classes.graph.Graph) A graph drawn according to the Stochastic 21 | Block Model over the cut. 22 | """ 23 | graph = nx.Graph() 24 | graph.add_nodes_from(cut.vertices) 25 | 26 | for side in (cut.left, cut.right): 27 | for start, end in itertools.combinations(side, 2): 28 | if random.random() < within: 29 | graph.add_edge(start, end) 30 | 31 | for start in cut.left: 32 | for end in cut.right: 33 | if random.random() < between: 34 | graph.add_edge(start, end) 35 | 36 | return graph 37 | -------------------------------------------------------------------------------- /cvxgraphalgs/structures/__init__.py: -------------------------------------------------------------------------------- 1 | from cvxgraphalgs.structures.cut import * 2 | -------------------------------------------------------------------------------- /cvxgraphalgs/structures/cut.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import networkx as nx 3 | 4 | WEIGHT = 'weight' 5 | 6 | 7 | class Cut: 8 | """ 9 | A data structure representing a cut in a graph; formally, for a graph G = 10 | (V, E), this is a partition of the vertices (S, V - S). This class stores 11 | a cut as a pair or sets, corresponding to the left and right side of the 12 | cut respectively. 13 | """ 14 | 15 | def __init__(self, left, right): 16 | """ 17 | :param left: (set) Vertices on the left side of the cut. 18 | :param right: (set) Vertices on the right side of the cut. 19 | """ 20 | self.left = left 21 | self.right = right 22 | self.vertices = list(itertools.chain(left, right)) 23 | 24 | def validate_cut(self, graph): 25 | """ 26 | :param graph: (nx.classes.graph.Graph) A NetworkX graph. 27 | :return: (NoneType) Ensures the left and right compose a valid cut of 28 | the graph so each vertex in the graph is in exactly one of these two 29 | sets. 30 | """ 31 | size = len(graph) 32 | left_size, right_size = len(self.left), len(self.right) 33 | assert left_size + right_size == size 34 | 35 | for vertex in graph.nodes(): 36 | assert vertex in self.left or vertex in self.right 37 | 38 | def evaluate_cut_size(self, graph): 39 | """ 40 | :param graph: (nx.classes.graph.Graph) A NetworkX graph. 41 | :return: (float | int) Returns the size of the cut, or more precisely 42 | the sum of the weights of the edges between right and left. When the 43 | graph is unweighted, edge weights are taken to be 1, so this counts 44 | the total edges between sides of the cut. 45 | """ 46 | self.validate_cut(graph) 47 | graph_weighted = nx.is_weighted(graph) 48 | total, weight = 0, 1 49 | 50 | for edge in graph.edges(): 51 | start, end = edge 52 | forward_order = start in self.left and end in self.right 53 | reverse_order = start in self.right and end in self.left 54 | if forward_order or reverse_order: 55 | if graph_weighted: 56 | weight = graph[start][end][WEIGHT] 57 | total += weight 58 | return total 59 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('README.md', 'r') as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name='cvxgraphalgs', 8 | version='0.1.2', 9 | packages=setuptools.find_packages(), 10 | install_requires=[ 11 | 'numpy', 12 | 'scipy', 13 | 'cvxpy', 14 | 'networkx' 15 | ], 16 | 17 | author='Hermish Mehta', 18 | author_email='hermishdm@gmail.com', 19 | keywords='graph algorithms theory convex optimization', 20 | description='Modern convex optimization-based graph algorithms.', 21 | long_description=long_description, 22 | long_description_content_type='text/markdown', 23 | url='https://github.com/hermish/cvx-graph-algorithms', 24 | classifiers=[ 25 | 'Programming Language :: Python :: 3', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Operating System :: OS Independent', 28 | ] 29 | ) 30 | --------------------------------------------------------------------------------