Chapter 5

Chapter 5
Implementation

Microsoft Foundation Classes

The Microsoft Foundation Classes (MFC) are a group of C++ classes that collectively form an application framework. These classes provide the essential components of an application written for the Microsoft Windows graphical environment:

Figure 20 Application Architecture

Coding Conventions

Since the SIMBIOSYS framework is an extension of the Microsoft Foundation Classes, the coding conventions used in the former were taken in large part from the latter.

Framework Prefix

All classes in the SIMBIOSYS framework begin with the prefix `bio' in order to prevent name collisions with other packages that may be used in the construction of the application.

Member Names

Member function names are descriptive composites of VerbNoun pairs such as SetWorld and GetProgram with each word in the name capitalized. Member data names are composites of adjectiveNoun pairs or simply a noun, capitalized like the function members except for the first letter.

Scope Prefixes

Protected members are prefixed with a single underscore (e.g. _world) and private members are prefixed with a double underscore (e.g. __orientation). This convention helps the readability of methods because data members are not declared. It also prevents name collisions with local variables which tend to have similar names, especially for SetData type methods.

Pseudo-keywords

Three identifiers have been defined so that they look like C++ keywords, though the language compiler treats them just like any other application defined symbol. The class definitions make use of these symbols which helps the readability of the code:


#define ctor		/**/
#define dtor		virtual
#define override 	virtual

Figure 21. Pseudo-keywords

The first two, ctor and dtor, indicate a constructor and destructor respectively. The pseudo-keywords take the place of return types in the header allowing all the members names to line up nicely in a column. The ctor keyword is a placeholder, being defined as just an open and close comment. Defining dtor as virtual ensures that deleting an instance of the class will call the correct destructor even if the pointer type refers to one of the class's base classes. The override symbol indicates that the member function is overloading one of its base class's member functions.

Framework Classes

This section will present and discuss the implementations of the classes designed in the previous chapter. All of the `top-level' SIMBIOSYS framework classes have been derived from the MFC base class CObject. This policy has two advantages. The MFC framework provides several collection classes which work with instances of classes derived from CObject. The linked list CObList is particularly useful. Secondly, CObject has facilities for persistence built in which all derived classes can exploit.

Figure 22 Framework Class Hierarchy

bioObject

The bioObject class is derived from the top-level MFC class, CObject. Its only purpose is to provide a convenient root class for all other SIMBIOSYS classes and to act as a place holder for methods and data common to all. Currently it has no methods or data.

bioSimulation

The bioSimulation base class implements the Simulation object designed in the previous chapter. An application using the framework must create a single instance of the bioSimulation class or subclass and call the SetWorld() method with an instance of the bioWorld class or subclass.

Data

The bioSimulation class stores a pointer to a bioWorld instance and one or more bioPopulation instances. In addition, the simulation stores several variables related to the cycles of the simulation: the current and total number of action cycles, environment cycles and breeding cycles.


class bioSimulation : public bioObject
{
  public:
  	ctor			bioSimulation (void);
  	dtor			~bioSimulation (void);

  	virtual void		SetWorld (bioWorld*);
  	virtual bioWorld*	GetWorld (void);

  	virtual void		AddPopulation (bioPopulation*);

  	virtual int		GetData (int src, void* dest);

  	virtual int		NextStep (void);
  	virtual int		StartEnvCycle (void) {return FALSE;}
  	virtual int		StartBreedCycle (void) {return FALSE;}
  	virtual void		DoBreedCycle (void);
  	virtual void		DoEnvCycle (void);
  	virtual void		DoActionCycle (void);

  protected:
 	bioWorld*		_world;
	CObList			_populations;

	int			_curAct;
	int			_totalAct;
  	int			_curEnv;
  	int			_totalEnv;
  	int			_curGen;
};

Figure 23. bioSimulation class header

Methods

The world instance can be stored and retrieved through SetWorld() and GetWorld() respectively. The simulation takes ownership of the world instance once it is set and will delete it in the simulation destructor. Population instances can optionally be added to the simulation through the AddPopulation() method. These are also destroyed in the simulation destructor.

The simulation relies on an external source to trigger clock events through the NextStep() method. This would typically be done from a "Single Step" menu item handler and the handler of a system timer which is started and stopped through menu events. The frequency of the timer is dependent on several factors: the amount of computation that must be done in an action cycle, the amount of redrawing for a single frame in the map animation, and the hardware platform. Typically, 20 frames per second (a 50 ms time interval for the timer) is pushing the limits of the simulation.

The NextStep() method first calls StartEnvCycle() to see if an environment cycle should be executed. If this returns true, StartBreedCycle() is called to see if a breeding cycle should be executed. If the latter also returns true, DoBreedCycle() is called before DoEnvCycle(). In any case, DoActionCycle() is called at the end. StartEnvCycle() and StartBreedCycle() both default to returning false, so the default behavior of NextStep() is to only call DoActionCycle() which sends NextStep() to the world instance. The default implementation of DoBreedCycle() sends the Breed() message to all populations stored in the simulation and increments the internal counters related to breeding cycles. The default implementation of DoEnvCycle() only increments the internal counters. If an application wants to make use of breeding cycles and/or environment cycles, it will have to override the Start*Cycle() methods and return true when certain conditions are met such as after a certain number of action cycles.

The GetData() method provides a single interface for all instrument objects and other interested clients to retrieve information from the simulation. The type of data is specified by a value passed in with the src variable. If the simulation recognizes the src, it will set the dest pointer to whatever the src parameter specifies, otherwise it will pass the request to its base class. In this way, subclasses of bioSimulation can easily extend the types of data provided without expanding the public interface.

bioWorld

The environment is implemented in the bioWorld abstract base class. Its purpose is to keep track of all the things in the environment, their position and orientation, and to oversee the redrawing of the map in the user interface. It also manages the action cycles, giving each thing in the world a turn to perceive its local environment and determine an intention. The world is responsible for resolving each thing's intention into an action, though subclasses of bioWorld may delegate that responsibility to other classes.

Data

The only data stored at this level in the hierarchy is a list of instances of bioThing.


class bioWorld : public bioObject
{
  public:
  	ctor			bioWorld (void);
  	dtor			~bioWorld (void);

	virtual void		Init (void) {}

	virtual void		AddThing (bioThing*, double x, double y);
	virtual void		RemoveThing (bioThing*);

  	virtual void		NextStep (void);
	virtual void		ResolveAction (bioThing*, int intent) = 0;

	virtual void		PlaceRandom (bioThing*) = 0;
	virtual void		MoveThing (bioThing*, double x, double y,
						int relative = 0);
	virtual void		TurnThing (bioThing*, double angle,
					int relative = 0);

	virtual bioObject*	WhatsAt (double x, double y, bioThing*) = 0;
	virtual void		XformPos (double x, double y, bioThing*,
					double& nx, double& ny);

  	virtual void		Redraw (CDC*) = 0;
  	virtual void		Resize (int width, int height) = 0;

  	virtual void		UserSelect (int x, int y) {}
  	virtual void		UserEdit (int x, int y)  {}

  protected:

void _SetPosition (bioThing*, double x, double y); CObList _things; };

Figure 24. bioWorld class header

Methods

Init() is a virtual method which allows subclasses to do any initialization best left out of their respective constructors such as memory allocation. AddThing() and RemoveThing() are public interfaces to the bioWorld's internal list of bioThing instances. An initial position must be provided with AddThing(). If an initial position is not known, the client should call PlaceRandom() which will find an empty spot on the world for the new thing. It is the responsibility of subclasses of bioWorld to implement PlaceRandom() because there is not enough information stored in bioWorld or bioThing to ensure that new things won't overlap or even if overlapping is allowed.

NextStep() executes a single action cycle. Each bioThing instance in the world's list is sent the Look() message which gives the thing a chance to perceive its local environment by sending the world one or more WhatsAt() messages. The WhatsAt() method returns the contents of the position provided by the thing, relative to the thing's current position and orientation. For instance, if a thing is at location (10, 10) and has a heading of 90 and asks what's at (0, 1), i.e. what is directly in front me, then the world would return the contents of location (11, 10) as shown in Figure 25. This is accomplished partially through the use of XformPos(), a method which transforms a position specified by an x- and y-coordinate into a different coordinate system. Separating XformPos() out into its own virtual method allows subclasses to optimize the transformation. The default implementation is quite general, and its use of transformation matrices is relatively computationally intensive.

After all things have been told to Look(), each thing is queried for its intention, returned as an integer value, which is passed to ResolveAction(). This is a pure virtual function, therefore subclasses must provide an implementation which determines how intentions are resolved into actions, that is, how the interaction of what all the things want to do is manifested into actual events.

MoveThing() and TurnThing() are used to manipulate the location and orientation of a thing respectively. Both methods take an extra parameter which specifies whether the change is absolute or relative to the thing's current state. The parameter is optional and defaults to absolute. The _SetPosition() protected method allows subclasses of bioWorld to set the position of a thing directly without having to first add it to the list of things.

The final four methods have to do with the user interface. Redraw() is responsible for drawing a single frame of the map animation into the passed in device context. Resize() is used to inform the world instance that the area it draws into has changed. UserSelect() and UserEdit() are to be called when the user clicks and double-clicks on the map. Subclasses are responsible for converting the device coordinates of the mouse event into world coordinates, finding the thing instance at that location, and sending that thing Select() and Edit() respectively.

Figure 25. WhatsAt() Example

bioCellWorld

The bioCellWorld class represents a discrete 2-dimensional world. It provides implementations for all the pure virtual methods defined in bioWorld except for Redraw().

Data

A bioCellWorld instance maintains a two-dimensional array of bioThing instances along with the size of the world. To make redrawing more convenient, the instance stores several data members relating to the window it is being displayed in: the window's width, and height, and how large the world and a cell are in display coordinates.


class bioCellWorld : public bioWorld
{
  public:
  	ctor			bioCellWorld (void);
  	dtor			~bioCellWorld (void);

  	virtual void		SetSize (int);
  	virtual void		ReadMap (char*);
  	virtual void		CreateThing (char id, int x, int y) {}

	override void		AddThing (bioThing*, double x, double y);
	override void		RemoveThing (bioThing*);
	override void		PlaceRandom (bioThing*);

  	override bioObject*	WhatsAt (double x, double y, bioThing*);

  	override void		Resize (int width, int height);
  	override void		UserSelect (int x, int y);
  	override void		UserEdit (int x, int y);

  protected:
  	int			_size;		// how many cells across
  	int			_width;	// how many pixels across one cell
  	int			_side; 	// how many pixels across the world
  	int			_winWidth;	// window width
  	int			_winHeight;	// window height
  	int			_offX;		// horizontal offset
  	int			_offY;		// vertical offset
  	bioThing***		_cells;	// discrete regions of the world
};
Figure 26. bioCellWorld Class Header

Methods

The world is initialized using one of two methods. SetSize() will create an empty world of the specified size. ReadMap() will read an ASCII file containing a representation of the world. The first line contains the size. There should follow a number of lines equal to size, each containing size characters. For each character in the file, the method CreateThing() is called along with the position of that character. Subclasses of bioCellWorld are expected to override this method and create an appropriate bioThing instance represented by that character. If it doesn't recognize the character, it should invoke its base class's implementation.

bioThing

bioThing provides the bioWorld class a public interface for all objects that can inhabit a world. Even though subclasses of bioThing can be completely passive (e.g. rocks and food), the bioThing class must define virtual functions for looking and retrieving intentions so that the world has a way to send messages to the active objects.

Data

bioThing stores its position and orientation in private members of type double. The reason they are private is that the information is really owned by bioThing's public friend, bioWorld. It is more convenient for bioWorld to store the information with the things themselves. Subclasses of bioThing can get at the information through public member functions, but cannot set it directly. A change of position or orientation would typically be a side effect of intentions to move or turn respectively which are communicated to the world through the thing's _world data member. The thing's positional coordinates and orientation angle are stored as doubles so that they would not be restricted to discrete worlds.


class bioThing : public bioObject
{
  public:
  	ctor			bioThing (void);
  	dtor			~bioThing (void);

  	virtual int		ClassID (void) {return 0;}

  	bioWorld*		GetWorld (void) {return _world;}
  	double			GetX (void) {return __x;}
  	double			GetY (void) {return __y;}
  	double			GetOrientation (void) {return __orientation;}

  	virtual void		Look (void) {}
  	virtual int		GetIntent (void) {return 0;}

  	virtual void		Select (void) {}
  	virtual void		Edit (void) {}

  	friend class		bioWorld;

  protected:
	bioWorld*		_world;

  private:
  	double			__x;
  	double			__y;
  	double			__orientation;
};

Figure 27. bioThing class header

Methods

ClassID() provides run-time class information in the form of an integer value. This is useful to the world instance if it wants to do all the drawing and intention resolution rather than delegating those tasks to the things themselves. GetWorld(), GetX(), GetY(), and GetOrientation() simply return the current values stored in the instance.

Look() is sent by the world once for every action cycle. This gives the thing an opportunity to query the world for the current state of its local environment before it decides on an intention. The default implementation of Look() does nothing and GetIntent() returns 0 which should be interpreted as an intent to do nothing. Unless a subclass overrides both, it will be a passive object.

Likewise, Select() and Edit() are no-ops at this level. It is up to subclasses to define what should happen if the user clicks and double-clicks on a thing.

bioAgent

A bioAgent is a thing whose behavior is driven by an internal program.

Data

The bioAgent instance stores a pointer to its program and a value representing its current intention. The instance takes ownership of the program instance, and will delete it in the bioAgent destructor.


class bioAgent : public bioThing
{
  public:
  	ctor			bioAgent (void);
  	dtor			~bioAgent (void);

  	void			SetProgram (bioProgram*);
  	bioProgram*		GetProgram (void);

  	override void		Look (void) = 0;
  	override int		GetIntent (void) {return _intention;}

  protected:
	bioProgram*		_program;
	int			_intention;
};

Figure 28. bioAgent class header

Methods

SetProgram() and GetProgram() simply store and retrieve the agent's internal program instance. Look() has been redefined as a pure virtual which forces subclasses to provide an implementation. Subclasses are expected to set the value of the protected _intention data member during their Look() which is returned through bioAgent's implementation of GetIntent().

bioProgram

The bioProgram abstract base class provides a minimal public interface for the bioAgent class.

Data

bioProgram stores no data.


class bioProgram : public bioObject
{
  public:
    	virtual void		Init (void) {}
  	virtual int		Think (int* inputVector, int size) = 0;

  protected:
};

Figure 29. bioProgram class header

Methods

The Init() method allows subclasses to do any initialization best left out of the constructor such as allocating memory. The agent which owns the program will query its local environment during its Look() method. The information it retrieves from the environment is translated into an input vector in the form of an array of integers which is passed to the program's Think() method. Subclasses of bioProgram should use this input vector as input to whatever program they implement and return an intention in the form of an integer value.

bioFSM

bioFSM implements a finite state machine program. The FSM is a look-up table which combines it current state and the input into an index. The contents of the table at the index yields an output and the next state of the table.

Data

In addition to its current state, the bioFSM instance stores the number of bits in its input, output, and state. The size of the table is 2 raised to the power of the sum of by the number of bits in the input and state (e.g. an FSM with 2 input bits and 4 state bits has 64 possible states). Since the table itself is implemented as an array of 16 bit integers, the sum of the number of bits in the state and output cannot exceed 16. The rest of the data members are used to facilitate internal bookkeeping.


class bioFSM : public bioProgram
{
  public:
    	ctor			bioFSM (int, int, int);
    	dtor			~bioFSM (void);

    	override void		Init (void);
  	override int		Think (int* inputVector, int size);
  	virtual void		ReadArray (int* array, int size);

  protected:
    	int          		_state;
    	int                	_stateSize;
    	int                	_stateMask;
    	int                	_inputSize;
    	int                	_inputMask;
    	int                	_output;
    	int                	_outputSize;
    	int                	_outputMask;
    	int*               	_table;
    	int                	_tableSize;
    	int                	_index;
};

Figure 30. bioProgram class header

Methods

The constructor requires three parameters: the number of bits in the state, input and output. Init() calculates the derived data members and allocates space for the table. ReadArray() populates the table from the passed in array and is provided to allow agents to set the table from an instance of bioGType().

Think() expects an input vector with a size equal to the input size specified in the constructor. It constructs an index from its current state and the input vector, looks up a new state, and returns the output defined in the table at that index.

bioNNet

The bioNNet class implements a recurrent neural network. Each node in the network is connected to every other node by an integer weight. A zero weight is equivalent to not being connected. At any given time a node can be in one of two states, on or off. Nodes are designated as being input, internodes or output. The only difference between them is that the state of input nodes are determined outside the network, and the state of output nodes is exported outside the network.

Data

The number of input nodes, internodes, and output nodes are stored in data members along with the sum of the three. Nodes states, next states and bias values are stored in the arrays called _neuron, _newValues and _base. Weights are stored in a 2-dimensional array called _weights.


class bioNNet : public bioProgram
{
  public:
	ctor			bioNNet (int, int, int);
	dtor			~bioNNet();

	override int		Think (int* vector, int size);
	virtual void		ReadArray (int* input, int count);

	void			SetSize (int, int, int);

  protected:
    	void			_FireNeuron (int, int);
	int			_IsOutput (int);
	int			_Compute();

    	int			_size;
    	int			_inputs;
    	int			_inters;
    	int			_outputs;
    	int*			_neuron;
    	int*			_newValues;
    	int*			_base;
    	int**			_weights;
};

Figure 31. bioNNet Class Header

Methods

The number of input nodes, internodes, and output nodes are defined in the constructor while SetSize() does the actual work of allocating memory and initializing the values. ReadArray() initializes the values for the weights, node biases and initial activations. This would be called by bioPType instance after extracting the information from a bioGType instance. Think() uses the input vector to set the activations of the input nodes using _FireNeuron(), calls _Compute which runs the network through a cycle, and returns the states of the output nodes compressed into a single integer. During a _Compute() cycle, the inputs to each node are multiplied by their corresponding weight, summed up and added to the node's bias value and compared to a preset threshold. If the final sum of the inputs is greater than 100 (chosen arbitrarily) then the node is turned on (its value is set to 1), otherwise it is turned off (its value is set to 0).

bioGType

The Genotype object designed in the previous chapter is implemented in the bioGType abstract base class. Instances are typically owned by instances of bioPtype and manipulated by instances of bioPopulation. bioPTypes would use an instance of bioGType to construct the phenotype including their respective program instances.

Data

bioGType stores no data of its own.


class bioGType : public bioObject
{
  public:
  	ctor			bioGType (void) {}
  	dtor			~bioGType (void) {}

  	virtual void		SetLength (int) = 0;
  	virtual int		GetLength (void) = 0;

  	virtual void		Randomize (void) = 0;
  	virtual void		Mutate (double rate) = 0;
  	virtual bioGType*	Crossover (bioGType*) = 0;

  protected:
};

Figure 32. bioGType class header

Methods

All methods are defined as pure virtuals which forces subclasses to provide implementations. The length of the genotype which defines the number of alleles it stores is set and retrieved via the SetLength() and GetLength() member functions. Randomize() should set all alleles in the genotype to random values which is only useful for the first generation in a population.

For each bit in the genotype, Mutate() should toggle it with the probability passed in. Crossover() should create a new genotype of the same class by applying the crossover operator to itself and the instance passed in.

bioHapGType

A haploid (single-strand) genotype is implemented in the bioHapGType class. It provides implementations for all the pure virtual functions defined in bioGType.

Data

The number of bits in the genotype is stored in _bits. The genotype itself is stored in a character array _dna and the size of the array is stored in _bytes.


class bioHapGType : public bioGType
{
  public:
  	ctor			bioHapGType (void);
  	ctor			bioHapGType (bioHapGType&);
  	dtor			~bioHapGType (void);

  	bioHapGType&		operator= (const bioHapGType& rhs);

  	override void		SetLength (int);
  	override int		GetLength (void);

  	override void		Randomize (void);
  	override void		Mutate (double rate);
  	override bioGType*	Crossover (bioGType*);

  	virtual char		GetByte (int idx);
  	virtual int		GetBit (int idx);

  	virtual void		SetBit (int idx, int on = 1);
  	virtual void		ResetBit (int idx);
  	virtual void		ToggleBit (int idx);

  protected:
	int			_bits;	// bit count
	int			_bytes;	// string length
	char*			_dna;   // bit string
};

Figure 33. bioHapGType class header

Methods

SetLength() allocates space for the genotype and sets the size data members. GetLength() returns the number of bits. Mutate() doesn't roll the dice for each bit, but rather uses the mutation rate to calculate the index of the next bit to be toggled. This method provides substantial performance gains. The Crossover() method treats the bit strings as circular, choosing two points randomly on one string and replacing the segment in between with the corresponding segment from the second bit string. The rest of the methods are self-explanatory.

bioPType

The Phenotype object designed in the previous chapter is implemented in the bioPType abstract base class. Phenotypes are agents which are created from genotypes. Instances of bioPopulation are collections of bioPType instances.

Data

The bioPType class stores only two pieces of data at this level: a pointer to the bioGType instance passed in the constructor, and a data member representing the fitness of this particular instance. Subclasses are expected to set the _fitness member at some time during a breeding cycle. The bioPopulation class uses the fitness to determine which instances are used to breed the next generation.


class bioPType : public bioAgent
{
  public:
  	ctor			bioPType (bioGType* gtype);
  	dtor			~bioPType (void);

  	virtual bioGType*	GetGType (void);
  	virtual bioGType*	ExtractGType (void);

  	virtual double	GetFitness (void);

  protected:
	bioGType*		_gtype;
	double			_fitness;
};

Figure 34. bioPType class header

Methods

GetGType() and GetFitness() simply return the instance's current values. bioPTypes will delete their _gtype member in the destructor. ExtractGType() is used by bioPopulations to transfer a bioGType instance from one phenotype to another, essentially a cloning operation.

bioPopulation

The Population object is realized in the bioPopulation abstract base class. Instances of bioPopulation store and manipulate and collection of bioPType instances along with their corresponding bioGType instances. bioPopulations essentially implement the genetic algorithm.

Data

The phenotype instances are stored in an array called _ptypes. The size of this array is kept in _size. A number of top performers in each generation may automatically make it into the next generation. The number of these special phenotypes is stored in the _elite data member. In addition, a few statistics related to fitness are kept for each generation: the minimum, maximum, average and total fitness.


class bioPopulation : public bioObject
{
  public:
  	ctor			bioPopulation (void);
  	dtor			~bioPopulation (void);

	virtual void		Breed (void);

  	virtual void		SetSize (int);
  	virtual int		GetSize (void);

  	virtual void		SetMutationRate (double);
  	virtual double	GetMutationRate (void);

  	virtual bioGType*	GenGType (void) = 0;
  	virtual bioPType*	GenPType (bioGType*) = 0;

	virtual bioPType*	GetPType (int idx);

	virtual double	GetMaxFitness (void);
	virtual double	GetMinFitness (void);
	virtual double	GetAvgFitness (void);

  protected:
	bioGType*		_ChooseParent (void);

	int			_size;
	int			_elite;

	bioPType**		_ptypes;

	double			_mutationRate;
	double			_totalFitness;
	double			_maxFitness;
	double			_minFitness;
	double			_avgFitness;
};

Figure 35. bioPopulation

Methods

GenGType() and GenPType() are pure virtual methods which generate new genotypes and phenotypes respectively. Subclasses must provide implementations which construct new instances of the appropriate class.

SetSize() allocates space for the population and generates an initial population by calling GenGType() and GenPType(). GetSize() returns the current population size. The population's mutation rate is stored and retrieved with SetMutationRate() and GetMutationRate() respectively.

Breed() performs a single step in the genetic algorithm. The population array is sorted according to fitness. Minimum, maximum, and total fitness are recorded and average fitness is calculated. Space for the next generation is allocated and the elite phenotypes are created with GenPType() using genotypes directly from the current generation. The rest of the next generation is created by following these steps:

The various fitness statistics can be retrieved with GetMaxFitness(), GetMinFitness(), and GetAvgFitness().