|← previous OutputData||AX Tutorial||→ next CPTAX_Program|
NOTE: Within emergent, you can return to this document by selecting
docs / TaskProgram in the left browser panel).
Programming the Task Environment
The core of most any simulation is the task environment and creating it is usually the most complicated and time-consuming aspect of the whole endeavor. The goal is this part of the tutorial is to write a program that will automatically generate a set of input/output patterns to train the network. Because our current task is so easy (at least to start), this effort will not be actually saving us any time, but it will hopefully begin to generate an understanding of how programs work and provide a basis for building more complex projects in the future --- when they can be major time-savers.
- The major steps involved are:
- Create the new program object
- Create special "enums" based on unit names -- allows us to refer to units by name (an enum is geek-speak for an enumerated set of labeled values -- more later).
- Create some program code that will iterate over the various trial types we want the network to experience, thereby defining the inputs and target outputs characterizing each trial.
- Write the appropriate environmental input and target output information into the input datatable, i.e.,
Create the Program
It is good to get in the habit of entering descriptions of various objects in your simulation, especially programs, so enter something like "generates the simple A-X target detection task" in the
desc field of the program.
Note that there are four sub-tabs or sub-panels for any program in emergent, displayed along the top of the middle Editor panel:
Program Ctrl: For the "end user" to control the running and key parameters of the program
Edit Program: For writing the program.
css Script: shows the css code that is generated by the Program, which is actually what runs. You can do powerful debugging and single-step running of the code in this panel.
Properties: For setting overall parameters of the program object, including
tagswhich help people find this program if it is uploaded to a common repository, and
flagsthat determine various advanced properties.
Edit Program tab and we can get started.
Overview of Programing Process
Basic programming in Emergent mostly consists of dragging program elements from the vertical Tools Toolbar along the very left edge of the display into your program, and then configuring their properties. You will also find that once you've added several of these elements to your program, it will often be easiest to duplicate, drag-and-drop, and then edit existing elements for a new purpose.
program object has several different locations for different types of program elements:
objs-- place to put misc objects that contain local data for the program (e.g., a local DataTable that might hold some intermediate processing data for the program).
types-- special user-defined types that define properties of corresponding variables (e.g., the enums we'll be using)
args-- (short for arguments) this is where you put variables that other programs will set when they run this program
vars-- place for other non-argument variables that are used in the program
functions-- you can define subroutines (functions) that can be called within a program to perform a given task that needs to be done repeatedly. These functions are only accessible from within this given program
init_code-- actions to be performed when the user presses the Init button -- to initialize the program and other objects that it operates on (e.g., initializing the network weights, as the initialization process does).
prog_code-- finally, this is where the main code for your program goes! Because the program code can depend on any of the preceding elements, it logically comes last (and it is typically the largest).
In the Tools toolbar, the program elements are organized under various tabs:
Functions, and so on. Click through the tabs and use the mouse-over to get an idea of the kinds of things are available.
Next, we'll create the enums mentioned previously. With the
AXTaskGen program open context-click on the
types group and select
New Dyn Enum. In the green edit panel at the top change the name to 'Input' and
Apply Then click on the
New Enum button and rename the new element created as 'I_A'. Repeat five more times for enum values I_B, I_C, I_X, I_Y, and I_Z. Then create one more named 'NInputs' which is the total number of Input values we are using. In a similar manner, create a second
New Dyn Enum, name it 'Output', and create three new elements with the
New Enum button. Name them O_N, O_T, and NOutputs. The first letter is taken from the first letter of the layer (I = Input, O = Output), and the remainder after the underbar is the name we are using to refer to the various stimuli.
The purpose of these enum data types is to allow you to use a symbol to refer to a unit. If you want to activate the X input unit, you can use the I_X enum value to do that. It represents the index of the X unit within the input layer -- when you click on I_X, you can see that it has a value of 3. Enums have both numeric and symbolic (string-like) properties, and can be converted to and from names and numbers. You'll understand more about why they are so useful as we go along.
Let's move on with writing our program.
Iterating over the Inputs
The core of our program will be to loop or iterate over each of the possible input units, and then generate an appropriate output for each. We will use a for loop for this purpose.
In the left-side Tools, click on the
Control tab and drag the
for element into your program code (
prog_code section within
programs → AXTaskGen).
You should see three main fields in the purple editing panel for the
for loop object that appears at the top of the Editor (middle panel):
iter -- the default values produce a
for loop that loops ten times (counts from 0 to 9):
initis for initializing your looping variable (
i = 0), where
iis the integer variable that keeps track of where we are in the loop -- it was automatically created by the for loop element.
testis for testing when to terminate the loop (
i < 10) -- as long as the
ivariable remains less than 10, we continue looping.
iteris what to do on each iteration prior to the test (
i++) -- i.e., increment the loop variable.
To see this in action, drag the
print variable program element (under the
Tools/Function tab) into the
loop_code of the
for loop. This is where we put the program statements ("code") that we want to run during each iteration of the loop. Select the
i variable for the
print_var field. Notice that you can print up to six variables using this single function.
Now we can and our simple AXTaskGen program. You should see a sequence of "i=0, i=1...i=9" in the css console window (typically located below the main project window). It is a very good idea to keep that window visible during when creating and running programs, as various informative messages may show up there.
Next, we want to define our trial types by iterating over all of the possible input values. To do that, we need to click on the variable in the
LocalVars section of the program (this was auto-created by the for loop -- LocalVars is a good place to put variables like this that the user doesn't set -- variables that are fully under Program control). Change the
var_type to DynEnum instead of Int. Then, click on the
enum_type field and select the enum Input type (which is what we created earlier). You can also change the name of this
i variable to something more expressive, like
input_unit. Note that when you apply this name change, the
for loop code automatically updates to use this new name, as does the
VERY HELPFUL TIP: You can auto-complete many fields without much typing by selecting from a list of known variables and/or enums by using the Ctrl-L "lookup" functionality. Also,
lookup_enum buttons are available in some cases.
input_unit = I_A
input_unit = I_B
input_unit = I_C
input_unit = I_X
input_unit = I_Y
input_unit = I_Z
Programming Shortcut: Just type it in!
As an alternative to drag-and-drop programming as we've been doing, you can work directly within the
prog code section much like you would with a normal text editor. For example, you could have done the above by clicking on the
prog_code section, then typing the following:
<Enter> for(input_unit=I_A; input_unit <= I_Z; input_unit++) <Enter>
(where <Enter> means hitting the enter/return key) -- the first <Enter> creates a new blank line below the
LocalVars (or wherever the cursor was at the time), and then typing in the code will create a for loop program object, with the expression as typed -- it will prompt you to create the
input_unit variable if it didn't already exist, etc. As you get more proficient with programming, this can be faster than dragging-and-dropping. You can just enter the basic keyword (e.g., "for" or "if" etc) and fill in the expression in the edit dialog, or enter the full expression like we did here -- it is very flexible.
For blank new lines, you can just start typing and it will automatically enter the text in the inline editor. To edit existing lines in the inline editor, just press
Ctrl+a to edit at the start, or
Ctrl+e to edit at the end (these are standard emacs keys -- see keyboard shortcuts)
- TIP: One subtlety when creating new lines in an Emergent project program has to do with a "step-over" versus "step-into" distinction regarding control elements like
ifclauses, etc.: if you select an existing element such as the
forloop we just created, pressing
Enterwill do two different things depending on whether the
forloop has any content in it yet. On the one hand, if you just created the
forloop and there's no actual code inside of the loop,
Enterwill add a new line inside the loop so you can start adding some; however, if there is already some code in there,
Enterwill step over the loop entirely and add a blank line outside of and after the
forloop block of instructions.
Generating the Correct Output
Next, we need to generate the correct output (target or non-target) corresponding to each input value. To do this, we first need to create a variable to hold the output value. Context-click on the
LocalVars group and select
Add Var. Set the new variable's
name to output_unit, change the
var_type to DynEnum, and then click on the
enum_type field and select the enum Output type.
Now we just need to set this variable inside our for loop code, depending on the value of the
input_unit variable. From the
Tools/Control tab, drag the
if element into the
for loop code, and drop it on top of the
Print statement we put there earlier. (The
if will become the first element in the loop.) In the
cond conditional expression, enter:
input_unit == I_A || input_unit == I_X. Again note that you can use the
var lookup or Ctrl+L to save some typing (and it also makes sure you've spelled them correctly). The
== is the logical test for equality, and the || is the logical OR operator (this is standard C++ syntax -- any valid C++ expression can be entered here -- see css expressions). This is the condition for a target.
We need to assign our
output_unit variable to the target value when this "if condition" is true, and to the non-target case otherwise. To assign a variable value, drag
variable= from the
Tools/Assign tab onto the
if statement (and select Copy Into). For the
result_var, select output_unit, and for the
expr expression, type in O_T (or choose O_T after using the Ctrl-L look-up). This will set output_unit to O_T (target) when the condition is true.
Next, we need to create an
else condition -- drag that on top of the existing
Print statement again, and then, as a timesaver, drag the previous
variable= code from the
if into the
Copy Into) -- then you can just change O_T to O_N very quickly. This sets output_unit to O_N (non-target) when the condition is false.
TIP: When using drag-n-drop to add elements to your code, if you drop the new element onto a "code" section (like
prog code), the
Copy Into option is used automatically and the new element will be appended (added as the last item) in that section. If you drop the new element onto an existing program statement (e.g.,
Print ...), the
Copy Here option is typically used automatically, so that the new element will be added in front of/above the existing element. Dropping onto is indicated by a box around the section/folder name or element you are dragging over. You can also slowly drag-n-drop a new element, waiting for a line to appear to indicate the exact position where the new element will be placed using
Copy Here (rather than a box indicating drop onto).
Finally, go back to the
Print statement and select output_unit for
print_var2. Your code at this point should look something like this:
LocalVars (2 vars) input_unit = I_A (Input) output_unit = O_N (Output) for (input_unit=I_A; input_unit <= I_Z; input_unit++) if (input_unit == I_A || input_unit == I_X) output_unit = O_T else output_unit = O_N Print: input_unit output_unit
(Again, you could have just typed this whole thing in directly in this format!)
input_unit = I_A output_unit = O_T
input_unit = I_B output_unit = O_N
input_unit = I_C output_unit = O_N
input_unit = I_X output_unit = O_T
input_unit = I_Y output_unit = O_N
input_unit = I_Z output_unit = O_N
Writing the Data to the Input Data Table
Now that we have all the key logic for the task, we need to write the results to the
input data table. There are four basic steps for this:
- Add a variable pointing to the input data table to the program
- Erase any existing data at the start
- In the
forloop we've already created, add a new row then...
- ...write the data for each trial to the new row (name, input, output), then...
- ...tell the system that we're done writing to each row so it can update the view
First, we need to add in a pointer variable called
input data that will point to the StdInputData table. The easiest way to do this to open up your AXTaskGen program (click on the arrow on the side), then scrolling up the side Navigator window, and click and drag the StdInputData table from the data folder over the vars folder in the AXTaskGen program, clicking on Add Var To once you drop it there. Another way to do this is to go to the program vars, click new (set the name to
input_data) and then click ok, then set the type to object and the min type to DataTable. Set the object val to StdInputData.
The second step is achieved by dragging a
reset rows element from the
Tools/Data R/W tab to the first line of the
prog code section (drop right on top of the
for loop) and choose
Copy Into. Then select
input_data for the
data_var field to denote what table we're resetting. This program statement will erase any existing data in the datatable when the program is run, while leaving the basic column structure of the table intact.
Next, drag-and-drop the
new row element from the
Tools/Data R/W onto the
for loop as before (adds it at the end). Again select
input_data for the
data_var field. This will add a new blank row to our
input_data data table (StdInputData).
Next, we want to write the
Name of the trial to StdInputData. Since there is a widget on the
Tools/Data R/W toolbar for reading and writing simple non-matrix cell data to and from tables we'll use that. But, first, we need to create a
Name variable to hold the values.
- Context-click on
Add Varand enter
namefield. Set the
- Since the value of the
input_unitcompletely defines our trial types we can just use that for naming. From the
variable=element onto the
forloop block and set
Name. Then, enter
input_unitis an enum this assignment will assign the character string associated with its current value (e.g., "I_A"); however, that's not exactly what we want for our naming scheme so we'll clean it up a bit.
- Duplicate (e.g.,
Ctrl-m) the assign statement we just made onto the next line, keeping the same
result var. In the
exprfield, however, replace "input_unit" with the following expression (exactly):
Name.after("_")and click apply. The
after()method available to instances of the
Stringclass (which is what "Name" is) will snip the beginning "I_" off the character string, just leaving the terminal character --- which is exactly what we want! To confirm this, click on the
Print ...statement again and set the
var 2field to
Name</code. This hit <code>AXTaskGen/Initand
Now we're almost ready to actually write everything to our StdInputData table, but first we need to create a program variable to be able to access it.
- Context-click on the
varssection of the program and select
New>. Fill in the appropriate fields in the brown editor panel at the top as follows:
name = input_data;
var type = Object*;
min type = DataTable; and
object val = StdInputData. Click
NOW we're actually ready to write out data:
- Drag-and-drop the
Tools/Data R/W) onto the
forloop (Copy Into). This should appear directly after the
Add New Row...statement.
- In the green editor section at the top select the
input_datavariable we just created in the
data varfield, and then click the
set_dataflag to true to write data to the data table (otherwise, data is read from the data table to variables sharing the same name with selected columns). Leave
row_spec = CUR_ROWand
all matchesfalse (unchecked).
- Finally, select
var 1field. This will write the current value of the
Namevariable (which should be the name of the current trial once everything is working properly) to the Name column of our StdInputData table.
Next, we also need to write the data to the Input and Output columns which as we saw earlier are composed of matrix-type cells that store arrays of data. Since there is no special widget for that in the
Data R/W section of the toolbar we'll do it another way.
- Go to
Tools/Functionssection and select the
method()element. Drop it onto the
forloop again and in the
objfield that appears in the blue editing region at the top select
- Then, in the
methodfield, scroll down and select
SetMatrixFlatVal. This writes data to matrix-type columns using a "flat" scalar-valued indexing scheme, which is perfect for using enums as indicies!
- You should observe that four argument fields opened up under
SetMatrixFlatVal(,,,), which should be filled in by selecting each in turn as follows:
Variant& val = 1;
Variant& col = "Input";
int row = -1; and,
int cell = input_unit. Key: val = the value to actually enter; col = which table-level column to use (IMPORTANT: needs the parens); row = -1 designates the last and current row; and cell = the sub-cell in the array/matrix to enter the data in, using a flat scalar indexing scheme. Using enums comes in particularly handy here because of their dual-personality as integers and symbols. It turns out that we actually created the enum in the beginning with this indexing scheme in mind so that the integer value corresponds to the appropriate index here and, of course, we can read the character rendering so we know what it is -- this makes it easy to check that it's doing the right thing. This is a nice illustration of the many advantages of using enums!
Next, we want to do the same thing for the Output column, but now our job is really easy:
- Duplicate (e.g., Ctrl+m) the
- Then, all we need to do is to change col = "Output" and cell = output_unit and we're done!
Finally, go back to the
Tools/Data R/W tab and drag-and-drop the
row done element onto the
for loop (Copy Into). This simply lets the system know that you're done writing to the current row of data, and that it can update any relevant displays.
prog code section should now look like something like Figure 1 at the right.
Congratulations -- you're done!!! Select the tab in the right Visualizer panel to view the grid view we created earlier. Then, and your AXTaskGen program several times -- you should see the display update with the correct patterns freshly generated by your program!
TIP: If the grid view doesn't seem to be changing each time you run the program it's probably because it's going too fast. You can try going to the underlying StdInputData table itself and deleting all its rows prior to running the program (while watching the grid view). That way you'll at least be able to see your program write the values the first time you run the program.
Creating a ControlPanel
A ControlPanel provides a convenient way for you and others to run your models. Control panels also provide a way to consolidate various important parameters in one place. So, let's create one, and populate it with a few things.
First, go to ctrl_panels section near the top of the left Navigator tree, and after selecting hit you can just hit
Return to create a new ControlPanel object. It its
Properties tab (middle Editor panel) rename it without the "_*" suffix to make it simpler ("ControlPanel").
Next, click on the LeabraTrain program, and, in the smaller menus just above the
Edit Program tab (top of middle Editor panel), find and click on the
Control Panel dropdown menu icon (far right with little arrow in front of it), and click the
Add Ctrl Funs to Control Panel button -- this adds the
Stop buttons to the control panel.
Finally, we can add a parameter to our ControlPanel, for example the learning rate, which is often an important parameter. Go to the
LeabraConSpec_1 object under
Network_1/specs, and then do the Context-click (right button or Ctrl + click) on the
lrate label in the green Editor panel-- you will see a menu with an option to
Add to Control Panel, where you should then select
ControlPanel (there is also a
ClusterRun control panel option provided). Now, when you click back on your
ControlPanel, you will see that parameter has been added (make sure you're in the
Control Panel tab and not
Properties. This parameter is now easily available to be modified right there in the
ControlPanel, along with the
Run buttons to run your model, all in the same place! Control panels are very widely used in most models and it is common to have more than one in a single model to serve different purposes. For example, the ClusterRun control panel we ran into a moment ago is extremely helpful for automating the running of a model on a compute cluster -- very convenient for exploring a lot of parameters!
You have now gotten a flavor for many of the main elements involved in building a running a simulation project using Emergent. The next two (extra) steps in the tutorial are advanced topics take this simple starting point and extend the project into scientifically interesting model of an actual psychological task, the CPT-AX task. It is a good idea to go ahead and save your project now with a new name that reflects the completion of this basic tutorial -- e.g., ax_tutorial_my_final.proj. Especially if you are fairly confident in your end result you can use your project as the starting point for doing the extra/advanced steps that come next. Alternatively, you can use the completed
ax_tutorial_final.proj version of the basic tutorial included in the
../demo/leabra folder of the Emergent software, or available for download on the emergent wiki here: tutorials/ax_tutorial_final.proj