8
\$\begingroup\$

This is a followup post to Particle Swarm Optimization.

I wrote a script in Python for particle swarm optimization and I posted it here to get comments on the design. I was told that encapsulating the list used to hold the particles in a class would be a good idea, but it seemed like putting the Particle class I already had inside as a subclass was a good idea.

Now, I'm asking if it was indeed a good idea, and moreover if I made any rookie errors in using a subclass. For example, I wasn't completely sure about shared variables from a parent to a child subclass, so I just passed the ones I needed in the subclass initialization (worldWidth, worldHeight, and k).

Here is the "top" of the class, with the initialization of the parent class, the initializer function for the list, and the entirety of the the subclass. I've omitted all of the public methods of the parent class. For the whole code, see the repository.

class ParticleList:
    """ParticleList encapsulates the list of particles and functions used to
    manipulate their attributes
    """

    def __init__(self, NP, I, C, SR, LR, WW, WH, MV, K, FN):
        """create an array, assign values, and initialize each particle"""
            self.pList        = []
            self.numParticles = NP
            self.inertia      = I
            self.cognition    = C
            self.socialRate   = SR
            self.localRate    = LR
            self.worldWidth   = WW
            self.worldHeight  = WH
            self.maxVelocity  = MV
            self.k            = K
            self.fname        = FN
            self.createParticles()

    def createParticles(self):
        """create a list of particles and then create neighborhoods if it's called for (k > 0)"""
        for i in range(0,self.numParticles):
            self.pList.append(self.Particle(i, self.worldWidth, self.worldHeight, self.k))

        #fill neighbor lists
        if self.k > 0:
            for p in self.pList:
                for x in range(p.index-(self.k/2),p.index+(self.k/2)+1):
                    if x > self.numParticles:
                        p.neighbors.append(x%self.numParticles)
                    elif x < 0:
                        p.neighbors.append(self.numParticles+x)
                    elif x == self.numParticles:
                        p.neighbors.append(0)
                    else:
                        p.neighbors.append(x)
            self.updatelBest()

        #initialize global and local bests
        self.updategBest()

    ###########

    class Particle:
        """this class is used for each particle in the list and all of their attributes"""
        #[Q value, x_pos, y_pos]
        gBest     = [0.0, 0, 0]
        bestIndex = 0

        #takes index in pList as constructor argument
        def __init__(self, i, worldWidth, worldHeight, K):
        #x,y coords, randomly initialized
        self.x          = randint(-worldWidth/2,worldWidth/2)
        self.y          = randint(-worldHeight/2,worldHeight/2)
        #x,y velocity
        self.velocity_x = 0.0
        self.velocity_y = 0.0
        #personal best
        #[fitness value, x coord, y coord]
        self.pBest      = [Q(self.x, self.y), self.x, self.y]
        self.index      = i
        #local best
        self.lBest      = []
        self.lBestIndex = 0
        #array for neighbor indicies
        self.neighbors  = []
        self.k          = K
        #for printing particle info
        def __str__(self):
            """Creates string representation of particle"""
            ret = """  i: {self.index!s}
            x: {self.x!s}
            y: {self.y!s}
            v_x: {self.velocity_x!s}
            v_y: {self.velocity_y!s}
            b: {self.pBest[0]!s}""".format(**locals())
            if self.k > 0:
                return ret+'  l: '+str(self.lBest)+'\n'
            else:
                return ret+'\n'

    ###########

Here are two example Q functions:

def Q(p_x,p_y):
    return float(100.0 * (1.0 - (pdist(p_x,p_y)/mdist())))

and

def Q(p_x,p_y):
    return float((9.0 * max(0.0, 10.0 - (pdist(p_x,p_y)**2))) + (10.0 * (1.0 - (pdist(p_x,p_y)/mdist()))) + (70.0 * (1.0 - (ndist(p_x,p_y)/mdist()))))
\$\endgroup\$
  • \$\begingroup\$ I'm hoping that you do post a followup as there's a few suggestions I have that would help move this in the direction of a high quality library. As it happens I'm quite interested in this algorithm and I have a bit of a vested interest in seeing this progress, if you aren't wanting to write a follow up for whatever reason perhaps I can fork the repository and do some work on this in the fork. The only reason I hesitated to fork in the first place was that I was somewhat under the impression that this code was part of some coursework you were doing. \$\endgroup\$ – shuttle87 Jul 18 '15 at 15:41
  • \$\begingroup\$ @shuttle87 It was originally part of some coursework, but I've since graduated. I was working on expanding it and wanted to learn more about Python through it because Python itself was never part of my coursework. I will finish making my updates and post a follow up! You're also more than welcome to fork it \$\endgroup\$ – dockleryxk Jul 22 '15 at 18:10
2
\$\begingroup\$

I'm not experienced enough at OOP to give you meaningful feedback on your class models, but here are two quick things I noticed when scanning my novice eyes over your code:

  1. Undocumented parameters. I don't know if NP, I, C, etc. are meant to be ints, floats, tuples, or what. I'd suggest adding comments to explain the parameters and their types in the docstring of your __init__ function.

  2. Too many parameters. One thing that would simultaneously clean up your code and make it easier to use would be to pass all those parameters to your ParticleList.__init__() as a single dictionary. You could form the dictionary like this:


params = {'number_of_particles': NP, 'max_velocity': MV, 'world_width': WW, ...}

, and then your __init__() would just be def __init__(self, params). In the body of the function you could unpack it:

self.number_of_particles = params['number_of_particles']
self.max_velocity = params['max_velocity']
...

The advantage of doing that is you don't have to remember what order parameters get passed into your initialization function when you use it.

\$\endgroup\$
  • \$\begingroup\$ Those are great ideas. Thank you so much! I'll definitely update it and incorporate them. \$\endgroup\$ – dockleryxk Jul 14 '15 at 16:26
2
\$\begingroup\$

I want to add my 2 cents to the code in general.

  1. Try to use pythonic variable naming, e.g. particles_list instead of pList. It will be easier to read your code in the future.

  2. I will rewrite create_particles method to make it more easier to read and a bit more efficient:

    def create_particles(self):
        """
        create a list of particles and then create neighborhoods if it's
        called for (k > 0)
        """
        for i in range(self.numParticles):
            particle = self.Particle(i, self.worldWidth, self.worldHeight, self.k)
            self.pList.append(particle)
    
        # fill neighbor lists
        if self.k > 0:
            for p in self.pList:
                start = p.index - (self.k / 2)
                stop = p.index + (self.k / 2) + 1
                for x in range(start, stop):
                    neighbor = x
                    if x > self.numParticles:
                        neighbor = x % self.numParticles
                    elif x < 0:
                        neighbor = self.numParticles + x
                    elif x == self.numParticles:
                        neighbor = 0
                    p.neighbors.append(neighbor)
            self.update_local_bests()
    
        self.update_global_bests()
    
  3. I will use more format() magic in __str__ method as well:

    def __str__(self):
        """Creates string representation of particle"""
        # Yes, it's a bit explicit, but we are not using locals() which
        # is kind of antipattern
        extras = 'l: {}\n'.format(self.lBest) if k > 0 else '\n'
        result = """
        i: {index!s}
        x: {x!s}
        y: {y!s}
        v_x: {velocity_x!s}
        v_y: {velocity_y!s}
        b: {best_particle!s}
        {extras}
        """.format(
            index=self.index, x=self.x, y=self.y,
            velocity_x=self.velocity_x, velocity_y=self.velocity_y,
            best_particle=self.pBest[0], extras=extras
        )
        return result
    
  4. If you consider createParticles method as a private one, start it's name with underscore _create_particles.

  5. I'm not 100% sure of making Particle class as a class which lives in scope of ParticleList. I will make it global. I believe you can use it later outside of the ParticleList.

  6. As an alternative solution for __init__ parameters, I can propose using def __init__(self, **kwargs): trick. But it will make your code harder to understand, so I agree with Curt F. solution.

\$\endgroup\$
  • \$\begingroup\$ Thank you so much for your two cents, I promise it's worth way more than that to me. Could you further explain your format() magic? Specifically the second call. \$\endgroup\$ – dockleryxk Jul 14 '15 at 22:39
  • \$\begingroup\$ also, I made Particle a subclass because this is simulating a swarm. Thus there will never be only one particle, and a list will always be needed. \$\endgroup\$ – dockleryxk Jul 14 '15 at 22:54
  • \$\begingroup\$ @dockleryxk, in the second format call I've tried to escape using of locals() method and use more explicit approach to map the exact values of the variables to the exact placeholders in the string. I believe my call will be more efficient from the performance perspective. \$\endgroup\$ – Mikhail Chernykh Jul 15 '15 at 20:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.