Skip to content

Commit

Permalink
Refactor test abstractions
Browse files Browse the repository at this point in the history
This commit adds test abstractions to PySwarms
Resolves #240, #241

Signed-off-by: Lester James V. Miranda <[email protected]>
  • Loading branch information
ljvmiranda921 committed Oct 1, 2018
1 parent 0d2b11c commit bafdcf1
Show file tree
Hide file tree
Showing 55 changed files with 1,010 additions and 1,486 deletions.
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[report]
exclude_lines =
pragma: no cover
@abc.abstractmethod
pos = init_pos
7 changes: 5 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
# All configuration values have a default; values that are commented out
# serve to show the default.

import sys
# Import standard library
import os
import sys

# Import from pyswarms
import pyswarms

# If extensions (or modules to document with autodoc) are in another
# directory, add these directories to sys.path here. If the directory is
Expand All @@ -32,7 +36,6 @@
# sys.path.insert(0, project_root)
sys.path.insert(0, os.path.abspath("../"))

import pyswarms

# -- General configuration ---------------------------------------------

Expand Down
50 changes: 38 additions & 12 deletions pyswarms/backend/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
"""

# Import standard library
import logging

# Import modules
import numpy as np

from ..utils.reporter import Reporter
Expand Down Expand Up @@ -44,6 +46,14 @@ def generate_swarm(
-------
numpy.ndarray
swarm matrix of shape (n_particles, n_dimensions)
Raises
------
ValueError
When the shapes and values of bounds, dimensions, and init_pos
are inconsistent.
TypeError
When the argument passed to bounds is not an iterable.
"""
try:
if (init_pos is not None) and (bounds is None):
Expand All @@ -70,12 +80,13 @@ def generate_swarm(
low=min_bounds, high=max_bounds, size=(n_particles, dimensions)
)
except ValueError:
rep.logger.exception(
"Please check the size and value of bounds and dimensions"
)
msg = "Bounds and/or init_pos should be of size ({},)"
rep.logger.exception(msg.format(dimensions))
raise
except TypeError:
rep.logger.exception("Invalid input type!")
msg = "generate_swarm() takes an int for n_particles and dimensions and an array for bounds"
rep.logger.exception(msg)
raise
else:
return pos

Expand All @@ -96,6 +107,18 @@ def generate_discrete_swarm(
init_pos : :code:`numpy.ndarray` (default is :code:`None`)
option to explicitly set the particles' initial positions. Set to
:code:`None` if you wish to generate the particles randomly.
Returns
-------
numpy.ndarray
swarm matrix of shape (n_particles, n_dimensions)
Raises
------
ValueError
When init_pos during binary=True does not contain two unique values.
TypeError
When the argument passed to n_particles or dimensions is incorrect.
"""
try:
if (init_pos is not None) and binary:
Expand All @@ -111,11 +134,12 @@ def generate_discrete_swarm(
size=(n_particles, dimensions)
).argsort(axis=1)
except ValueError:
rep.logger.exception(
"Please check the size and value of bounds and dimensions"
)
rep.logger.exception("Please check the size and value of dimensions")
raise
except TypeError:
rep.logger.exception("Invalid input type!")
msg = "generate_discrete_swarm() takes an int for n_particles and dimensions"
rep.logger.exception(msg)
raise
else:
return pos

Expand Down Expand Up @@ -145,11 +169,13 @@ def generate_velocity(n_particles, dimensions, clamp=None):
size=(n_particles, dimensions)
) + min_velocity
except ValueError:
rep.logger.exception(
"Please check the size and value of clamp and dimensions"
)
msg = "Please check clamp shape: {} != {}"
rep.logger.exception(msg.format(len(clamp), dimensions))
raise
except TypeError:
rep.logger.exception("Invalid input type!")
msg = "generate_velocity() takes an int for n_particles and dimensions and an array for clamp"
rep.logger.exception(msg)
raise
else:
return velocity

Expand Down
6 changes: 6 additions & 0 deletions pyswarms/backend/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
to specify how the swarm will behave.
"""

# Import standard library
import logging

# Import modules
import numpy as np

from ..utils.reporter import Reporter
Expand Down Expand Up @@ -69,6 +71,7 @@ def compute_pbest(swarm):
rep.logger.exception(
"Please pass a Swarm class. You passed {}".format(type(swarm))
)
raise
else:
return (new_pbest_pos, new_pbest_cost)

Expand Down Expand Up @@ -139,8 +142,10 @@ def compute_velocity(swarm, clamp):
rep.logger.exception(
"Please pass a Swarm class. You passed {}".format(type(swarm))
)
raise
except KeyError:
rep.logger.exception("Missing keyword in swarm.options")
raise
else:
return updated_velocity

Expand Down Expand Up @@ -187,5 +192,6 @@ def compute_position(swarm, bounds):
rep.logger.exception(
"Please pass a Swarm class. You passed {}".format(type(swarm))
)
raise
else:
return position
1 change: 1 addition & 0 deletions pyswarms/backend/swarms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
as input to most backend cases.
"""

# Import modules
import numpy as np
from attr import attrib, attrs
from attr.validators import instance_of
Expand Down
3 changes: 2 additions & 1 deletion pyswarms/backend/topology/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
:mod:`pyswarms.backend.swarms.Swarm` module.
"""

# Import standard library
import abc
import logging

Expand All @@ -33,7 +34,7 @@ def __init__(self, static, **kwargs):
self.rep.log(
"Running on `dynamic` topology,"
"set `static=True` for fixed neighbors.",
lvl=10,
lvl=logging.DEBUG,
)

@abc.abstractmethod
Expand Down
4 changes: 3 additions & 1 deletion pyswarms/backend/topology/pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
This class implements a pyramid topology. In this topology, the particles are connected by N-dimensional simplices.
"""

# Import standard library
import logging

# Import modules
import numpy as np
from scipy.spatial import Delaunay

Expand All @@ -29,7 +31,7 @@ def __init__(self, static=False):
super(Pyramid, self).__init__(static)
self.rep = Reporter(logger=logging.getLogger(__name__))

def compute_gbest(self, swarm):
def compute_gbest(self, swarm, **kwargs):
"""Update the global best using a pyramid neighborhood approach
This topology uses the :code:`Delaunay` class from :code:`scipy`. To prevent precision errors in the Delaunay
Expand Down
27 changes: 16 additions & 11 deletions pyswarms/backend/topology/random.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
This class implements a random topology. All particles are connected in a random fashion.
"""

# Import standard library
import itertools
import logging

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

Expand All @@ -18,23 +20,19 @@


class Random(Topology):
def __init__(self, k, static=False):
def __init__(self, static=False):
"""Initializes the class
Parameters
----------
k : int
number of neighbors to be considered. Must be a
positive integer less than :code:`n_particles-1`
static : bool (Default is :code:`False`)
a boolean that decides whether the topology
is static or dynamic
"""
super(Random, self).__init__(static)
self.k = k
self.rep = Reporter(logger=logging.getLogger(__name__))

def compute_gbest(self, swarm):
def compute_gbest(self, swarm, k, **kwargs):
"""Update the global best using a random neighborhood approach
This uses random class from :code:`numpy` to give every particle k
Expand All @@ -51,6 +49,9 @@ def compute_gbest(self, swarm):
----------
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
-------
Expand All @@ -62,7 +63,7 @@ def compute_gbest(self, swarm):
try:
# 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, self.k)
adj_matrix = self.__compute_neighbors(swarm, k)
self.neighbor_idx = np.array(
[
adj_matrix[i].nonzero()[0]
Expand All @@ -83,10 +84,14 @@ def compute_gbest(self, swarm):
).astype(int)

# Obtain best cost and position
best_cost = np.min(swarm.pbest_cost[best_neighbor])
best_pos = swarm.pbest_pos[
best_neighbor[np.argmin(swarm.pbest_cost[best_neighbor])]
]
if np.min(swarm.pbest_cost) < swarm.best_cost:
best_cost = np.min(swarm.pbest_cost[best_neighbor])
best_pos = swarm.pbest_pos[
best_neighbor[np.argmin(swarm.pbest_cost[best_neighbor])]
]
else:
# Just get the previous best_pos and best_cost
best_pos, best_cost = swarm.best_pos, swarm.best_cost

except AttributeError:
self.rep.logger.exception(
Expand Down
27 changes: 13 additions & 14 deletions pyswarms/backend/topology/ring.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
optimizers.
"""

# Import standard library
import logging

# Import modules
import numpy as np
from scipy.spatial import cKDTree

Expand All @@ -20,27 +22,19 @@


class Ring(Topology):
def __init__(self, p, k, static=False):
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
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.
k : int
number of neighbors to be considered. Must be a
positive integer less than :code:`n_particles`
"""
super(Ring, self).__init__(static)
self.p, self.k = p, k
self.rep = Reporter(logger=logging.getLogger(__name__))

def compute_gbest(self, swarm):
def compute_gbest(self, swarm, p, k, **kwargs):
"""Update the global best using a ring-like neighborhood approach
This uses the cKDTree method from :code:`scipy` to obtain the nearest
Expand All @@ -50,6 +44,13 @@ def compute_gbest(self, swarm):
----------
swarm : pyswarms.backend.swarms.Swarm
a Swarm instance
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.
k : int
number of neighbors to be considered. Must be a
positive integer less than :code:`n_particles`
Returns
-------
Expand All @@ -63,14 +64,12 @@ def compute_gbest(self, swarm):
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=self.p, k=self.k
)
_, 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 self.k == 1:
if k == 1:
# The minimum index is itself, no mapping needed.
best_neighbor = swarm.pbest_cost[self.neighbor_idx][
:, np.newaxis
Expand Down
Loading

0 comments on commit bafdcf1

Please sign in to comment.