Skip to content

Commit

Permalink
Merge pull request #3 from qtothec/joglekar_community_detection
Browse files Browse the repository at this point in the history
Suggested edits
  • Loading branch information
rahuljoglekar47 authored May 26, 2020
2 parents 7de3e80 + 3c3cbd2 commit bc65026
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 143 deletions.
9 changes: 9 additions & 0 deletions doc/OnlineDocs/contributed_packages/community.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Community Detection for Pyomo models
====================================

Take a look at the other files near this one to see how
they document their capabilities and structure documentation.

To build the documentation locally for testing, use Sphinx.

I would include a couple of nice pictures here. :D
1 change: 1 addition & 0 deletions doc/OnlineDocs/contributed_packages/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Contributed packages distributed with Pyomo:
mcpp.rst
mindtpy.rst
satsolver.rst
community.rst

Contributed packages distributed independently of Pyomo, but accessible
through ``pyomo.contrib``:
Expand Down
5 changes: 2 additions & 3 deletions pyomo/contrib/community_detection/community_graph.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""Model Graph Generator Code - Rahul Joglekar"""

from pyomo.environ import *
from pyomo.common.dependencies import networkx as nx
from pyomo.core import Constraint, Objective, Var
from pyomo.core.expr.current import identify_variables
from itertools import combinations
import os
import networkx as nx
import logging


Expand Down
177 changes: 57 additions & 120 deletions pyomo/contrib/community_detection/detection.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,61 @@
"""Community Detection Code - Rahul Joglekar"""
from pyomo.core import ConcreteModel
from contrib.community_detection import community_graph
import community
import logging
"""
Main module for community detection integration with Pyomo models.
This module separates model variables or constraints into different communities
distinguished by the degree of connectivity between community members.
def detect_communities(model, node_type='v', with_objective=True, weighted_graph=True, file_destination=None,
log_level=logging.WARNING, random_seed=None):
Original implementation developed by Rahul Joglekar in the Grossmann research group.
"""
from logging import getLogger

from pyomo.common.dependencies import attempt_import
from pyomo.contrib.community_detection.community_graph import _generate_model_graph

logger = getLogger('pyomo.contrib.community_detection')

# Yikes, the naming. I hope the user doesn't have another package installed named 'community'
community, community_available = attempt_import(
'community', error_message="Could not import the 'community' library, available via 'python-louvain' on PyPI.")


def detect_communities(model, node_type='v', with_objective=True, weighted_graph=True, random_seed=False):
"""
Detects communities in a Pyomo optimization model
This function takes in a Pyomo optimization model, organizes the variables and constraints into a graph of nodes
and edges, and then by using Louvain community detection on the graph, a dictionary is ultimately created, mapping
the communities to the nodes in each community.
Args:
model (Block): a Pyomo model or block to be used for community detection
node_type : a string that specifies the dictionary to be returned; 'v' returns a dictionary with communities
based on variable nodes, 'c' returns a dictionary with communities based on constraint nodes, and any other
input returns an error message
with_objective: a Boolean argument that specifies whether or not the objective function will be
Parameters
----------
model: Block
a Pyomo model or block to be used for community detection
node_type: str
A string that specifies the dictionary to be returned.
'v' returns a dictionary with communities based on variable nodes,
'c' returns a dictionary with communities based on constraint nodes.
with_objective: bool
a Boolean argument that specifies whether or not the objective function will be
treated as a node/constraint (depending on what node_type is specified as (see prior argument))
weighted_graph: a Boolean argument that specifies whether a weighted or unweighted graph is to be
weighted_graph: bool
a Boolean argument that specifies whether a weighted or unweighted graph is to be
created from the Pyomo model
file_destination: an optional argument that takes in a path if the user wants to save an edge and adjacency
list based on the model
log_level: determines the minimum severity of an event for it to be included in the event logger file; can be
specified as any of the following (in order of increasing severity): logging.DEBUG, logging.INFO,
logging.WARNING, logging.ERROR, logging.CRITICAL; These levels correspond to integer values of 10, 20, 30, 40,
and 50 (respectively). Thus, log_level can also be specified as an integer.
random_seed : takes in an integer to use as the seed number for the heuristic Louvain community detection
Returns:
community_map: a Python dictionary whose keys are integers from zero to the number of communities minus one
random_seed: int, optional
Specify the integer to use the random seed for the heuristic Louvain community detection
Returns
-------
community_map: dict
a Python dictionary whose keys are integers from zero to the number of communities minus one
with values that are sorted lists of the nodes in the given community
"""

# Use this function as a check to make sure all of the arguments are of the correct type, else return None
if check_for_correct_arguments(model, node_type, with_objective, weighted_graph, file_destination, log_level,
random_seed) is False:
return None
assert node_type in ('v', 'c'), "Invalid node type specified. Valid values: 'v', 'c'."

# Generate the model_graph (a networkX graph) based on the given Pyomo optimization model
model_graph = community_graph._generate_model_graph(model, node_type=node_type, with_objective=with_objective,
weighted_graph=weighted_graph,
file_destination=file_destination)
model_graph = _generate_model_graph(
model, node_type=node_type, with_objective=with_objective,
weighted_graph=weighted_graph, )

# Use Louvain community detection to determine which community each node belongs to
partition_of_graph = community.best_partition(model_graph, random_state=random_seed)
Expand All @@ -58,99 +69,25 @@ def detect_communities(model, node_type='v', with_objective=True, weighted_graph
community_map[nth_community].append(node)

# Log information about the number of communities found from the model
logging.info("%s communities were found in the model" % number_of_communities)
logger.info("%s communities were found in the model" % number_of_communities)
if number_of_communities == 0:
logging.error("in detect_communities: Empty community map was returned")
logger.error("in detect_communities: Empty community map was returned")
if number_of_communities == 1:
logging.warning("Community detection found that with the given parameters, the model could not be decomposed - "
"only one community was found")
logger.warning("Community detection found that with the given parameters, the model could not be decomposed - "
"only one community was found")

return community_map


def check_for_correct_arguments(model, node_type, with_objective, weighted_graph, file_destination, log_level,
random_seed):
"""
Determines whether the arguments given are of the correct types
This function takes in the arguments given to the function detect_communities and tests whether or not all of them
are of the correct type. If they are not, the function detect_communities will return None and the incorrect
arguments will be logged by the event logger as errors.
Args:
model (Block): a Pyomo model or block to be used for community detection
node_type : a string that specifies the dictionary to be returned; 'v' returns a dictionary with communities
based on variable nodes, 'c' returns a dictionary with communities based on constraint nodes, and any other
input returns an error message
with_objective: a Boolean argument that specifies whether or not the objective function will be
treated as a node/constraint (depending on what node_type is specified as (see prior argument))
weighted_graph: a Boolean argument that specifies whether a weighted or unweighted graph is to be
created from the Pyomo model
file_destination: an optional argument that takes in a path if the user wants to save an edge and adjacency
def write_community_to_file(community_map, filename):
"""Writes the community edge and adjacency lists to a file.
Parameters
----------
community_map
filename
an optional argument that takes in a path if the user wants to save an edge and adjacency
list based on the model
log_level: determines the minimum severity of an event for it to be included in the event logger file; can be
specified as any of the following (in order of increasing severity): logging.DEBUG, logging.INFO,
logging.WARNING, logging.ERROR, logging.CRITICAL; These levels correspond to integer values of 10, 20, 30, 40,
and 50 (respectively). Thus, log_level can also be specified as an integer.
random_seed : takes in an integer to use as the seed number for the heuristic Louvain community detection
Returns:
correct_arguments: a Boolean that indicates whether the arguments are of the correct type
"""
# Assume the given arguments are all of the correct type and set this indicator variable to True
correct_arguments = True

# Check log_level
if not isinstance(log_level, int):
# Configure logger so that the error message is properly formatted
logging.basicConfig(filename='community_detection_event_log.log', format='%(levelname)s:%(message)s',
filemode='w', level=logging.WARNING)
logging.error(" Invalid argument for function detect_communities: 'log_level=%s' (log_level must be "
"of type int)" % log_level)
correct_arguments = False

# If the log_level is an int, then configure the logger as specified by the user
else:
logging.basicConfig(filename='community_detection_event_log.log', format='%(levelname)s:%(message)s',
filemode='w', level=log_level)

# Check that model is a ConcreteModel
if not isinstance(model, ConcreteModel):
logging.error(" Invalid argument for function detect_communities: 'model=%s' (model must be of type "
"ConcreteModel)" % model)
correct_arguments = False

# Check node_type
if node_type != 'v' and node_type != 'c':
logging.error(" Invalid argument for function detect_communities: 'node_type=%s' (node_type must be "
"'v' or 'c')" % node_type)
correct_arguments = False

# Check with_objective
if not isinstance(with_objective, bool):
logging.error(" Invalid argument for function detect_communities: 'with_objective=%s' (weighted_graph must be "
"a Boolean)" % with_objective)
correct_arguments = False

# Check weighted_graph
if not isinstance(weighted_graph, bool):
logging.error(" Invalid argument for function detect_communities: 'weighted_graph=%s' (with_objective must be "
"a Boolean)" % weighted_graph)
correct_arguments = False

# Check file_destination
if file_destination is not None and not isinstance(file_destination, str):
logging.error(" Invalid argument for function detect_communities: 'file_destination=%s' (file_destination must "
"be a string)" % file_destination)
correct_arguments = False

# Check random_seed
if random_seed is not None and not isinstance(random_seed, int):
logging.error(" Invalid argument for function detect_communities: 'random_seed=%s' (random_seed must be "
"of type int)" % random_seed)
correct_arguments = False

# At this point, if any arguments were not of the correct type, then correct_arguments will be False; if all of the
# arguments are of the correct type, then correct_arguments will be true
return correct_arguments
"""
pass
2 changes: 2 additions & 0 deletions pyomo/contrib/community_detection/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def load():
import pyomo.contrib.community_detection.detection
31 changes: 17 additions & 14 deletions pyomo/contrib/community_detection/tests/test_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@
# ___________________________________________________________________________

from __future__ import division

import logging

import pyutilib.th as unittest
from six import StringIO

from pyomo.environ import *
from pyomo.common.dependencies import networkx_available
from pyomo.common.log import LoggingIntercept
from pyomo.environ import ConcreteModel, Constraint, Integers, minimize, Objective, Var
from pyomo.contrib.community_detection.detection import *

from pyomo.solvers.tests.models.LP_unbounded import LP_unbounded
Expand All @@ -23,6 +29,8 @@
from pyomo.solvers.tests.models.SOS1_simple import SOS1_simple


@unittest.skipUnless(community_available, "'community' package from 'python-louvain' is not available.")
@unittest.skipUnless(networkx_available, "networkx is not available.")
class TestDecomposition(unittest.TestCase):

def test_communities_1(self):
Expand Down Expand Up @@ -255,19 +263,14 @@ def test_communities_6(self):

def test_communities_7(self):
model = create_model_6()
empty_model = detect_communities(ConcreteModel())
bad_model = detect_communities(2)
bad_node_type = detect_communities(model, node_type=[])
bad_objective = detect_communities(model, with_objective='c')
bad_weighted_graph = detect_communities(model, weighted_graph=set())
bad_file_destination = detect_communities(model, file_destination=dict())
bad_log_level = detect_communities(model, log_level=[])
bad_seed_value = detect_communities(model, random_seed='v')

test_results = (empty_model, bad_model, bad_node_type, bad_objective, bad_weighted_graph, bad_file_destination,
bad_log_level, bad_seed_value)

correct_community_maps = ({}, None, None, None, None, None, None, None)

output = StringIO()
with LoggingIntercept(output, 'pyomo.contrib.community_detection', logging.ERROR):
detect_communities(ConcreteModel())
self.assertIn('Empty community map was returned', output.getvalue())

with self.assertRaisesRegex(AssertionError, "Invalid node type specified. Valid values: 'v', 'c'."):
detect_communities(model, node_type='foo')


def create_model_5(): # MINLP written by GAMS Convert at 05/10/19 14:22:56
Expand Down
16 changes: 10 additions & 6 deletions pyomo/environ/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
# ___________________________________________________________________________

import sys as _sys

if _sys.version_info[0] >= 3:
import importlib


def _do_import(pkg_name):
importlib.import_module(pkg_name)
else:
Expand Down Expand Up @@ -45,7 +47,7 @@ def _do_import(pkg_name):
# we silently ignore any import errors because these
# packages are optional and/or under development.
#
_optional_packages = set([
_optional_packages = {
'pyomo.contrib.example',
'pyomo.contrib.fme',
'pyomo.contrib.gdpbb',
Expand All @@ -58,21 +60,22 @@ def _do_import(pkg_name):
'pyomo.contrib.preprocessing',
'pyomo.contrib.pynumero',
'pyomo.contrib.trustregion',
])
'pyomo.contrib.community_detection',
}


def _import_packages():
#
# Import required packages
#
for name in _packages:
pname = name+'.plugins'
pname = name + '.plugins'
try:
_do_import(pname)
except ImportError:
exctype, err, tb = _sys.exc_info() # BUG?
import traceback
msg = "pyomo.environ failed to import %s:\nOriginal %s: %s\n"\
msg = "pyomo.environ failed to import %s:\nOriginal %s: %s\n" \
"Traceback:\n%s" \
% (pname, exctype.__name__, err,
''.join(traceback.format_tb(tb)),)
Expand All @@ -88,14 +91,15 @@ def _import_packages():
# Import optional packages
#
for name in _optional_packages:
pname = name+'.plugins'
pname = name + '.plugins'
try:
_do_import(pname)
except ImportError:
continue
pkg = _sys.modules[pname]
pkg.load()


_import_packages()

#
Expand All @@ -106,5 +110,5 @@ def _import_packages():
from pyomo.opt import (
SolverFactory, SolverManagerFactory, UnknownSolver,
TerminationCondition, SolverStatus,
)
)
from pyomo.core.base.units_container import units

0 comments on commit bc65026

Please sign in to comment.