Getting Started with PyBioSim

From Multiagent Robots and Systems Group
Revision as of 21:02, 12 March 2015 by Arindam (Talk | contribs) (Enabling a physics based ball)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Working with Simulator.py

When you first open Simulator.py, you will see something like this.

'''
Created on Jan 16, 2015

@author: Arindam
'''

import numpy as np
import matplotlib.pyplot as plt
from numpy import *
from World import *
from Agent import Agent
from Obstacle import *
from pylab import *
import os
from Ball import Ball
from matplotlib import animation
from LinearAlegebraUtils import distBetween
from RunAtBallBrain import RunAtBallBrain
from Team import Team
from copy import deepcopy
import time
from FlockingBrain import FlockingBrain


#Called once for initialization
'''
Usage guidelines:
1. Define globals required for the simulation in the __init__ constructor, here we define a bunch of waypoints for the ball
2. Initialize the globals in the setup() method. 
'''

class Simulator(object):
    def __init__(self, world, simTime, fps, imageDirName):
        self.world = world
        self.simTime = simTime
        self.fps = fps
        self.imageDirName = imageDirName

    def setup(self):    
        
        
#called at a fixed 30fps always
    def fixedLoop(self):

    
#Called at specifed fps
    def loop(self, ax):       
        self.world.draw(ax)
       
                
    def run(self):
        #Run setup once
        self.setup()
        
        #Setup loop
        timeStep = 1/double(30)
        frameProb = double(self.fps) / 30
        currTime = double(0)
        drawIndex = 0
        physicsIndex = 0
        while(currTime < self.simTime):
            self.fixedLoop()
            currProb = double(drawIndex)/double(physicsIndex+1)
            if currProb < frameProb:
                self.drawFrame(drawIndex)  
                drawIndex+=1
            physicsIndex+=1
            currTime+=double(timeStep)
     
        print "Physics ran for "+str(physicsIndex)+" steps"
        print "Drawing ran for "+str(drawIndex)+" steps"
            
    def drawFrame(self, loopIndex):
        fig = plt.figure(figsize=(16,12))
        ax = fig.add_subplot(111, projection='3d') 
        ax.view_init(elev = 30)
        ax.set_xlabel("X")
        ax.set_ylabel("Y")
        ax.set_zlabel("Z")    
        fname = self.imageDirName + '/' + str(int(100000000+loopIndex)) + '.jpg' # name the file 
        self.loop(ax)
        plt.gca().set_ylim(ax.get_ylim()[::-1])
        savefig(fname, format='jpg', bbox_inches='tight')
        print 'Written Frame No.'+ str(loopIndex)+' to '+ fname
        plt.close()


#Simulation runs here
#set the size of the world
world = World(100, 100)
#specify which world to simulate, total simulation time, and frammerate for video
sim = Simulator(world, 60, 30, "images")
#run the simulation
sim.run()

'''
To create a video using the image sequence, execute the following command in command line.
>ffmpeg -f image2 -i "1%08d.jpg" -r 30 outPut.mp4
Make sure to set your current working directory to /images and have ffmpeg in your path.
'''

    

The import methods to note here are the setup(), loop() and fixedLoop() methods. setup() : This method is called once before the simulation, we'll use this to initialize the world and setup various parameters. loop() : This method is called repeatedly at some specified frame rate(we'll see how to change this later, right now its set to 30), you can think of this as a draw loop, by default it calls world.draw() which draws the world. fixedLoop() : This method is called at 30 times a second fixed, always. This method should be used for moving objects in the world. This allows your movement to be asynchronous to the speed of the video.

Below is an example of how to use these methods to create a simulation of two teams chasing a moving ball. Using setup() to initialize the world: First Lets setup two teams in our setup() method.

teamA = Team("A", '#ff99ff')
teamB = Team("B", '#ffcc99')

Syntax: Team("String Name of team", "Hex color code for team color")

Now lets create the agents that will be part of these teams. Here's a snippet on how to create an agent.

ag1Pos = array([80, 50, -20])
ag1Rot = array([30, 0, 0])
ag1Brain = RunAtBallBrain()
agent1 = Agent(teamA, ag1Pos, ag1Rot, ag1Brain, 5, 5)

First we define the position where we want the agent to start. ag1Pos is a numpy array of the (x, y, x) co-ordinates. Second we define the rotation we want the agent to start with. ag1Rot is a numpy array of (yaw, pitch , roll) in degrees. We then initialize an instance of a "Brain" which defines the behavior of the agent. We'll see how to create a custom one later, right now you can use the provided RunAtBallBrain() which defines a behaviour in which the agent chases the ball. Lastly, we initialize an agent as an Object of the Agent class. Syntax : Agent( "team of Agent", "Starting Position", "Starting Rotation", "Brain", "Radius of Collision", "Draw Radius") To add this agent into the world we need to call:

self.world.agents.append(agent1)


Now lets define the ball which the agents will chase.

#define a ball
ball = Ball(array([0, 0, 0]))
          
#add the ball to the world
self.world.balls.append(ball)

Similar to our agents we provide a numpy array as a position of our ball and we then add it to the world. Syntax: Ball( "position" )

Now for some decorative obstacles.

#define a bunch of obstacles
ob1Pos = array([-50,-50,-50])
ob1 = Obstacle(ob1Pos, 30)
         
ob2Pos = array([80,-50,-50])
ob2 = Obstacle(ob2Pos, 20)
         
#add obstacles to the world
self.world.obstacles.append(ob1);
self.world.obstacles.append(ob2)

Just like the ball, we define two obstacles with a numpy array as a position, additionally we provide a radius for the obstacle. Syntax: Obstacle("position", "radius")

That's it, the world is setup, no we'll see how to move the agents and the ball around using fixedLoop(). Using fixedLoop() to move objects: Lets make sure we tell all our agents to move to do this we call,

for agent in self.world.agents:
    agent.moveAgent(self.world)

This uses the behavior defined in the agents brain to move them around. Now, to move the ball. The ball has a method call moveBall("position", "speed") which moves the ball to a certain position at a certain speed. We'll provide a series of points to this method to move it around. Heres the logic for it: 1. Maintain a list of waypoints 2. Move the ball with a certain speed to waypoint[0]. 3. If I'm really close to waypoint[0] then remove waypoint[0] from my lest of waypoints.(Python will conveniently reorder the list so waypoint[1] is now waypoint[0]) 4. Continue till waypoints.size > 0

Important: We need to maintain list of "waypoints" for the ball, we cannot do this inside fixedLoop() because it would be re-created at the start of every fixedLoop() call, thus we need to define it as a global. Instead of using python's global keyword, we define it as a member of our Simulator class inside the Simulator class's __init()__ constructor.

Here's the above logic in code form,

for ball in self.world.balls:  
  if len(self.ballWPs) > 0:  
     ball.moveBall(self.ballWPs[0], 1)
         if distBetween(ball.position, self.ballWPs[0]) < 0.5:
             if len(self.ballWPs) > 0:
                 self.ballWPs.remove(self.ballWPs[0])

That,s it! Your Simulator.py should look like this:

'''
Created on Jan 16, 2015

@author: Arindam
'''

import numpy as np
import matplotlib.pyplot as plt
from numpy import *
from World import *
from Agent import Agent
from Obstacle import *
from pylab import *
import os
from Ball import Ball
from matplotlib import animation
from LinearAlegebraUtils import distBetween
from RunAtBallBrain import RunAtBallBrain
from Team import Team
from copy import deepcopy
import time
from FlockingBrain import FlockingBrain


#Called once for initialization
'''
Usage guidelines:
1. Define globals required for the simulation in the __init__ constructor, here we define a bunch of waypoints for the ball
2. Initialize the globals in the setup() method. 
'''

class Simulator(object):
    def __init__(self, world, simTime, fps, imageDirName):
        self.world = world
        self.simTime = simTime
        self.fps = fps
        self.imageDirName = imageDirName
        self.currWP = 0
        self.ballWPs = [array([50.0, -100.0, 0.0]), array([0.0, 100.0, -70.0]), array([50.0, 20.0, 100.0]),array([-30.0, 50.0, -100.0]), array([80.0, -50.0, 50.0]), array([80.0, -50.0, -50.0]), array([-65.0, 20.0, 50.0]), array([-50.0, 20.0, -60.0])]

    def setup(self):    
        #setup directory to save the images
        self.imageDirName = 'images'
        try:
            os.mkdir(self.imageDirName)
        except:
            print self.imageDirName + " subdirectory already exists. OK."

  
         #define teams which the agents can be a part of
        teamA = Team("A", '#ff99ff')
        teamB = Team("B", '#ffcc99')
        #Defining a couple of agents 
        ag1Pos = array([80, 50, -20])
        ag1Rot = array([30, 0, 0])
        ag1Brain = RunAtBallBrain()
        agent1 = Agent(teamA, ag1Pos, ag1Rot, ag1Brain, 5, 5)
         
         
        ag2Pos = array([-80, 0, 0])
        ag2Rot = array([0, 0, 0])
        ag2Brain = RunAtBallBrain()
        agent2 = Agent(teamA, ag2Pos, ag2Rot, ag2Brain, 5, 5)
         
        ag3Pos = array([70, 30, 50])
        ag3Rot = array([0, 0, 0])
        ag3Brain = RunAtBallBrain()
        agent3 = Agent(teamB, ag3Pos, ag3Rot, ag3Brain, 5, 5)
         
        ag4Pos = array([-80, 20, 60])
        ag4Rot = array([0, 0, 0])
        ag4Brain = RunAtBallBrain()
        agent4 = Agent(teamB, ag4Pos, ag4Rot, ag4Brain, 5, 5)
         
        #Add the agent to the world
        self.world.agents.append(agent1)
        self.world.agents.append(agent2)
        self.world.agents.append(agent3)
        self.world.agents.append(agent4)

#         
        #define a bunch of obstacles
        ob1Pos = array([-50,-50,-50])
        ob1 = Obstacle(ob1Pos, 30)
         
        ob2Pos = array([80,-50,-50])
        ob2 = Obstacle(ob2Pos, 20)
         
        #add obstacles to the world
        self.world.obstacles.append(ob1);
        self.world.obstacles.append(ob2)
        
        #define a ball
        ball = Ball(array([0, 0, 0]))
        
        
        #add the ball to the world
        self.world.balls.append(ball)
        
#called at a fixed 30fps always
    def fixedLoop(self):
        for agent in self.world.agents:
            agent.moveAgent(self.world)
         
        for ball in self.world.balls:  
            if len(self.ballWPs) > 0:  
                ball.moveBall(self.ballWPs[0], 1)
                if distBetween(ball.position, self.ballWPs[0]) < 0.5:
                    if len(self.ballWPs) > 0:
                        self.ballWPs.remove(self.ballWPs[0])

    
#Called at specifed fps
    def loop(self, ax):       
        self.world.draw(ax)
       
                
    def run(self):
        #Run setup once
        self.setup()
        
        #Setup loop
        timeStep = 1/double(30)
        frameProb = double(self.fps) / 30
        currTime = double(0)
        drawIndex = 0
        physicsIndex = 0
        while(currTime < self.simTime):
            self.fixedLoop()
            currProb = double(drawIndex)/double(physicsIndex+1)
            if currProb < frameProb:
                self.drawFrame(drawIndex)  
                drawIndex+=1
            physicsIndex+=1
            currTime+=double(timeStep)
     
        print "Physics ran for "+str(physicsIndex)+" steps"
        print "Drawing ran for "+str(drawIndex)+" steps"
            
    def drawFrame(self, loopIndex):
        fig = plt.figure(figsize=(16,12))
        ax = fig.add_subplot(111, projection='3d') 
        ax.view_init(elev = 30)
        ax.set_xlabel("X")
        ax.set_ylabel("Y")
        ax.set_zlabel("Z")    
        fname = self.imageDirName + '/' + str(int(100000000+loopIndex)) + '.jpg' # name the file 
        self.loop(ax)
        plt.gca().set_ylim(ax.get_ylim()[::-1])
        savefig(fname, format='jpg', bbox_inches='tight')
        print 'Written Frame No.'+ str(loopIndex)+' to '+ fname
        plt.close()


#Simulation runs here
#set the size of the world
world = World(100, 100)
#specify which world to simulate, total simulation time, and frammerate for video
sim = Simulator(world, 60, 30, "images")
#run the simulation
sim.run()

'''
To create a video using the image sequence, execute the following command in command line.
>ffmpeg -f image2 -i "1%08d.jpg" -r 30 outPut.mp4
Make sure to set your current working directory to /images and have ffmpeg in your path.
'''

    

Using Brain's to define custom behaviors for agents

You can define custom behaviors for agents by writing custom implementations for brains. Here is the structure of the RunAtBallBrain.py

from Agent import Agent
from Ball import Ball
from Obstacle import Obstacle
from LinearAlegebraUtils import getYPRFromVector
import numpy as np
from Action import Stun, Kick
class RunAtBallBrain(object):
    '''
    classdocs
    '''


    def __init__(self):      
        pass
    
    def takeStep(self, myTeam=[], enemyTeam=[], balls=[], obstacles=[]):
        actions = []
        deltaPos = np.array([1, 0, 0])
        deltaRot = getYPRFromVector(balls[0].position)
        myTeamName = myTeam[0].team.name
        for agent in enemyTeam:
            if myTeamName == 'A':
                actions.append(Stun(agent, 10))
        for ball in balls:
            actions.append(Kick(ball, np.array([1, 0, 0]), 100))
        return deltaPos, deltaRot, actions

The brain contains a takeStep() method which receives the following inputs: myTeam = A list of agent objects which are in the same team as the agent enemyTeam = A list of agent objects which are in the opposing team balls = A list of attractor's or balls in the scene obstacles = A lost of spherical obstacles which the agents should avoid

Each of the above elements contains a position represented in egocentric co-ordinate frame where the agent itself is the origin, forward of the agent is positive x, upwards is positive z, and right is positive y.

The brain returns three things: deltaPos = a delta position, can be thought of as a direction vector deltaRot = a delta rotation, represented in degrees in the order [yaw, pitch, roll] actions = a list of actions the agent needs to perform, actions are objects of two classes, Kick and Stun

Syntax:

Kick('ball to kick', 'egocentric vector of direction to kick in', 'kick intensity')
Stun('agent to stun', 'duration in seconds')

'ball to kick' and 'agent to stun' are the references to the objects passed in balls and enemyTeam.

In the above example we want team A to be able to stun agents in team B. So, we loop over the enemy team list and add Stun objects to actions list for each agent in the enemyTeam list only if the team the agent is in, is team 'A'. Similarly, we loop over all the 'balls' in the balls list and add a Kick object to the actions list for every ball in the list. Kicking also requires the ball to be dynamic so that it can be moved with physics rather than manually as before, thi sis shown in the next section.

Enabling a physics based ball

When you create a ball in the simulator you can now mark it to be dynamic as follows.

ball = Ball(array([0, 0, 0]))
ball.isDynamic = True

Now in the fixedLoop() function you can call the ball's updatePhysics() method so that it is moved with physics. This is done as follows:

for ball in self.world.balls:
ball.updatePhysics(self.world)

The updatePhysics() method needs a reference to the world so that the ball knows the bounds and obstacles of the world so it can bounce off them. Thats it!