Skip to content

Commit

Permalink
Add Random topology class (#155)
Browse files Browse the repository at this point in the history
Reference: #129 

Added a new topology with random neighbors. Added documentation and
a test file for it and reworked the documentation of the GeneralOptimizer 
class to incorporate all available topologies. Simplified the fixture function 
for the GeneralOptimizer class. Cited the relevant paper for the 
algorithm implemented.

Updated the documentation of the Random class. 
Especially the __compute_neighbor() method. Added the comments
inside the method to the docstring and deleted irrelevant comments.
Changed the nested for-loops to one loop with the itertools library.
Added a new test for the return value of the __compute_neighbor() 
method, which checks the shape and the symmetry.
Added a new test for the return value of the __compute_neighbors() 
method, which compares the returned matrix with a preset comparison 
matrix using a seed.

Signed-off-by: Lester James V. Miranda <[email protected]>
Committed-by: @whzup
  • Loading branch information
whzup authored and ljvmiranda921 committed Jul 14, 2018
1 parent 3d5e346 commit cca8d3c
Show file tree
Hide file tree
Showing 9 changed files with 408 additions and 69 deletions.
9 changes: 9 additions & 0 deletions docs/api/pyswarms.topology.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,12 @@ pyswarms.backend.topology.pyramid module
:undoc-members:
:show-inheritance:
:special-members: __init__

pyswarms.backend.topology.random module
--------------------------------------

.. automodule:: pyswarms.backend.topology.random
:members:
:undoc-members:
:show-inheritance:
:special-members: __init__
3 changes: 2 additions & 1 deletion pyswarms/backend/topology/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .star import Star
from .ring import Ring
from .pyramid import Pyramid
from .random import Random


__all__ = ["Topology", "Star", "Ring", "Pyramid"]
__all__ = ["Topology", "Star", "Ring", "Pyramid", "Random"]
2 changes: 1 addition & 1 deletion pyswarms/backend/topology/pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def compute_gbest(self, swarm):
# Insert all the neighbors for each particle in the idx array
idx = np.array([index_pointer[indices[i]:indices[i+1]] for i in range(swarm.n_particles)])
idx_min = np.array([swarm.pbest_cost[idx[i]].argmin() for i in range(idx.size)])
best_neighbor = np.array([idx[i][idx_min[i]] for i in range(idx.size)]).astype(int)
best_neighbor = np.array([idx[i][idx_min[i]] for i in range(len(idx))]).astype(int)

# Obtain best cost and position
best_cost = np.min(swarm.pbest_cost[best_neighbor])
Expand Down
201 changes: 201 additions & 0 deletions pyswarms/backend/topology/random.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-

"""
A Random Network Topology
This class implements a random topology. All particles are connected in a random fashion.
"""

# Import from stdlib
import logging
import itertools

# Import modules
import numpy as np
from scipy.sparse.csgraph import connected_components, dijkstra

# Import from package
from ..import operators as ops
from .base import Topology

# Create a logger
logger = logging.getLogger(__name__)


class Random(Topology):
def __init__(self):
super(Random, self).__init__()

def compute_gbest(self, swarm, k):
"""Update the global best using a random neighborhood approach
This uses random class from :code:`numpy` to give every particle k
randomly distributed, non-equal neighbors. The resulting topology
is a connected graph. The algorithm to obtain the neighbors was adapted
from [TSWJ2013].
[TSWJ2013] Qingjian Ni and Jianming Deng, “A New Logistic Dynamic
Particle Swarm Optimization Algorithm Based on Random Topology,”
The Scientific World Journal, vol. 2013, Article ID 409167, 8 pages, 2013.
https://doi.org/10.1155/2013/409167.
Parameters
----------
swarm : pyswarms.backend.swarms.Swarm
a Swarm instance
k : int
number of neighbors to be considered. Must be a
positive integer less than :code:`n_particles-1`
Returns
-------
numpy.ndarray
Best position of shape :code:`(n_dimensions, )`
float
Best cost
"""
try:
adj_matrix = self.__compute_neighbors(swarm, k)
idx = np.array([adj_matrix[i].nonzero()[0] for i in range(swarm.n_particles)])
idx_min = np.array([swarm.pbest_cost[idx[i]].argmin() for i in range(len(idx))])
best_neighbor = np.array([idx[i][idx_min[i]] for i in range(len(idx))]).astype(int)

# Obtain best cost and position
best_cost = np.min(swarm.pbest_cost[best_neighbor])
best_pos = swarm.pbest_pos[
np.argmin(swarm.pbest_cost[best_neighbor])
]

except AttributeError:
msg = "Please pass a Swarm class. You passed {}".format(
type(swarm)
)
logger.error(msg)
raise
else:
return (best_pos, best_cost)

def compute_velocity(self, swarm, clamp=None):
"""Compute the velocity matrix
This method updates the velocity matrix using the best and current
positions of the swarm. The velocity matrix is computed using the
cognitive and social terms of the swarm.
A sample usage can be seen with the following:
.. code-block :: python
import pyswarms.backend as P
from pyswarms.swarms.backend import Swarm
from pyswarms.backend.topology import Random
my_swarm = P.create_swarm(n_particles, dimensions)
my_topology = Random()
for i in range(iters):
# Inside the for-loop
my_swarm.velocity = my_topology.update_velocity(my_swarm, clamp)
Parameters
----------
swarm : pyswarms.backend.swarms.Swarm
a Swarm instance
clamp : tuple of floats (default is :code:`None`)
a tuple of size 2 where the first entry is the minimum velocity
and the second entry is the maximum velocity. It
sets the limits for velocity clamping.
Returns
-------
numpy.ndarray
Updated velocity matrix
"""
return ops.compute_velocity(swarm, clamp)

def compute_position(self, swarm, bounds=None):
"""Update the position matrix
This method updates the position matrix given the current position and
the velocity. If bounded, it waives updating the position.
Parameters
----------
swarm : pyswarms.backend.swarms.Swarm
a Swarm instance
bounds : tuple of :code:`np.ndarray` or list (default is :code:`None`)
a tuple of size 2 where the first entry is the minimum bound while
the second entry is the maximum bound. Each array must be of shape
:code:`(dimensions,)`.
Returns
-------
numpy.ndarray
New position-matrix
"""
return ops.compute_position(swarm, bounds)

def __compute_neighbors(self, swarm, k):
"""Helper method to compute the adjacency matrix of the topology
This method computes the adjacency matrix of the topology using
the randomized algorithm proposed in [TSWJ2013]. The resulting
topology is a connected graph. This is achieved by creating three
matrices:
* adj_matrix : The adjacency matrix of the generated graph.
It's initialized as an identity matrix to
make sure that every particle has itself as
a neighbour. This matrix is the return
value of the method.
* neighbor_matrix : The matrix of randomly generated neighbors.
This matrix is a matrix of shape
:code:`(swarm.n_particles, k)`:
with randomly generated elements. It's used
to create connections in the adj_matrix.
* dist_matrix : The distance matrix computed with Dijkstra's
algorithm. It is used to determine where the
graph needs edges to change it to a connected
graph.
.. note:: If the graph isn't connected, it is possible that the
PSO algorithm does not find the best position within
the swarm.
Parameters
----------
swarm : pyswarms.backend.swarms.Swarm
a Swarm instance
k : int
number of neighbors to be considered. Must be a
positive integer less than :code:`n_particles-1`
Returns
-------
numpy.ndarray
Adjacency matrix of the topology
"""

adj_matrix = np.identity(swarm.n_particles, dtype=int)

neighbor_matrix = np.array(
[np.random.choice(
# Exclude i from the array
np.setdiff1d(
np.arange(swarm.n_particles), np.array([i])
), k, replace=False
) for i in range(swarm.n_particles)])

# Set random elements to one using the neighbor matrix
adj_matrix[np.arange(swarm.n_particles).reshape(swarm.n_particles, 1), neighbor_matrix] = 1
adj_matrix[neighbor_matrix, np.arange(swarm.n_particles).reshape(swarm.n_particles, 1)] = 1

dist_matrix = dijkstra(adj_matrix, directed=False, return_predecessors=False, unweighted=True)

# Generate connected graph.
while connected_components(adj_matrix, directed=False, return_labels=False) != 1:
for i, j in itertools.product(range(swarm.n_particles), repeat=2):
if dist_matrix[i][j] == 0:
adj_matrix[i][j] = 1

return adj_matrix
59 changes: 44 additions & 15 deletions pyswarms/single/general_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
# Import from package
from ..base import SwarmOptimizer
from ..backend.operators import compute_pbest
from ..backend.topology import Topology, Ring
from ..backend.topology import Topology, Ring, Random
from ..utils.console_utils import cli_print, end_report


Expand Down Expand Up @@ -98,18 +98,29 @@ def __init__(
social parameter
* w : float
inertia parameter
if used with the :code:`Ring` topology the additional
parameters k and p must be included
if used with the :code:`Ring` or :code:`Random` topology the additional
parameter k must be included
* k : int
number of neighbors to be considered. Must be a
positive integer less than :code:`n_particles`
if used with the :code:`Ring` topology the additional
parameter p must be included
* p: int {1,2}
the Minkowski p-norm to use. 1 is the
sum-of-absolute values (or L1 distance) while 2 is
the Euclidean (or L2) distance.
topology : :code:`Topology` object
topology : pyswarms.backend.topology.Topology
a :code:`Topology` object that defines the topology to use
in the optimization process
in the optimization process. The currently available topologies
are:
* Star
All particles are connected
* Ring
Particles are connected with the k nearest neighbours
* Pyramid
Particles are connected in N-dimensional simplices
* Random
Particles are connected to k random particles
bounds : tuple of :code:`np.ndarray` (default is :code:`None`)
a tuple of size 2 where the first entry is the minimum bound
while the second entry is the maximum bound. Each array must
Expand Down Expand Up @@ -149,22 +160,33 @@ def __init__(

# Case for the Ring topology
if isinstance(topology, Ring):
# Assign k-neighbors and p-value as attributes
self.k, self.p = options["k"], options["p"]

# Exceptions for the k and p values
if not all(key in self.options for key in ("k", "p")):
raise KeyError("Missing either k or p in options")
if not 0 <= self.k <= self.n_particles:
raise ValueError(
"No. of neighbors must be between 0 and no. " "of particles."
)
# Assign p-value as attributes
self.p = options["p"]
# Exceptions for the p value
if "p" not in self.options:
raise KeyError("Missing p in options")
if self.p not in [1, 2]:
raise ValueError(
"p-value should either be 1 (for L1/Minkowski) "
"or 2 (for L2/Euclidean)."
)

# Case for Random and Ring topologies
if isinstance(topology, (Random, Ring)):
# Assign k-neighbors as attribute
self.k = options["k"]
if not isinstance(self.k, int):
raise ValueError(
"No. of neighbors must be an integer between"
"0 and no. of particles."
)
if not 0 <= self.k <= self.n_particles-1:
raise ValueError(
"No. of neighbors must be between 0 and no. " "of particles."
)
if "k" not in self.options:
raise KeyError("Missing k in options")

def optimize(self, objective_func, iters, print_step=1, verbose=1, **kwargs):
"""Optimizes the swarm for a number of iterations.
Expand Down Expand Up @@ -207,6 +229,13 @@ def optimize(self, objective_func, iters, print_step=1, verbose=1, **kwargs):
self.swarm.best_pos, self.swarm.best_cost = self.top.compute_gbest(
self.swarm, self.p, self.k
)
# If the topology is a random topology pass the neighbor attribute to the compute_gbest() method
elif isinstance(self.top, Random):
# Get minima of pbest and check if it's less than gbest
if np.min(self.swarm.pbest_cost) < self.swarm.best_cost:
self.swarm.best_pos, self.swarm.best_cost = self.top.compute_gbest(
self.swarm, self.k
)
else:
# Get minima of pbest and check if it's less than gbest
if np.min(self.swarm.pbest_cost) < self.swarm.best_cost:
Expand Down
7 changes: 7 additions & 0 deletions tests/backend/topology/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ def swarm():
"options": {"c1": 0.5, "c2": 1, "w": 2},
}
return Swarm(**attrs_at_t)


@pytest.fixture
def k():
"""Default neighbor number"""
_k = 1
return _k
Loading

0 comments on commit cca8d3c

Please sign in to comment.