Events is one of the most complex sub-systems within avida, but because of this we have automated the process of implementing new events. Normally, when you want to add to a component of avida, you need to make changes in at least three places: the header file to declare new classes or methods, the code file to write the full definition of those methods, and a third location to activate whatever you just wrote. A conscientious programmer will also make a fourth change to document this new feature.
Events used to be the worst case of this. Not only were more than these four procedures required, but they had to be done in a specific format that even I would need to look up.
A few years ago Travis Collier wrote a program (in the language Perl) that would parse a single, specially-formatted file and write out the corresponding C++ code, including documentation. This program is "current/source/event/make_events.pl", in case you are interested in it (fortunately, you don't need to understand it to add events because Perl code can be a bit dense). The file that the program reads in is "current/source/event/cPopulation.events", which is what you will actually be editing.
When you type "make" to compile Avida, it will automatically look to see if any modifications have been made to cPopulation.events, and if so it will run make_events.pl in order to update all of the event-related C++ files.
This file defines all events currently in avida, most of which affect the main object from class cPopulation, hence the origin of the filename. There were once other event-definition files named for non-population objects that could be affected, but we collapsed it all down to this single file to avoid confusion. Ideally we'll change this file's name soon for further clarity.
The text in this file is broken up into blocks, separated by blank lines. Each block represents the full definition of a single event. Do not skip any lines when you are defining an event, or else the Perl script will think you have begun the definition of a new event. Below is the basic format for a single event. Anything in brown should be changed to be event specific, while portions in black are the same for all events:
event_name :descr: /** * This is the documentation for this event. Notice that it can be * multiple lines, but each line must begin with a single asterisk. **/ :args: variable_type variable_name default_value var2_type var2_name var2_default ... ... ... varN_type varN_name varN_default :body: FirstThingToDo(); ThenDoThis(); AndAnyOtherFunctionsToBeRun();
For any event whose only function is to execute a method of the population object there really isn't much to write. All events have access to population, which is a pointer to the primary population (of class cPopulation) in the Avida run. Additionally the events have access to several static classes such as cAvidaDriver_Base, but since this isn't something you've learned about yet, you shouldn't worry about it here.
Let's step through the sections of the event definition. First we have the event_name, which can be any alpha-numeric sequence plus the underscore. This name is the name that is used from the events.cfg configuration file to specify that this is the event to be triggered.
The :descr: section is dropped as-is into the avida source code as the comments for this event. In C++, two methods for including comments are possible. The first, is a pair of slashes ('//') that make the remainder of the line a comment. The second is a slash-star ('/*') to begin a comment, and then a star-slash ('*/') to denote its end. This latter approach allows a command to go for multiple lines. To make it clear to a reader that this is all part of a single comment, good programming practice dictates that we should begin each line with a single star ('*'). In addition to being placed in the source code, this documentation is output when you run avida with the "-events" flag from the command line. In the future, it will also be available from the graphical interface.
Next, we have the :args section of the event description. Here we list the variables that we want the user to set when they include this event in their configuration. For example, if we create an event that forces a single organism to write a term paper, we need to specify the organism that will be subject to this unfortunate task. We might include a line in this section like "int cell_id". This would mean that when the user sets up this event, they must specify which cell they are targeting. We could then include a second argument "int num_pages 5" so that the user can also specify how long the term paper should be. But notice that I included a "5" in this latter example. This means that the user can include the second argument to specify the number of pages, but if they don't, 5 will be the default. Thus the event "write_term_paper 100 3" would make the organism in cell 100 write a three page term paper, while "write_term_paper 42" would make the organism in cell 42 write a five page paper. Since we did not include a default argument for cell_id, that variable must always be specified. Now, all you need to do is figure out how to best write this event, and you'll never have to write a paper again! Unfortunately, knowing them, they would probably make huge margins and pad the paper with a bunch of nop-X commands, hoping we won't notice.
Finally, we come to the :body: section, which contains the commands to be executed when this event is run. Since so much functionality is already implemented in the cPopulation class, quite often all that we need to do here is run a method of the population object. Below is such an example, using the inject command.
The section headings are all bold; the remaining code uses a color scheme similar to the one I used previously:
Comments are BROWN Names of Methods are GREEN | Names of types (including
classes) are RED Variable names are BLUE |
inject :descr: /** * Injects a single organism into the population. * * Parameters: * filename (string) * The filename of the genotype to load. If this is left empty, or the keyword * "START_CREATURE" is given, than the genotype specified in the genesis * file under "START_CREATURE" is used. * cell ID (integer) default: 0 * The grid-point into which the organism should be placed. * merit (double) default: -1 * The initial merit of the organism. If set to -1, this is ignored. * lineage label (integer) default: 0 * An integer that marks all descendants of this organism. * neutral metric (double) default: 0 * A double value that randomly drifts over time. **/ :args: cString fname "START_CREATURE" int cell_id 0 double merit -1 int lineage_label 0 double neutral_metric 0 :body: if (fname == "START_CREATURE") fname = cConfig::GetStartCreature(); cGenome genome = cInstUtil::LoadGenome(fname, population->GetEnvironment().GetInstLib()); population->Inject(genome, cell_id, merit, lineage_label, neutral_metric);
As indicated by its description, the "inject" command will inject a single organism into the population. The user can specify a filename that contains the organism's genome (or START_CREATURE to use this value from the genesis file), the cell in which the organism should be placed, the merit to start the organism with (or -1 to auto-calculate it on loading), a lineage label to tag all of this organism's offspring, and a neutral value that will drift over each generation. Each of these configuration values is supplied with a default so that the user is not required to enter any additional information.
But what actually happens when this event is run?
First, the if-statement checks if the user has entered (or left as default) the value "START_CREATURE" as the filename. If so, it looks up the proper filename (from the cConfig class that holds the current state of all the genesis variables -- we'll talk more about it soon) and sets this variable such that it is now indicating a real file.
The next line builds an object of class cGenome using a function called LoadGenome in the utility class cInstUtil. This function needs the filename to load from, and an instruction set to convert the contents of that file into a genome. The instruction set is stored within the population's environment, and therefore requires a series of Methods to be called to retrieve it.
Finally, this event takes that newly created genome, and runs the method Inject on the population object. This method takes in all of the information about the organism to be injected and then actually builds the cOrganism object and places into the population.
Below is a slight variation on the inject command. Rather than injecting a single organism into a specific cell, it will inject identical organisms into every cell in the population.
inject_all :descr: /** * Injects identical organisms into all cells of the population. * * Parameters: * filename (string) * The filename of the genotype to load. If this is left empty, or the keyword * "START_CREATURE" is given, than the genotype specified in the genesis * file under "START_CREATURE" is used. * merit (double) default: -1 * The initial merit of the organism. If set to -1, this is ignored. * lineage label (integer) default: 0 * An integer that marks all descendants of this organism. * neutral metric (double) default: 0 * A double value that randomly drifts over time. **/ :args: cString fname "START_CREATURE" double merit -1 int lineage_label 0 double neutral_metric 0 :body: if (fname == "START_CREATURE") fname = cConfig::GetStartCreature(); cGenome genome = cInstUtil::LoadGenome(fname, population->GetEnvironment().GetInstLib()); for (int i = 0; i < population->GetSize(); i++) { population->Inject(genome, i, merit, lineage_label, neutral_metric); } population->SetSyncEvents(true);
The first difference that you should notice is the lack of the cell_id variable. Since we are no longer injecting into just a single cell, no cell ID needs to be included. The remaining differences are all in the body of the event.
The first two commands in the body remain the same; that is, the filename is finalized, and the genome used to create the organisms is itself constructed. The next thing we need to do is actually inject the organisms into each cell of the population. To run the Inject command once for each position we employ a for command. This command has a strange syntax:
for ( setup ; continue_test ; step_method ) { loop body }
In our case, we want to set a variable to every number that represents a cell in the population. These numbers go from zero to the population size minus one. To build this loop, we setup by creating a variable called i and initialize it to 0 -- the ID of the first cell we plan to inject into. We want to keep injecting into all cells with ID less than the full population size, so our continue_test tells us to keep going as long as i is less than that number. Finally our step_method tells the program that after each execution of the contents of the loop, it should increment the value of i by one. The Inject method itself works the same as in the previous event.
Finally, we run the method SetSyncEvents(true) on the population object. This is not necessary, but it helps keep the event triggers in proper sync. Since we just injected organisms into every site in the population, we have effectively reset the generation of the population to zero. This could screw up some events that are triggered based on generation if we're not careful, so we warn the population that such trouble may occur, and it should re-sync the events at its earliest convenience.
So that isn't so hard is it? Good! Now we get to some...
Here are a couple of example events that you can try implementing. The first one of them is the "inject_range" event. This event will be used when you want to inject organisms into more than one cell in the population, but not all of them. Where the normal inject event took a single cell_id value, this new event should take two of them -- a starting cell and a stopping cell. You should then use a for loop (similar to the one in "inject_all") to inject the genome into all cells in range. Extra credit if you test the order of the two numbers and swap them if the start_cell is larger than the end_cell.
The second event you are going to implement is "kill_cell"
This is again somewhat similar to inject in code structure, but
you're goal is to kill the organism (if one exists) in the specified cell.
For help with how to do this, take a look at the apocalypse event,
which causes a larger scale extinction.