User Tools

Site Tools


dev:program_ulam

Programming with ULAM

If you don't have ULAM compiler, here is the portal to Install ULAM. If you already have ULAM, let's start programming. We can mkdir for our element repository. In the following examples, we use home/user/elements/ to store our source codes for elements. They are programmed and saved as .ulam files.

A Few Preliminaries

To help reduce confusion going forward, it's important to introduce a couple key terms and ideas of programming language ULAM.

Element vs Atom

In ULAM, as in many (but not all) object-oriented languages, there is a strong distinction between a description of a thing and an instance of that thing. In Java or C++, for example, the description of a thing is called a class while an instance of a thing is called an object. In ULAM, the description is called an element and the instance is called an atom: An atom is an instance of an element. If Foo is an element, we've got the language just right if we say something like “This function creates a new Foo atom to the west”, but we'd be misusing it if we said “This function creates a new Foo element to the west.

Capitalization Matters

In ULAM, we capitalize the first letter of all Type names. Type names include the names of the elements we write, such as the element First below, as well as 'primitive' types like Int, Bool, and Void.

Similarly, the first letter of every variable name, and of the name of every function or method (both terms mean the same thing in ULAM), must be a lowercase letter. So examples of legal variable or function names could include i, count, isInitted, or cMAX_VALUE, but not IsInitted, Reset, or MAX_VALUE.

EventWindow Indices

The image at right (click to enlarge) shows the indexing scheme used for sites within an EventWindow. Both 2D 'coord' numbering and a 1D 'site number' approach are offered.

The getCoord(siteNum) function converts an 1D index (which is an Unsigned(6) ) to a 2D coordinate (which is a quark C2D). The getSiteNumber(coord) convert C2D back to 1-d Unsigned(6). The quark C2D has getX() and getY() methods to access the x and y coordinate values individually.

In general, when code is referring to individual fixed sites within the event window, the 1D coordinate system is used (see the 'First' element below for an example). Two-dimensional C2D indexing is more commonly used when there is some flexibility depending on geometry or relative positions.

The 'First' Element

Our first ULAM element is literally named First, and is stored in a file called First.ulam:

First.ulam
element First{
  EventWindow ew;
  Void behave(){
    ew[1]=ew[0]; // Copy self one site west
  }
}

The syntax of an element is reminiscent of a class in Java or other object-oriented language. Inside the main {} brackets are a combination of data members and method declarations. In element First, we have one data member: ew, an instance of type EventWindow, and one method, behave, that takes no arguments and returns Void.

The function Void behave() is special because it is called automatically when it is time for a First atom to perform an event. An element is not required to provide a Void behave() method, but if it doesn't, an instance of that element will just sit passively and do nothing whenever its turn to have an event occurs.

An instance of EventWindow provides access to the neighborhood during an event. It has 41 cells. The event center, where an element of type First will be found when behave() begins, is ew[0]. ew[1] is the cell 1 step west of the center ew[0] (in the normal case), so the above code will duplicate the center atom along the x axis towards the west. This element behaves like an arrow in the MFM, creating a line of First atoms, growing to the west until it runs out of space.

Compile

Because we are now in our element directory, we must specify where the ULM compiler is located. (We could also adjust our PATH environment variable in any normal 'Unixy' way.)

/home/user/ULAM/ulam1/bin/ulam -l First.ulam

Running ulam with the -h switch will show all the switches for the ULAM compiler. The switch -l requests the creation of a dynamically-loadable library file from all the .ulam files on the command line. The resulting library file, called libcue.so, will be created in the /home/user/elements/.gen/bin/ folder.

Run

We also must specify where the simulator mfmsis located. The switch -ep tells mfms to load our ulam elements. The mfms -h can show all the switches for MFM.

/home/user/MFM/bin/mfms -ep /home/user/elements/.gen/bin/libcue.so

A quick tour of mfms simulator

On the left is a screenshot of the mfms simulator at startup . The simulator is an 160*96 canvas. The help menu will pop at startup unless We use mfms -n to switch it off. We can also use the h command to turn it on and off manually.

On the right, we use the t command to show the palette. The colors are elements. The pencil tool is selected by default. We can draw a dot or a line of Atoms of an element use the pencil tool. For short, we will use Atom instead of Atom of an element in the following tutorial. If we want to draw or delete atoms in a big area, we can use the brush or eraser tool. The rightmost button also allow us to change the size of our brush and eraser. We can use two colors at one time. In this screenshot, we can draw back Empty atom with left-clicks, and white Wall atom with the right-clicks.

Two other useful panels are logs and statistics and settings. They are brought up by l command and i command respectively. We can demonstrate the use of these two panels later in the tutorial.

experiment the First element

To place a First atom, select it from the palette and left-click on the canvas. To start or pause the simulation, use the SPACE command. The following screenshots shows the behavior of First. At 0.000 kAEPS there is only 1.0 First atom on the canvas. At 0.025 kAEPS, the First grows westward. There are 10 of them on the canvas. At 0.505 kAEPS, the First stops growing because they hit the west border. There are 67.4 atoms on the canvas. Why 0.4 atoms?

From the left to the right, these pictures shows a sequence of how First grows to the west.

The 'Second' Element

In ULAM, we use structured comments before elements, not just to document the code, but actually to affect the behavior of elements.

Second.ulam
/**
  The Second element demonstrates the power of structured comments 
  in ULAM.  Even though the code looks almost identical to First,
  its behavior is totally different!
 
  \color #2e2
  \symbol S1
  \symmetries all
*/
element Second{
  EventWindow ep;
  Void behave(){
    ep[1]=ep[0];
  }
}

The parameters in the structured comments can specify not only the color and/or symbol of the element, but also its “symmetries”. Symmetries determine the layout of the EventWindow coordinate axes during events, By default, the only allowed symmetry is “North goes up, East goes right”, but if we said \symmetries 90L (90 degrees left), then North would go left and East would go up.

But that's not all: We can specify multiple symmetries in the \symmetries declaration, in which case one of them will be picked at random before each event. Second is an example of the most extreme case, using all to enable all eight flips and rotations of the axes. With a random coordinate transformation on each event, the “west” neighbor might actually be any of the four sites immediately adjacent to the Second atom. Look what Second does:

Second grows and fills the entire canvas.

The 'Third' Element

We already know duplicate. Then we can swap.

Third.ulam
/**
  Third is a demo element.
  \color #f0f
  \symbol T1
  \symmetries normal
*/
element Third{
  EventWindow ep;
  Void behave(){
    ep.swap(0,1);
  }
}

The symmetries is normal. The element will swap itself with its west neighbor. So our Third element will fly along the x axis to the west.

The 'Flip' Element

This element Flip will shake back and forth 1 step along x axis.

Flip.ulam
/**
  Flip is a demo element.
  \color #606
  \symbol Fp
  \symmetries 0
*/
element Flip{
  Bool xFlip;
  EventWindow ew;
  Void behave(){
    if (xFlip==false){
        xFlip=true;
        ew.swap(0,4);
     }
    else{
        xFlip=false;
        ew.swap(0,1);
     }
  }
}

The xFlip is stored in the ew[0]. Make sure to change this field before swap. If we do swap first, this field will not store the value that we want it to save.

The 'Box' Element

This element Box will move in a loop of north, east, south, west. It seems like moving inside a box.

Box.ulam
/**
  Box is a demo element.
  \color #666
  \symbol Bx
  \symmetries normal

  \author Xinyu Chen
  \author Dave Ackley

  \license lgpl
*/
element Box{
  // Typedefs
  typedef EventWindow.Symmetry Symmetry;

  // Utilities
  DebugUtils du;
  EventWindow ew;

  // Data members
  Unsigned(2) ns;

  Void behave(){
    ew.changeSymmetry((Symmetry) (ns + 1));
    if(ns == ns.maxof) ns = ns.minof;
    else ++ns;
    ew.swap(0, 1);
  }
}

The ew.changeSymmetry() method accepts 0,1,2,3,4,5,6,7. We use 0,1,2,3 to let the EventWindow rotate 0,90,180,270 degree to the right hand direction(clockwise). In ULAM primitive types like Unsigned, we can specify any number of bits from 1 to 32. The type Unsigned(2) uses two bits and has possible values 0,1,2,3. So by giving our ns data member the type Unsigned(2) we use as few bits as possible while providing four possible states.

Interaction between three Elements

We have tried several elements above. Here we implemented a game involves three elements: Provider, Signal and Request. The Requests diffuse around. Whenever they see Providers, they disappear. The Providers send out Signals to attract Requests. A Signal can catch a Request by a gradient check rule. The gradient here refers to the number of Signals observed by Signals and by Requests respectively.

Provider.ulam
/**
  Provider is a demo element.
  \color #fba
  \symbol Pv
  \symmetries normal
*/
element Provider{
  DebugUtils du;
  EventWindow ew;
  AtomUtils au;
  Random rm;

  Void behave(){
    WindowServices ws;
    ws.reset(1,1);
    Empty e;
    Signal s;
    Int et=au.getType((Atom) e);
    if(ws.scan(et)){ //check if there is empty slot to produce Signal atoms 
      if(rm.oneIn(3)){
        ew[ws.getPick(0)]=s;//pick a random site to send out a Signal
      }
    }
  }
}
Signal.ulam
/**
  Signal is a demo element.
  \color #ac3
  \symbol Sg
  \symmetries all
*/
element Signal{
  DebugUtils du;
  EventWindow ew;
  AtomUtils au;
  Random rm;
  Unsigned(6) density;
  Void diffuse(){
    if(ew[1] is Empty){
      ew.swap(0,1);
    }
  }
  Bool chkDense(){
    WindowServices ws;
    ws.reset(0,4);
    Signal s;
    Int st=au.getType((Atom) s);
    if(ws.scan(st)){
      density=ws.getHits(0);
      if(density<=1){
        return false;
      }
    }
    return true;
  }
  Void catchRequest(){
    WindowServices ws;
    Random rdm;
    ws.reset(1,4);
    Request r;
    Int rt=au.getType((Atom) r);
    for(Int slot=ws.next();slot>=0;slot=ws.next()){
      Atom a= ew[slot];
      if(a is Request){
        Request you=(Request) a;
        if(density>you.density){
          if(rdm.oneIn(2)){
            ew.swap(0,slot);
          }
        }
      }
    }
  }
  Void behave(){
    Bool isAlive=true;
    if(chkDense()){// if alone, I can walk around. I store the number of Signals that I observed. 
      diffuse();
    }
    else{          // if I am alone, I have some chance to disappear.
      if(rm.oneIn(density*5-4)){
        Empty e;
        ew[0]=e;
        isAlive=false;
      }
    }
    if(isAlive){
      catchRequest(); // if not disappear, I will swap position with some Request who observe  
                      // less Signal than me.
    }
  }
}
Request.ulam
/**
  Request is a demo element.
  \color #740
  \symbol Rq
  \symmetries all
*/
element Request{
  DebugUtils du;
  EventWindow ew;
  AtomUtils au;
  Random rm;
  Unsigned(5) density;
  Unsigned(5) pvdrcnt;
  Void diffuse(){
    if(ew[1] is Empty){
      ew.swap(0,1);
    }
  }
  Void chkDense(){
    WindowServices ws;
    ws.reset(0,4);
    Signal s;
    Provider p;
    Int st =au.getType((Atom) s);
    Int pt =au.getType((Atom) p);
    if(ws.scan(st,pt)){  // observe either Providers or Signals around me.
      density=ws.getHits(0);
      pvdrcnt=ws.getHits(1);
      if (pvdrcnt>0){    // if Provider exists, I will disappear.
        Empty e;
        ew[0]=e;
      }else{
          diffuse();     // if no Provider, I walk away and store the number of Signals I observed.
      }
    }
    else diffuse();      // if neither Provider or Signal appears, I walk away.
  }

  Void behave(){
    chkDense();
  }
}

Node Elements

We have some cool features in the new MFM simulator. One of them is that an Atom can change its color dynamically. We made this little Node element to simulate a neuron node. When a Node is triggered by its left neighbors within its EventWindow, it fires. We let a Node to display various colors as long as it is trigger. If it doesn't fire, it remains dark.

Node.ulam
/**
  Node is a demo element.
  \color #986
  \symbol Nd
  \symmetries normal
*/
element Node{
  typedef Unsigned(6) SiteNum;
  typedef Int(16) Coord;
  typedef Unary(1) FireFlag;
  typedef Int(4) Weight;
  typedef Unsigned(8) ARGB[4];
  typedef Unsigned(8) ColorValue;

  DebugUtils du;
  EventWindow ew;
  Once oc;
  AtomUtils au;
  Weight wa;
  Weight wm;
  Weight wb;
  FireFlag fire;
  Weight thresh;
  ColorValue red;
  ColorValue grn;
  ColorValue blu;

  Int getSum(){
    Int sum;
    C2D mcd;
    WindowServices ws;
    ws.reset(1,4);
    for(Int slot=ws.next();slot>=0;slot=ws.next()){
      if(ew[slot] is Node){
	Atom a=ew[slot];
	Node you=(Node) a;
	mcd=ew.getCoord((SiteNum)slot);
	Coord mx=mcd.getX();
	Coord my=mcd.getY();
	if(mx<0){
	  if(my<0){
	    sum=sum+wa*you.fire;
	  }else if(my==0){
	    sum=sum+wm*you.fire;
	  }else if(my>0){
	    sum=sum+wb*you.fire;
	  }
	}
      }
    }
    return sum;
  }
  FireFlag getFire(Int s){
    if(s>=thresh){
      return 1;
    }
    return 0;
  }
  Void initSelf(){
    Random rd;
    Weight w;
    wa=(Weight)rd.between((Int)w.minof,(Int)w.maxof);
    wm=(Weight)rd.between((Int)w.minof,(Int)w.maxof);
    wb=(Weight)rd.between((Int)w.minof,(Int)w.maxof);
    thresh=(Weight)rd.between((Int)w.minof,(Int)w.maxof);
  }
  Void changeColor(){
    Random rd;
    red=rd.between(0,3)*80+rd.between(0,16);
    grn=rd.between(0,3)*80+rd.between(0,16);
    blu=rd.between(0,3)*80+rd.between(0,16);
  }
  ARGB getColor(Unsigned selector){
    ColorUtils cu;
    ARGB ret=cu.color(0xff00ff00u);
    if(fire==1){
      ret[1]=red;
      ret[2]=grn;
      ret[3]=blu;
    }else{
      ret=cu.color(0xff090806u);
    }
    return ret;
  }
  Void behave(){
    Int msum;
    if(oc.new()){
      initSelf();
      du.printContext();
    }
    msum=getSum();
    fire=getFire(msum);
    changeColor();
  }
}

We use a Once quark to call the initSelf() once. We have used a lot of quarks before without realizing that. The EventWindow, DebugUtils, Random, AtomUtils are all quarks. It's the same thing here to use a Once: instantiate Once oc, then use oc.new() to decide if this is the first time of a call.

In the changeColor() of this Node element, we use ARGB getColor() to give new color values. This getColor() returns a array with Four values: Opacity, Red, Green and Blue.

Like other Neurons, our Node will be fired by neighbors. In this simplified case, only the left neighbors can trigger our Nodes. This time we need to scan the whole EventWindow. To decide which neighbors are located to the left we use C2D coordinate. Those Atoms with negative x coordinate are to the left of our Node.

dev/program_ulam.txt · Last modified: 2016/01/14 07:13 by ackley