-
Notifications
You must be signed in to change notification settings - Fork 113
Development Tutorial Environment
The environment source code consists of several main components: resources, reactions, and task triggers, plus the libraries that maintain each of these.
A task library is composed of a collection of entries, each of which fully describes a single task that can be used to trigger reactions.
typedef double (cTaskLib::*tTaskTest)(cTaskContext*) const;class cTaskEntry { private: cString m_name; // Short keyword for task cString m_desc; // For more human-understandable output... int m_id; tTaskTest m_test_fun; cString m_info; // extra info (like the string or whatever to match)
public: cTaskEntry(const cString& name, const cString& desc, int in_id, tTaskTest test_fun, const cString& info); : m_name(name), m_desc(desc), m_id(in_id), m_test_fun(test_fun), m_info(info) { } ~cTaskEntry() { ; }
const cString& GetName() const { return m_name; } const cString& GetDesc() const { return m_desc; } const int GetID() const { return m_id; } const tTaskTest GetTestFun() const { return m_test_fun; } const cString& GetInfo() const { return m_info; } };
Task entries are very straight-forward. They consist of a name, a description, a unique ID number, and a method from the task library (cTaskLib) that they are associated with. This method looks at the inputs the organism has taken in, the values it has output, and returns a number between 0.0 and 1.0 representing how well the task was performed. Currently, all task tests will return an exact zero or one, but fractions are possible if there is a quality component associated with the task.
Here is an abridged version of the task library class that manages all of the individual entries:
class cTaskLib { private: Apto::Array<cTaskEntry*> task_array; public: int GetSize() const { return task_array.GetSize(); } cTaskEntry* AddTask(const cString& name, const cString& info); const cTaskEntry& GetTask(int id) const; void SetupTests(cTaskContext& ctx) const; inline double TestOutput(const cTaskEntry& task, cTaskContext* ctx) const; private: double Task_Echo(cTaskContext* ctx) const; double Task_Add(cTaskContext* ctx) const; double Task_Sub(cTaskContext* ctx) const; double Task_Not(cTaskContext* ctx) const; double Task_Nand(cTaskContext* ctx) const; double Task_And(cTaskContext* ctx) const; // ... And a whole bunch more ... };
The task library contains an array of task entries that define all of the rewarded (or otherwise acted upon) tasks in an environment.
The TestOutput() method can only be run with as cTaskContext object that has been initialized with the SetupTests method. It will test the specific task passed in and return the 0.0 - 1.0 quality measure of how well that task was done with the most recent output.
Below is a sample task-tester implementation:
double cTaskLib::Task_Add(cTaskContext* ctx) const { const int test_output = ctx->output_buffer[0]; for (int i = 0; i < ctx->input_buffer.GetNumStored(); i++) { for (int j = 0; j < i; j++) { if (test_output == ctx->input_buffer[i] + ctx->input_buffer[j]) return 1.0; } } return 0.0; }
This case tests to see if the organism has performed an addition operation. It compares all pairs of inputs summed together against the most recent output of the organism. If there is a match a full reward (1.0) is given. If no match is found, no reward is given (0.0).
The SetupTests method performs some precomptution for all of the logic tasks, creating the value logic_id within the task context. The logic_id has 256 possible values, each of which can only be associated with a single logic task. These tests look more like:
double cTaskLib::Task_AndNot(cTaskContext* ctx) const { const int logic_id = ctx->logic_id; if (logic_id == 10 || logic_id == 12 || logic_id == 34 || logic_id == 48 || logic_id == 68 || logic_id == 80) return 1.0; return 0.0; }
If the logic ID is on the list, the task has been done, otherwise it hasn't. In each case, the outside world needs to request a test of which tasks have been performed, and the library just replied with a numerical answer.
The reaction class keeps track of all of the information associated with a single possible environmental reaction. Each reaction must have a unique name and a unique numerical ID associated with them. In addition to those data, a reaction object also has a task that acts as its trigger, a list of other requisites that must be met for the trigger to work, and a list of processes that will occur if the reaction goes off. The cReaction object acts a a single place to store all of this information.
Resources are a little more complicated than task entries to manage and understand. An object of type cResource contains 19 pieces of data, and the associated accessors. Like all of the other individual units we have discussed, resources have a unique name and numerical id. For all resource we store the quantities associated with their inflow, outflow, and initial count (each stored as a double) as well as the geometry of that resource.
For spatial resources we need to be able to describe how a resource exists in space so we store data for:
- inflowX1, inflowX2, inflowY1, and inflowY2 to describe a rectangle where resources flow in.
- outflowX1, outflowX2, outflowY1, and outfowY2 for a rectangle where resources flow out.
- cell_list is a list of individual cells with their own initial, inflow and outflow values.
- xdiffuse and ydiffuse describe how fast resources will flow from cells of higher amounts of that resource to cells with lower amounts of that resource.
- xgravity and ygravity describe the preferential flow of resource in a given direction.
This class describes the dynamics of a resource, not its current count (since, for example, we might want local resources where each cell would have its own count). However, every time a resource is needed, any changes in its quantity from the last time it was used can be calculated using these numbers.
The cEnvironment class is used to maintain the details of how the environments work using the classes described above and a few others. Below is an abbreviated version of this class:
class cEnvironment { private: // Keep libraries of resources, reactions, and tasks. cResourceRegistry resource_lib; cReactionLib reaction_lib; cTaskLib task_lib; cInstLib inst_lib; cMutationRates mut_rates; public: bool Load(const cString& filename); // Interaction with the organisms bool TestOutput(cAvidaContext& ctx, cReactionResult& result, cTaskContext& taskctx, const tBuffer<int>& send_buf, const tBuffer<int>& receive_buf, const Apto::Array<int>& task_count, const Apto::Array<int>& reaction_count, const Apto::Array<double>& resource_count) const; };
The private data members include all of the libraries needed to specify the environment, plus its mutation rates. The Load() method takes a filename (environment.cfg by default) and will fill out all of the libraries in this environment. The most important feature of this class is the TestOutput() method, which takes in all sorts of information about the current state of the organism that has just done an output and fills out an object of type cReactionResult with information about what happened. It also directly returns a bool that will indicate if there have been any changes at all. The specific information it uses to determine the results are the inputs the organism has taken in and the outputs it has produced -- both needed to determine what tasks have been done, and therefore what reactions may have been triggered. This information is encapsulated in the task context taskctx. The organism's previous task_count and resource_count are also needed to determine if the reactions requisites have been met. And finally the resource_count available to the organisms is needed to determine how much of each resource can be used in the reactions.