AXTut TaskProgram

From emergent
Jump to: navigation, search
← 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:
  1. Create the new program object
  2. 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).
  3. 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.
  4. Write the appropriate environmental input and target output information into the input datatable, i.e., StdInputData.


Create the Program

Click on the programs group in the left Navigator tree, and hit the New button at the bottom of the middle Editor panel. The default parameters are fine, so hit OK.

This should have created a Program_12 or similarly named program, which you should now click on, and change the name to: AXTaskGen. As the rest of the links here will assume this name, enter exactly this name.

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 tags which help people find this program if it is uploaded to a common repository, and flags that determine various advanced properties.

Select the 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.

The 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 LeabraTrain 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: New, Control, Assign, Functions, and so on. Click through the tabs and use the mouse-over to get an idea of the kinds of things are available.


Creating Enums

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): init, test, iter -- the default values produce a for loop that loops ten times (counts from 0 to 9):

  • init is for initializing your looping variable (i = 0), where i is the integer variable that keeps track of where we are in the loop -- it was automatically created by the for loop element.
  • test is for testing when to terminate the loop (i < 10) -- as long as the i variable remains less than 10, we continue looping.
  • iter is 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 Init and Run 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 i 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 print code.

Next, let's go back to that for loop (for loop), and change the test field to: input_unit <= I_Z. Note how you can just type in I_Z and this is automatically treated as a number -- this is what enums do.

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_var and lookup_enum buttons are available in some cases.

Init and Run the AXTaskGen program again. You should now see the following sequence appear in the console window:

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 for loops, if clauses, etc.: if you select an existing element such as the for loop we just created, pressing Enter will do two different things depending on whether the for loop has any content in it yet. On the one hand, if you just created the for loop and there's no actual code inside of the loop, Enter will add a new line inside the loop so you can start adding some; however, if there is already some code in there, Enter will step over the loop entirely and add a blank line outside of and after the for loop 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 else (use 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!)

Let's see if it all works! Init and Run your program, and you should see the correct values for input_unit and output_unit being displayed in your console -- something like this:

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:

  1. Add a variable pointing to the input data table to the program
  2. Erase any existing data at the start
  3. In the for loop we've already created, add a new row then...
  4. ...write the data for each trial to the new row (name, input, output), then...
  5. ...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 StdInputData 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 LocalVars, select Add Var and enter Name in the name field. Set the var type to String.
  • Since the value of the input_unit completely defines our trial types we can just use that for naming. From the Tools/Assign tab drag variable= element onto the Print statement at the end of the for loop block and set result var to Name. Then, enter input_unit in the expr field. Since input_unit is 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 expr field, however, replace "input_unit" with the following expression (exactly): Name.after("_") and click apply. The after() method available to instances of the String class (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 2 field to Name</code. This hit <code>AXTaskGen/Init and Run again.

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 vars section 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 Apply.

NOW we're actually ready to write out data:

  • Drag-and-drop the data=vars element (Tools/Data R/W) onto the for loop (Copy Into). This should appear directly after the Add New Row... statement.
  • In the green editor section at the top select the input_data variable we just created in the data var field, and then click the set_data flag 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_ROW and all matches false (unchecked).
  • Finally, select Name in the var 1 field. This will write the current value of the Name variable (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/Functions section and select the method() element. Drop it onto the for loop again and in the obj field that appears in the blue editing region at the top select input_data.
  • Then, in the method field, 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!
Figure 1: Final AXTaskGen program code.

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 SetMatrixFlatVal(,,,) line
  • 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.

Your full prog code section should now look like something like Figure 1 at the right.


Congratulations -- you're done!!! Select the StdInputData tab in the right Visualizer panel to view the grid view we created earlier. Then, Init and Run 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 Init, Run, Step, and 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 Init and 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

→ next CPTAX_Program