Skip to content

Commit

Permalink
Add static/dynamic functionality for topologies (#164)
Browse files Browse the repository at this point in the history
Reference: #129

Added two new attributes to the topologies. `static` decides whether the topology
is a static or a dynamic one, it is initialized together with the class. `neighbor_idx` 
is an array that stores the indices of the neighbors of every particle. Changed 
all occurrences of topologies to fit the new initialization. Reworked the tests to 
fit the new functionality and added more parametrization of fixture functions. 
Updated the documentation to incorporate the new functionality.

Signed-off-by: Lester James V. Miranda <ljvmiranda@gmail.com>
Commited-by: @whzup
  • Loading branch information
whzup authored and ljvmiranda921 committed Jul 17, 2018
1 parent cca8d3c commit c95661b
Show file tree
Hide file tree
Showing 14 changed files with 173 additions and 125 deletions.
23 changes: 21 additions & 2 deletions pyswarms/backend/topology/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,30 @@
:mod:`pyswarms.backend.swarms.Swarm` module.
"""

# Import from stdlib
import logging

# Import from package
from ...utils.console_utils import cli_print


class Topology(object):
def __init__(self, **kwargs):
def __init__(self, static, **kwargs):
"""Initializes the class"""
pass

# Initialize logger
self.logger = logging.getLogger(__name__)

# Initialize attributes
self.static = static
self.neighbor_idx = None

if self.static:
cli_print("Running on `dynamic` topology, neighbors are updated regularly."
"Set `static=True` for fixed neighbors.",
1,
0,
self.logger)

def compute_gbest(self, swarm):
"""Computes the best particle of the swarm and returns the cost and
Expand Down
35 changes: 26 additions & 9 deletions pyswarms/backend/topology/pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@


class Pyramid(Topology):
def __init__(self):
super(Pyramid, self).__init__()
def __init__(self, static=False):
"""Initialize the class
Parameters
----------
static : bool (Default is :code:`False`)
a boolean that decides whether the topology
is static or dynamic
"""
super(Pyramid, self).__init__(static)

def compute_gbest(self, swarm):
"""Updates the global best using a pyramid neighborhood approach
Expand All @@ -49,12 +57,21 @@ def compute_gbest(self, swarm):
best_pos = swarm.pbest_pos[np.argmin(swarm.pbest_cost)]
best_cost = np.min(swarm.pbest_cost)
else:
pyramid = Delaunay(swarm.position)
indices, index_pointer = pyramid.vertex_neighbor_vertices
# 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(len(idx))]).astype(int)
# Check if the topology is static or dynamic and assign neighbors
if (self.static and self.neighbor_idx is None) or not self.static:
pyramid = Delaunay(swarm.position)
indices, index_pointer = pyramid.vertex_neighbor_vertices
# Insert all the neighbors for each particle in the idx array
self.neighbor_idx = np.array(
[index_pointer[indices[i]:indices[i + 1]] for i in range(swarm.n_particles)]
)

idx_min = np.array(
[swarm.pbest_cost[self.neighbor_idx[i]].argmin() for i in range(len(self.neighbor_idx))]
)
best_neighbor = np.array(
[self.neighbor_idx[i][idx_min[i]] for i in range(len(self.neighbor_idx))]
).astype(int)

# Obtain best cost and position
best_cost = np.min(swarm.pbest_cost[best_neighbor])
Expand Down Expand Up @@ -86,7 +103,7 @@ def compute_velocity(self, swarm, clamp=None):
from pyswarms.backend.topology import Pyramid
my_swarm = P.create_swarm(n_particles, dimensions)
my_topology = Pyramid()
my_topology = Pyramid(static=False)
for i in range(iters):
# Inside the for-loop
Expand Down
25 changes: 18 additions & 7 deletions pyswarms/backend/topology/random.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@


class Random(Topology):
def __init__(self):
super(Random, self).__init__()
def __init__(self, static=False):
"""Initializes the class
Parameters
----------
static : bool (Default is :code:`False`)
a boolean that decides whether the topology
is static or dynamic"""
super(Random, self).__init__(static)

def compute_gbest(self, swarm, k):
"""Update the global best using a random neighborhood approach
Expand Down Expand Up @@ -55,10 +62,14 @@ def compute_gbest(self, swarm, k):
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)
# Check if the topology is static or dynamic and assign neighbors
if (self.static and self.neighbor_idx is None) or not self.static:
adj_matrix = self.__compute_neighbors(swarm, k)
self.neighbor_idx = np.array([adj_matrix[i].nonzero()[0] for i in range(swarm.n_particles)])
idx_min = np.array([swarm.pbest_cost[self.neighbor_idx[i]].argmin() for i in range(len(self.neighbor_idx))])
best_neighbor = np.array(
[self.neighbor_idx[i][idx_min[i]] for i in range(len(self.neighbor_idx))]
).astype(int)

# Obtain best cost and position
best_cost = np.min(swarm.pbest_cost[best_neighbor])
Expand Down Expand Up @@ -91,7 +102,7 @@ def compute_velocity(self, swarm, clamp=None):
from pyswarms.backend.topology import Random
my_swarm = P.create_swarm(n_particles, dimensions)
my_topology = Random()
my_topology = Random(static=False)
for i in range(iters):
# Inside the for-loop
Expand Down
27 changes: 18 additions & 9 deletions pyswarms/backend/topology/ring.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@


class Ring(Topology):
def __init__(self):
super(Ring, self).__init__()
def __init__(self, static=False):
"""Initializes the class
Parameters
----------
static : bool (Default is :code:`False`)
a boolean that decides whether the topology
is static or dynamic"""
super(Ring, self).__init__(static)

def compute_gbest(self, swarm, p, k):
"""Updates the global best using a neighborhood approach
Expand Down Expand Up @@ -53,21 +60,23 @@ def compute_gbest(self, swarm, p, k):
Best cost
"""
try:
# Obtain the nearest-neighbors for each particle
tree = cKDTree(swarm.position)
_, idx = tree.query(swarm.position, p=p, k=k)
# Check if the topology is static or not and assign neighbors
if (self.static and self.neighbor_idx is None) or not self.static:
# Obtain the nearest-neighbors for each particle
tree = cKDTree(swarm.position)
_, self.neighbor_idx = tree.query(swarm.position, p=p, k=k)

# Map the computed costs to the neighbour indices and take the
# argmin. If k-neighbors is equal to 1, then the swarm acts
# independently of each other.
if k == 1:
# The minimum index is itself, no mapping needed.
best_neighbor = swarm.pbest_cost[idx][:, np.newaxis].argmin(
best_neighbor = swarm.pbest_cost[self.neighbor_idx][:, np.newaxis].argmin(
axis=1
)
else:
idx_min = swarm.pbest_cost[idx].argmin(axis=1)
best_neighbor = idx[np.arange(len(idx)), idx_min]
idx_min = swarm.pbest_cost[self.neighbor_idx].argmin(axis=1)
best_neighbor = self.neighbor_idx[np.arange(len(self.neighbor_idx)), idx_min]
# Obtain best cost and position
best_cost = np.min(swarm.pbest_cost[best_neighbor])
best_pos = swarm.pbest_pos[
Expand Down Expand Up @@ -98,7 +107,7 @@ def compute_velocity(self, swarm, clamp=None):
from pyswarms.backend.topology import Ring
my_swarm = P.create_swarm(n_particles, dimensions)
my_topology = Ring()
my_topology = Ring(static=False)
for i in range(iters):
# Inside the for-loop
Expand Down
3 changes: 2 additions & 1 deletion pyswarms/backend/topology/star.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

class Star(Topology):
def __init__(self):
super(Star, self).__init__()
super(Star, self).__init__(static=False)

def compute_gbest(self, swarm):
"""Obtains the global best cost and position based on a star topology
Expand Down Expand Up @@ -60,6 +60,7 @@ def compute_gbest(self, swarm):
Best cost
"""
try:
self.neighbor_idx = np.repeat(np.array([np.arange(swarm.n_particles)]), swarm.n_particles, axis=0)
best_pos = swarm.pbest_pos[np.argmin(swarm.pbest_cost)]
best_cost = np.min(swarm.pbest_cost)
except AttributeError:
Expand Down
12 changes: 6 additions & 6 deletions pyswarms/discrete/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
For the velocity update rule, a particle compares its current position
with respect to its neighbours. The nearest neighbours are being
determined by a kD-tree given a distance metric, similar to local-best
PSO. However, this whole behavior can be modified into a global-best PSO
by changing the nearest neighbours equal to the number of particles in
the swarm. In this case, all particles see each other, and thus a global
best particle can be established.
PSO. The neighbours are computed for every iteration. However, this whole
behavior can be modified into a global-best PSO by changing the nearest
neighbours equal to the number of particles in the swarm. In this case,
all particles see each other, and thus a global best particle can be established.
In addition, one notable change for binary PSO is that the position
update rule is now decided upon by the following case expression:
Expand Down Expand Up @@ -147,9 +147,9 @@ def __init__(
# Initialize the resettable attributes
self.reset()
# Initialize the topology
self.top = Ring()
self.top = Ring(static=False)

def optimize(self, objective_func, iters, print_step=1, verbose=1,**kwargs):
def optimize(self, objective_func, iters, print_step=1, verbose=1, **kwargs):
"""Optimizes the swarm for a number of iterations.
Performs the optimization to evaluate the objective
Expand Down
17 changes: 10 additions & 7 deletions pyswarms/single/general_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
# Set-up hyperparameters and topology
options = {'c1': 0.5, 'c2': 0.3, 'w':0.9}
my_topology = Pyramid()
my_topology = Pyramid(static=False)
# Call instance of GlobalBestPSO
optimizer = ps.single.GeneralOptimizerPSO(n_particles=10, dimensions=2,
Expand Down Expand Up @@ -104,7 +104,7 @@ def __init__(
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
parameters k and 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
Expand All @@ -115,12 +115,15 @@ def __init__(
are:
* Star
All particles are connected
* Ring
Particles are connected with the k nearest neighbours
* Pyramid
* Ring (static and dynamic)
Particles are connected to the k nearest neighbours
* Pyramid (static and dynamic)
Particles are connected in N-dimensional simplices
* Random
* Random (static and dynamic)
Particles are connected to k random particles
Static variants of the topologies remain with the same neighbours
over the course of the optimization. Dynamic variants calculate
new neighbours every time step.
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 @@ -180,7 +183,7 @@ def __init__(
"No. of neighbors must be an integer between"
"0 and no. of particles."
)
if not 0 <= self.k <= self.n_particles-1:
if not 0 <= self.k <= self.n_particles - 1:
raise ValueError(
"No. of neighbors must be between 0 and no. " "of particles."
)
Expand Down
8 changes: 6 additions & 2 deletions pyswarms/single/local_best.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
In this implementation, a neighbor is selected via a k-D tree
imported from :code:`scipy`. Distance are computed with either
the L1 or L2 distance. The nearest-neighbours are then queried from
this k-D tree.
this k-D tree. They are computed for every iteration.
An example usage is as follows:
Expand Down Expand Up @@ -113,6 +113,7 @@ def __init__(
center=1.00,
ftol=-np.inf,
init_pos=None,
static=False
):
"""Initializes the swarm.
Expand Down Expand Up @@ -151,6 +152,9 @@ def __init__(
the Minkowski p-norm to use. 1 is the
sum-of-absolute values (or L1 distance) while 2 is
the Euclidean (or L2) distance.
static: bool (Default is :code:`False`)
a boolean that decides whether the Ring topology
used is static or dynamic
"""
# Initialize logger
self.logger = logging.getLogger(__name__)
Expand All @@ -172,7 +176,7 @@ def __init__(
# Initialize the resettable attributes
self.reset()
# Initialize the topology
self.top = Ring()
self.top = Ring(static=static)

def optimize(self, objective_func, iters, print_step=1, verbose=1, **kwargs):
"""Optimizes the swarm for a number of iterations.
Expand Down
4 changes: 2 additions & 2 deletions tests/backend/topology/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pyswarms.backend.swarms import Swarm


@pytest.fixture
@pytest.fixture(scope="module")
def swarm():
"""A contrived instance of the Swarm class at a certain timestep"""
attrs_at_t = {
Expand All @@ -27,7 +27,7 @@ def swarm():
return Swarm(**attrs_at_t)


@pytest.fixture
@pytest.fixture(scope="module")
def k():
"""Default neighbor number"""
_k = 1
Expand Down
15 changes: 9 additions & 6 deletions tests/backend/topology/test_pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,36 @@
from pyswarms.backend.topology import Pyramid


def test_compute_gbest_return_values(swarm):
@pytest.mark.parametrize("static", [True, False])
def test_compute_gbest_return_values(swarm, static):
"""Test if compute_gbest() gives the expected return values"""
topology = Pyramid()
topology = Pyramid(static=static)
expected_cost = 1
expected_pos = np.array([1, 2, 3])
pos, cost = topology.compute_gbest(swarm)
assert cost == expected_cost
assert (pos == expected_pos).all()


@pytest.mark.parametrize("static", [True, False])
@pytest.mark.parametrize("clamp", [None, (0, 1), (-1, 1)])
def test_compute_velocity_return_values(swarm, clamp):
def test_compute_velocity_return_values(swarm, clamp, static):
"""Test if compute_velocity() gives the expected shape and range"""
topology = Pyramid()
topology = Pyramid(static=static)
v = topology.compute_velocity(swarm, clamp)
assert v.shape == swarm.position.shape
if clamp is not None:
assert (clamp[0] <= v).all() and (clamp[1] >= v).all()


@pytest.mark.parametrize("static", [True, False])
@pytest.mark.parametrize(
"bounds",
[None, ([-5, -5, -5], [5, 5, 5]), ([-10, -10, -10], [10, 10, 10])],
)
def test_compute_position_return_values(swarm, bounds):
def test_compute_position_return_values(swarm, bounds, static):
"""Test if compute_position() gives the expected shape and range"""
topology = Pyramid()
topology = Pyramid(static=static)
p = topology.compute_position(swarm, bounds)
assert p.shape == swarm.velocity.shape
if bounds is not None:
Expand Down
Loading

0 comments on commit c95661b

Please sign in to comment.