This chapter describes how to use the Shape language and the LatFunction module to create prototypes of new modules very quickly without having to use the Module Builder. It describes the Shape language syntax in detail.
Each module in an Explorer map performs a well-defined, often complicated, function. On occasion, you will find that no module exists for the lattice-based operation you want to perform. You can place a LatFunction module in your map, then read in a program written in LatFunction's Shape language, to perform this operation. The module's interpreter carries out the actions you specify.
The LatFunction module is an interpreter for a lattice manipulation language that makes it easy to operate on lattices. It lets you interactively change the way LatFunction acts on the data it receives, which provides a quick way to prototype a new module without going through the Module Builder or writing C or Fortran code. It also gives you programmatic control beyond the range you would have with dials and sliders, and it eliminates the need to compile, link, and install a module each time you modify it.
As a language interpreter designed to work with lattices and arrays, the LatFunction module runs programs written in Shape and fed to it from LatFunction's Program File or Program Text input ports. Shape's syntax is similar to the C programming language. Its main advantages are that it allows you to work directly with arrays and to change programs dynamically.
LatFunction is structured like other modules except that it accepts only lattice and parameter data. Its ports are wired in the same way as other module ports, and LatFunction makes data on those ports available to the Shape language interpreter as internal variables. You can:
Perform simple arithmetic on lattices, much as the P-Func Editor lets you perform arithmetic on parameters.
Test an idea for a full-blown module by creating a prototype of it.
Change your prototype interactively and view its computational results without building or loading a new module.
Mimic the effects of several other modules in one to obtain a general idea of the results of the combination.
LatFunction has two forms. The basic LatFunction module has a fixed set of ports, uses fixed variable names for its input and output data, and does not accept parameters. The second form of LatFunction is analogous to the DataScribe. You create your own module resources to describe input ports, output ports, and control panel, and you write a Shape program. The generic LatFunction executable then executes in the guise of your module, according to its Shape program. This is called a LatFunction-based module. The main differences between the two are the port names and control panels. Both interpret the Shape language identically.
The basic LatFunction module (see Figure 1-1) has three input ports, two parameters, and a lattice that controls its program execution, plus data ports.
You can either enter programs in the Program Text input port or load them from a file by typing a filename into the Program File file selector.
LatFunction has fixed names for its data ports. On input, data ports are First In, Second In, and Third In. On output, they are First Out, Second Out, and Third Out.
A LatFunction-based module can accept any number of input parameters and lattices and can output any number of lattices. It does not output parameters. The input ports are shown in Figure 1-1.
The LatFunction-based module uses the name of an input port, such as Pressure, to create an internal variable with the same name, Pressure, in the Shape language. If the input port contains a non-alphanumeric character (a space is considered a non-alphanumeric character), the LatFunction-based module turns that character into an underscore. For instance, an input port called Temperature Array is assigned the internal variable name Temperature_Array.
A LatFunction-based module creates internal array variables only for those input ports that have data. It does not create internal variable names for ports that do not contain data. The size and shape of the internal array depend on the port data type, which can be either parameter or lattice.
Parameters on input ports can be of two types:
A scalar, such as the value of a slider widget, is sent to Shape as a scalar. A character string is sent to Shape as a 1D array of integers representing the character string in the ASCII collating sequence.
A LatFunction-based module sends a lattice to Shape in each of three forms: the data array, the coordinate array, and the combined lattice. The variable name indicates the form: the suffixes _lat and _coord signify the combined lattice and the coordinate array, respectively. For instance, a Pressure port yields data Pressure coordinates Pressure_coord, and lattice Pressure_lat.
The Shape ports themselves have names derived from the Explorer ports. Input ports take the prefix iport_ followed by the Explorer port name; output ports use the prefix oport_. For instance, the port names iport_Pressure and oport_Temperature_Array correspond to the example in the previous paragraphs.
A LatFunction-based module assumes it must produce data for all of its output ports. Thus, for each output port name, the LatFunction-based module uses the internal variable of the same name to create an output lattice.
Name mapping differs for input and output ports. For input ports, only the data goes into the variable; for output ports, the variable contains data and may also contain coordinates.
A LatFunction-based module distinguishes the different output possibilities as follows:
If a given variable is a single array, Shape assumes this array contains data and appends default, uniform coordinates to it. (See the IRIS Explorer Reference Pages for a description of cxCoordDefaultNew, which creates these coordinates.)
If the variable is a full lattice (containing two arrays), Shape uses those two arrays for coordinates and data in the output lattice. For instance, in the case of the Pressure input port, the LatFunction-based module creates the internal variable Pressure to contain the data array, and Pressure_coord to hold the location coordinates.
Thus, Pressure_lat can be seen as a list of two arrays, one that contains data and one that contains location coordinates. If the output array Temperature has two arrays, they are assumed to be coordinates and data (see Figure 1-2).
To get coordinates from input to output ports, you must build them into the Shape internal variable by using make_lat(coordinates, data). The order of the arguments is significant: coordinates come first.
For example, suppose a reactor vessel has radially symmetrical pressure coordinates with pressure data at the nodes. To compute the gradient of the pressure using LatFunction, use the following procedure:
Create the module resources for a LatFunction-based module with an input lattice port named Pressure and an output port named Pressure Gradient (see “Building a LatFunction-based Module”).
Create a gradient program in Shape, using Pressure and Pressure_coord to compute a gradient field called Field.
Use make_lat to compose the gradient lattice (the define operator := is described in “Shape Language Particulars”):
Pressure_Gradient := make_lat(Pressure_coord, Field); |
When the LatFunction script finishes, the output port contains the pressure gradient based on the same coordinate system as the input pressure. An output port can use output_data_flush to send multiple data sets downstream during one module firing.
The LatFunction module takes lattices as its inputs and produces lattice as its outputs, but Shape operates only on arrays. Shape cannot operate directly on lattices because each lattice contains two arrays, one for the data and one for its location coordinates, thus requiring each lattice to be split into its component parts.
Lattices can have coordinates of three types: uniform, curvilinear, and perimeter, all three of which are acceptable in Shape. For uniform and curvilinear lattice coordinates, the coordinate array passed through to Shape consists of the floating point coordinates of the lattice in whatever type was present on input. This array looks exactly like the values array of the comparable lattice coordinates structure.
For example, the program to scale an array of data by 3.0 while maintaining coordinate positions is:
First_Out := make_lat(First_In_coord, 3.0* First_In); |
The program to scale a lattice's coordinate positions by 3.0 while maintaining data values is:
First_Out := make_lat(3.0* First_In_coord, First_In); |
![]() | Note: This program works only for uniform and curvilinear coordinates. Perimeter coordinates are handled separately (see “List Operators”). |
You can give your LatFunction-based module any control panel you want, with any widgets and any port names. However, every LatFunction module must have a widget named “Program File”. This widget can be either a File Browser or a Text Type-in widget but it must deliver a text string. Your module may also have a radio button widget named “Debug?” This widget generates debugging printouts when its value is non-zero.
To build a custom LatFunction-based module, follow these steps:
Use the Module Builder to construct a module with the desired input and output port names and control panel (see Chapter 2).
In the “Program File” type-in, save the name of the file that will contain the Shape program for this module.
Click on the Yes radio button for “Alternate Executable” on the Build Options panel and type in LatFunction. This tells Explorer to look for the executable under LatFunction instead of under the name of the module you have just built.
Save the module resources, and build and install the module.
Write the Shape program, saving it under the same filename as you gave the module in Step 1.
You can now load the module into an Explorer map.
If you bring up an Explorer map with a LatFunction-based module in it, you can now change that module's execution simply by changing the contents of the program file. Pressing the <Enter> key with the cursor in the “Program File” type-in forces the LatFunction module to reread and interpret that file. This, in turn, forces the module to refire based on a new program.
![]() | Note: A refiring caused by the module receiving a different value from a control panel widget does not cause the program to be reinterpreted. |
Think of a channel selector that takes a three-color RGB (red, green, blue) image as input. Based on your selection, it sends either red, green, or blue downstream. You can change the selected channel on a control panel but, by modifying the program, you can also change the representation of output data. It is clear that prototyping on the fly has definite advantages when compared to the more laborious process of creating a module from scratch.
For instance, the following channel selector has three input ports:
| Multi Channels | Lattice data | |
| Selection | Parameter channel | |
| Program File | Parameter type-in with program file name |
and produces one lattice as output:
| Channel Out | Lattice data out |
Its action is to select the red, green, or blue channel of the input data (Multi_Channels), depending on the value of Selection, and output a gray-scale image on the output port.
The Selection parameter can be any widget that produces an integer between 0 and 2, inclusive, including a dial, slider, option menu, radio box, or even a type-in. Values below 0 and above the number of channels of the input are clipped to the valid range. The program creates a single-channel output. Commented out at the end of the program is another way of creating the output. It creates a multi-channel output with zeros in the unselected channels for a colored, rather than a gray-scale, effect.
// Take an input channel indicator and a multi-D lattice with // nDataVar and select one of the data variables. // Protect against out-of-range inputs on Selection. // Input ports: // Multi_Channels Lattice data // Selection Parameter channel // // Output ports: // Channel_Out chan = max(0,min(Selection,nChan -1)); // Allocate a single channel byte output of the right shape. int outsz[rank] = sz; outsz[rank-1] = 1; Channel_Out := allocate ((byte)1, outsz, fill_zero); // Copy the selectd channel to the output array. Channel_Out = inside(Multi_Channels[chan]); |
![]() | Note: Leading // (double slash) characters indicate a Shape comment line. |
The instructions for working with input data are given to LatFunction in the form of a Shape program.
As an array-oriented language with a C-like syntax, Shape has many similarities to C. Shape includes all of C's math library functions (see Table 1-2), as well as some other common operations. Like C, Shape function names and variable names are case-sensitive, and Shape has zero-based indexing. Shape extends the power of C by operating on arrays rather than on single scalar values.
For instance, if lattice input ports First In and Second In are of the same size and shape, you can add them to produce a lattice output port, First Out, with
First_Out:= First_In + Second_In; |
![]() | Note: Using the define operator := is analogous to assigning a pointer to a structure or to an array in C. Here it makes First_Out a reference to the temporary array created by the addition of First_In and Second_In. |
To try this and the next example, wire the output of GenLat to ports “First In” and “Second In” of LatFunction and wire LatFunction's “First Out” port to IsosurfaceLat and then to Render.
Multiplication is:
First_Out:= First_In * Second_In; |
You can compile the array of maximums with the max function, a Shape function analogous to the Fortran function max:
First_Out:= max(First_In, Second_In); |
You can produce a double precision output lattice with:
First_Out:= (double) First_In; |
Most other C arithmetic and logical operations work analogously on Shape arrays. Try:
First_Out:= sin(First_In); |
Shape differs from C in several important ways, including the use of the define operator (:=), and the way it manipulates lattices.
In Shape, lattices are first class values. They behave much like arrays in C or Fortran, and they also resemble arrays in Fortran 90. For example, in this and the following sections, you can wire LatFunction's First Out port to PrintLat, then type:
First_Out:=<varname> |
whenever you want to print out the variable varname at the end of a computation. The examples show the Shape syntax of the computed result with an arrow symbol (for instance, ==> [[1,2],[3,4],[5,6]]). They also show Shape arrays printed in a more conventional array layout, such as:

The array [[[1,2], [3,4]], [[5,6], [7,8]]] could be displayed equivalently as

For example, in this fragment, the vectors u, v, and r are declared and allocated, then individual values are assigned as in C. Finally, the sum of u and v is copied (with the = operator) to r.
int u[3]; int v[3]; int r[3]; u[0] = 1; u[1] = 5; u[2] = 10; v[0] = 2; v[1] = 3; v[2] = -3; r = u + v; r; ==>[3, 8, 7] |
A Shape array is the same as the data array of an Explorer lattice. Its dimensions are identical to those of the Explorer lattice, with nDataVar treated as the fastest varying dimension. nDataVar defines the number of data variables per node in a lattice. For a 2D lattice of three data variables, with dimensions 10 x 20, the shape of the Shape array is [20, 10, 3].
All nD lattices in Explorer are (n+1)D Shape arrays by default, with the nDataVar size taken as the extent of the fastest varying, n+1, dimension.
The rank of a lattice is the number of dimensions it has. The shape of a lattice is the vector whose entries are the lengths in each dimension. For example, [1, 2, 3] has rank 1 and shape [3], and [[1, 2], [3, 4], [5, 6]] has rank 2 and shape [3, 2]. Rank 1 arrays are often called vectors and rank 2 arrays are often called matrices. An array with rank 0 is a scalar.
Arithmetic with arrays is almost always done element-wise, as follows:
u := [1, 2, 3]; v := [4, 5, 6]; u * v; ==>[4, 10, 18] |
Arrays are automatically promoted to higher rank as required. Promotion entails augmenting the rank of an array and copying array values to fill in the new array elements. Lattices promote on the right, in the most rapidly varying dimension. That is, arrays with the following shapes are compatible with respect to promotion: [5, 4, 3], [5, 4], and [5]. Data values are copied to consecutive array locations during promotion.
For instance, given u from the above example:
u + 2; ==>[3, 4, 5] m := [[1, 2], [3, 4]]; m * [3, 4]; ==>[[3,6],[12,16]] |

![]() | Note: The fragment above is not matrix-vector multiplication |
m + 1; ==>[[2,3],[4,5]] |

m = [1, 2]; ==>[[1,1],[2,2]] |

You can change the direction in which lattices promote: refer to the Inside, Outside, and InsideOut functions listed under “Miscellaneous” in Table 1-3 as well as in “Promotion of Arrays” and “Promotion of Arrays” .
It is an error to operate on arrays of differing lengths in any dimension. For example, [1, 2, 3] + [3, 4] is an error.
The define operator, denoted var:= value, binds a variable without requiring that the variable be previously defined. Define is not present in C, but it provides functionality similar to that of C pointers.
The := operator, unlike =, copies no data. Instead, the value is simply given a new alias, or name. The define operator is important because:
Define lets you name only a part of an object. For example, take a five-element vector L (a 1D lattice) with the values 0, 1, 2, 3, 4 stored in it (such as L := [0, 1, 2, 3, 4]), in which:
a = L[1] copies an element of L into a a := L[1] defines a by reference to part of L |
To show the effect of these definitions, set a to 5 after each, then print L. In the first case, the result is:
a = L[1]; a = 5; // L is [0, 1, 2, 3, 4] |
But, in the second case, the result is:
a := L[1]; a = 5; // L is [0, 5, 2, 3, 4] |
Define (:=) is more efficient than copy (=). Define attaches a name to a piece of data rather than moving data to the name. For a single integer, the difference does not matter, but it is significant if the data is a large, multidimensional array.
For example, you could make the statement
a:= read(iport_Pressure). |
The data from the input port may be a whole lattice, and simply labelling the lattice instead of moving each piece of the lattice to the label is less work.
Define obviates the need for some variable declarations. It is not necessary to preallocate or type the defined variable in Shape, since the alias picks up the type and size of its source.
Define lets you refer to a regularly-strided portion of a larger array (see “Subshape.”)
Define is commonly used to change a small part of a lattice. For example, after:
int m[10, 10]; // a 10 by 10 array d := diagonal(m); // point to diagonal of m d += 1; // increment the diagonal of m |
m is now a 10 x 10 identity matrix. In the first line, m is declared to be a 10 x 10 matrix and is initialized with zeros. In the second line, the vector d is defined as the diagonal of the matrix. Finally, the scalar 1 is promoted to a vector and added to each entry of d. Since d is an alias for part of m (the diagonal), the result is that 1 has been added to each entry of the main diagonal of m.
Arrays promote on assignment and during binary operations. Arrays are promoted by padding the shape on the right, that is, in the fastest varying dimension. The promotion iterates each element of the source until the newly padded dimensions have been filled with the value, then moves on to the next source value.
int b[2,3,4]; // declare a 3D array of zeros
a := [[1,2,3],[4,5,6]]; // define a 2D array
b = a;
==> [[[1,1,1,1],[2,2,2,2],[3,3,3,3]],
[[4,4,4,4],[5,5,5,5],[6,6,6,6]]]
|

b + a;
==> [[[2,2,2,2],[4,4,4,4],[6,6,6,6]],
[[8,8,8,8],[10,10,10,10],[12,12,12,12]]]
|

Arrays are promoted by compressing the shape on the right, that is, in the fastest varying dimension. The reduction selects the first element of the source array in all of the newly reduced dimensions on assignment. On updating assignment (for example, +=) the dimensions are compressed by the reduction operator.
Reduction assignment by addition adds the data of the extra dimensions together. Using the previous values of a and b gives:
a := (long) a; ==>a a += ==> [[5,10,15],[20,25,30]] |

Assignment reduction looks like a sum over i of inside(A = B[i]) on the correct number of dimensions. The following examples show a few updating operator reductions:
a = ==> [[1,2,3],[4,5,6]] |

long s; ==>s s += b; ==>84 // ... bitwise conjunction of an array, ... long s = 1; ==>1 s &= b; ==>0 long s = 1; ==>1 s &= [1, 1, 1, 1]; ==>1 // ... logical conjunction of an array, ... long s = 1; ==>1 s &= (b>0); ==>1 |
The update operators, such as += and &=, behave somewhat differently in Shape. For example, take the expression x += y.
If x has lower rank than y, then y is reduced into x, as follows:
int x; y := [1, 2, 3]; x += y; ==>6 |
![]() | Note: The fragment above assigns to x the sum of the entries of y. |
int v[3] = 1; m := [[1, 2], [3, 4], [5, 6]]; v *= m; ==>[2, 12, 30] // the products of each row of m |
Reduction occurs along the same dimension as promotions, on the right of the array's dimension vector.
The base data types in Shape differ from those in C. Shape lacks char and all the C-type modifiers, such as extern, static, unsigned, signed, short, and long, but it has the additional data types byte, complex and double complex.
All the base data types in Shape match the Explorer data types, except the two complex types. The complex numbers consist of a pair of floats, and the double complex numbers consist of a pair of doubles. Complex numbers must be read into Explorer as floats or doubles, converted in Shape, and read out again as floats or doubles. compares the data types in the two languages.
Table 10-1. Data Types in C and Shape
Language | Data Types |
|---|---|
C | char, short, int[a], long, float, double |
Shape | byte, short, int, long, float, double, complex, double complex |
[a] In Shape, int is equivalent to long. | |
Functions are declared differently in Shape than in C. For example, the C-style declaration
float f(float x, int y) {...; return z;}
|
is expressed in Shape as
function f(x, y) {...; return z;}.
|
To summarize:
Functions and their arguments are not typed.
They start with the keyword “function”.
The keyword return signals the value to be returned from the function. In the absence of a return statement, the last computed value in the function is returned.
Table 1-3 lists the built-in functions available in Shape.
Shape's loop syntax is fundamentally identical to that of C. The following examples illustrate correct usage.
Shape's For loop construction is identical to that of C. The three expressions in parentheses following For give the initial settings, test logic, and loop counter changing, respectively. The code block for the loop can be one statement or a sequence of statements; if the block is a sequence of statements, it is enclosed in curly braces. For instance:
int i;
int a[10];
for (i=0; i< 10; i += 1)
a[i] = sin(i); // single statement -- no curly braces
|
![]() | Caution: Because the increment operator ++ is not yet implemented, use the construction i += 1 instead of i ++ in loop iterations, as shown above. Using the ++ operator will produce a “Not Yet” error message. |
Shape's While loop construction is identical to that of C. The loop iterates while the conditional expression evaluates to true, or non-zero. For instance, the For loop of the previous example could be rewritten as:
int i;
int a[10];
i = 0
while (i < 10) {
a[i] = sin(i);
i += 1;
}
|
Shape's continue and break statements are identical to those of C. The continue statement causes execution in a For or While loop to branch to the next iteration of the loop. The break statement causes execution in a loop to branch out of the loop to the next higher loop level or to exit from a singly-nested loop.
Shape has the same concept as C of automatic variables, which are freed when the scope of their declaration is exited. This is useful for memory allocation, since it relieves you of having to worry about freeing variables. However, it can be inconvenient at times, particularly when you want to use a conditional assignment to an array of a variable size. For instance, the following program fragment is in error:
if (n==1) {
array := index_fill([10,1]);
}
else if (n==2){
array := index_fill([20,20,2]);
}
Out_Lattice := array
|
This Shape program fails to set Out_Lattice because array is freed when its scope (an if or else clause) is exited. You can circumvent this problem by creating a function that returns the array, as follows:
function ret_array(n)
{
if (n==1) {
return index_fill([10,1]);
}
else if (n==2) {
return index_fill([20,20,2]);
}
}
Out_Lattice := ret_array(n);
|
Shape offers some features that are not available in C. These include:
See Table 1-2 and Table 1-3 in “Shape Function Tables” for lists of Shape functions.
The copy function implements a copy on write (COW) functionality for its single argument. Its action lies between that of define (:=) and assign (=) in that it creates a pointer to its argument only if no writes are done to that pointer. However, when the first write to the returned pointer occurs, the copy function copies the data values of the source. This late binding of copying is efficient for routines that access input port data; it prevents Shape programs from writing over input data, but delays making a copy until a write is attempted.
During the read call on an input port, for instance, this looks like:
val := read(iport_Pressure);
slice := val[0]; // uses only the original data
val[0] = slice + 1 // makes a copy of the data before storing
// the incremented values in val
|
Shape's scatter and gather functions extract data from one array and copy it to another array. The extracted data need not be arranged in a regular pattern, as is required of subshape, below. However, the values are copied rather than simply assigned a pointer, as they would be with subshape. Thus, scatter and gather are more expensive than subshape.
scatter takes three arguments: the destination (dest), index, and source arrays. scatter's functionality implements the mathematical operation:
dest (index) = source |
which copies each value from source into the dest position specified by index. Source and dest must have the same type.
The shapes of index, source, and dest must conform, with the last dimension of index equalling the rank of dest and the remaining dimensions of index equalling their respective dimensions in source. For instance, if source is 2 x 3 and dest is 4 x 1 x 6, a valid index would have shape 2 x 3 x 3 and might be [[0,0,0],[0,0,2],[0,0,4]],[[2,0,0],[2,0,1],[2,0,2]]].

With a source of [[1,2,3],[4,5,6]], the dest would be [[[1,x,2,x,3,x]],[[x,x,x,x,x,x]],[[4,5,6,x,x,x]],[[x,x,x,x,x,x]]]

where x denotes the previous value from dest.
gather is a function that returns a gathered array, with two arguments, index and source, interpreted according to the mathematical operation source(index).
gather returns the array of selected values in the same shape as index.
The shape function returns a dimension vector giving the shape of its input argument. For instance, a 128 x 256 lattice with three channels of data (dims = (128,256), nDataVar = 3) would yield an array of shape [256,128,3].
The shape function is commonly used to discover the rank of an array. An array's rank is its number of dimensions—essentially, the shape of the shape. The correct syntax is:
int rank = shape(shape(val))[0]; |
Writing only
int rank = shape(shape(val)); |
would fail, because the returned shape would be a vector of length 1 rather than a scalar rank. The zero-ordered slice extracts the scalar from its vector position.
The allocate function allocates an array of the given shape and type (such as long or float, as given in the example below) and fills it with values according to the fill_type. The basic syntax is:
allocate(base_type_example, shape, fill_type) |
| fill_none | leave uninitialized | ||||
| fill_zero | fill with zeros | ||||
| fill_index | fill with uniform coordinates (with integer indices) | ||||
| fill_NaN | fill with IEEE NaN symbols
![]()
![]() |
The type of the first argument is used to fix the type of the allocated array. You may use a known array or value, use a casted value, such as (byte)1, or use an explicitly typed number, such as 1L for long or 1.0L for double.
Slice extracts a multidimensional slice from an array. It has two syntactical forms. The first one is x[y], where y is a scalar that selects the yth subarray of x, exactly as in C. When you use this form, the sliced array has rank one less than the source. For example, take a 2D array:
A = [[1,2,3], [2,4,6], [4,8,12], [8,16,24]]; |
Then A[0] = [1, 2, 3], a three-length vector derived from A in the ordinary C manner.
The second form of slice is A[y:z], where y and z are scalars. In this form an array section, or subarray, of A is extracted. The subarray has the same rank as A, but contains only slices x through y. For example, given A, the statement A[0:1] extracts two vectors from the original array, and the shape of the array is 2 x 3. The rank is 2.
In C and Shape, you can write A[0][0] to get the first element of A. In Shape, however, A[0:2][0:1] is different from A[0:2, 0:1]. The respective shapes are 2 x 3 and 3 x 2, because A[0:2][0:1] is the same as A[0:1].
A[0:2][0:1]; ==> [[1,2,3],[2,4,6]] |

The subshape function takes three or four arguments and returns a reference to a subarray (or subsection) of its input array. It does not copy any data. The syntax is:
subshape (start, end, source) |
or
subshape (start, end, stride, source) |
where start, end, and stride are vectors of size shape(source) that signal the indices of the beginning and end of the array subsection and the strides between successive elements in each dimension. The default stride is 1. For example, with a 10 x 10 source, you can extract the central 2 x 2 square with subshape([4,4],[5,5], source)and extract every other row with subshape([0,0],[10,11],[1,2], source).
An ending index greater than the size of source is legal only if the actual indices used in the computation do not exceed the array bounds. In this example, rows 0, 2, 4, 6, 8, 10 would be extracted, but row 11 would not.
Subshape does not copy data; it returns a reference to the selected subarray.
The stacking operator concatenates arrays of compatible shape to yield a new array of rank one higher. The stacking syntax is:
[expr, expr, ...] |
The shape of the stacked array is equal to the shape of the stacked components, with an added component in the slowest varying dimension. That component is the number of stacked arrays. For instance, let:
a:= [1,2,3]; b:= [4,5,6]; c:= [a,b]; ==> [[1,2,3],[4,5,6]] |
In this case, both a and b have shape [3], while c has shape [2,3]. You can change the stacking dimension with inside.
The send_indices function takes an index permutation vector and a lattice. The permutation controls where each coordinate ends up. As a special case, if permutation indices agree, then a diagonal is taken in those coordinates. For example:
send_indices([1, 0], [[1, 2], [3, 4]]); ==> [[1,3],[2,4]] |

The above example is a transpose of the 2 x 2 matrix because dimensions 0 and 1 were swapped by the [1,0] index vector.
send_indices([0, 0], [[1, 2], [3, 4]]); ==> [1,4] |

The above example is a diagonal of the 2 x 2 matrix because dimensions 0 and 1 were simultaneously selected by the [0,0] index vector.
Min and max take one or more arguments. With two or more, min does an element-wise minimum over all its arguments. With one argument, it performs a reduction by one coordinate. For example, the first statement takes the pointwise minimum of three vectors, while the second finds the minimum element of one vector:
min([1, 2, 3], [4, -2, 6], [7, 8, 9]); ==> [1, -2, 3] min([1, 2, -2, 3]); ==> -2 |
The dotproduct operator +.* may be used to form the dot product between vectors and/or arrays. This is matrix or tensor multiplication.
[1, 2, 3] +.* [4, 5, 0]; ==> 14 [[1, 2], [3, 4]] +.* [5, 6]; ==> [17, 39] [5, 6] +.* [[1, 2], [3, 4]]; ==> [23, 34] [[1, 2], [3, 4]] +.*[[5, 9], [10, 13]]; |

The form A+.*B is identical to the function dot_product(A, B). dot_product has an optional third argument, which directs some number of leading coordinates not to be involved in the product, but, rather, to indicate that several matrix multiplications of lower rank are to be performed. For instance, if A and B are [3, 3, 3] dimensional arrays, then dot_product(A, B) has shape [3, 3, 3, 3], while dot_product(A, B, 1) has shape [3, 3, 3]. The former is a single, 3D tensor product, while the latter consists of three 2D matrix products.
Bitproduct resembles as dotproduct, except that it substitutes bit-wise “or” and “and” functions for addition and multiplication. The bitproduct operator is |.&.
The conditional evaluation operator ?: is similar to that of C. When working with scalar values, its operation is identical to that of C, so that
x ? a : b |
evaluates to a if x is true (or non-zero) and to b if x is false (or zero). The operator goes beyond the C interpretation in that it can accept arrays as well. In this case, the arrays x, a, and b must be compatibly shaped. For instance, all three could have the same shape, or either a or b could be a scalar.
The array operation of x ? a : b creates an array the size of x but with values chosen from a or b, depending on the values of x. Scalars a and b are promoted to constant-filled arrays the shape of x, but no additional storage is allocated.
The inside, outside and insideout forms are used.
The argument to the inside form is evaluated with different indexing rules than normal. The four array operations — indexing, concatenation (stacking), promotion, and reduction — start from the opposite end of the shape vector. For instance, A[0] selects the first slice of A in the slowest varying dimension, while inside(A[0]) selects the first slice of A in the fastest varying dimension. The former is a blocked selection, while the latter is an interleaved selection. For example:
v := [1, 2]; w := [3, 4]; [v,w]; ==> [[1,2],[3,4]] |

inside([v, w]); ==> [[1,3],[2,4]] |

m := [[1, 2], [3, 4]]; inside(m[0]); ==> [1, 3] v := [0, 0]; inside(v += m); ==> [4, 6] inside(m + [5, 6]); ==> [[6,7],[8,9]] |

Shape provides some input and output capabilities in addition to creating array variables from input ports automatically and sending data out on output ports. For instance, it lets you flush multiple arrays to output lattices during one computation, and you can also write files.
Before writing a file, you must open it. The file opening commands return a port identifier as a Shape variable. For instance,
globe := open_output_file (“earthData.rgb”); |
would open an output file.
You can write an array data to a port identified by portID using the write command:
write(data, portID); |
If you want this data to enter the Explorer map immediately, rather than waiting until module firing finishes, you can use the flush_output_port command, as in
flush_output_port(portID); |
In the absence of a flush, the lattice propagates to the map after the LatFunction module finishes firing.
You can also write an array to standard output, which can be useful for debugging, with
write(data); |
Also useful for debugging is the dump command, which prints the shape, type, contents, and other information for an array.
You can write characters on ports with the write_char function, which transmits a single character. write_char is similar to the write function in that it writes to standard output if no port identifier is given.
Because Explorer lattices use their nDataVar length as one of their array dimensions when they are converted to and from Shape arrays, it can be inconvenient to work with scalar lattices (nDataVar = 1) in LatFunction. For instance, a 2D scalar lattice of size 10 x 20 in Explorer would have shape [20,10,1] and be 3D in Shape. The scalar_lattice_in function removes the last dimension of its input argument. In fact, scalar_lattice_in removes the last dimension even if its input is not a scalar lattice; in this case, it returns the first data variable at each node.
The function scalar_lattice_out adds a trailing unit dimension to an array. It can be used to convert a 2D Shape array into a 2D Explorer data array (with nDataVar = 1).
Shape can work with lists of arrays, as well as handling arrays. A list is an ordered collection of items, each of which can be an array or another list. For instance, a lattice is represented in Shape as a list of two arrays:
(coord, data) |
![]() | Note: Lists are delimited by parentheses, not by square brackets. |
The perimeter coordinates of a lattice are stored as individual vectors in a list. For a three-dimensional lattice, this would be:
(Xcoord, Ycoord, Zcoord) |
Thus the entire lattice would be:
((Xcoord, Ycoord, Zcoord), data) |
Most Shape commands do not work on lists. Instead, you must either use one of the special list processing commands or extract an array from the list and work with it directly. The list processing commands are:
| list | creates a list | |
| list_first | returns the first list element | |
| list_rest | returns the tail of the list, that is, all but what is returned by list_first. | |
| map | operates on list elements | |
| apply | operates on list elements | |
| pair_p | is this a pair (that is, a list of two elements)? | |
| null_p | is this a null list? |
The empty list has its own predefined symbol, nil.
The list function is used to concatenate two or more arrays in Shape. This can be useful when passing several arguments to a user function, or when assembling arguments for use in a map or apply call. The syntax for this function is:
list(array [, array] ...) |
list uses parentheses (), not square brackets []. It can build arbitrarily complex, heterogenous structures. list(1,2,3) is not equal to [1,2,3]. For example, to create and then concatenate two scalars and a two-vector:
x := 1; ==>x y := 2; ==>y z := [3,4]; ==>z list(x,y,z); ==>(1 2 [3 4]) |
The two functions list_first and list_rest decompose lists, extracting the first and all but the first list elements, respectively. For instance, if a is:
(1, 2, 3), then list_first(a); ==> 1 list_rest(a); ==> (2,3) |
For perimeter coordinates, the Shape array structure is a list of vectors, one for each coordinate dimension of the lattice. For instance, 3D data with perimeter coordinates has three vectors in a list: the first vector represents the X dimension, and the second and third represent the Y and Z dimensions, respectively.
You can access the list of vectors with the list processing commands, list_first and list_rest, which return the first element of a list and the remainder of the list, respectively. For instance, to access the X, Y, and Z vectors of a perimeter lattice, you could use:
X_vector := list_first(Perim_Data_coord); Y_vector := list_first(list_rest(Perim_Data_coord)); Z_vector := list_first(list_rest(list_rest(Perim_Data_coord))); |
On output, such a list is interpreted as perimeter coordinates for a lattice. You can construct a list using the list function and the nil token. You will need either to write LatFunction routines to handle all coordinate types or else restrict inputs to a known type, using the Module Builder.
The functions map and apply are used to run a Shape function on a set of inputs. The syntax for these functions is:
map(func, list...) apply(func, list...) |
The effects of the calls are different in that map runs the named function on each item in the list of inputs, whereas apply runs the named function on the concatenated list as a single input:
map(sin, list(0, 1, 2, 3)); ==>(0 0.841471 0.909297 0.14112) |
This is equivalent to concatenating several calls to sin.
sin(0); ==>0 sin(1); ==>0.841471 sin(2); ==>0.909297 sin(3); ==>0.14112 |
When the list has several arrays in it, the sin function works on each input:
map(sin, list([1, 2, 3], [4, 5], [1.1])); ==>([0.841471, 0.909297, 0.14112], [-0.756802, -0.958924],[0.8912007]) |
This is equivalent to concatenating several calls to sin.
sin([1, 2, 3]); ==>[0.841471 0.909297 0.14112] sin([4, 5]); ==>[-0.756802 -0.958924] sin([1.1]); ==>[0.891207] |
The apply function treats the list as the list of inputs to a single call of the named function.
apply(min, list(3, 4)); ==>3 |
This is equivalent to the call:
min(3, 4); ==>3 |
but is different from the concatenation of calls:
min(3); ==>3 min(4); ==>4 |
The module prelude is a set of predefined operations that runs when the LatFunction module begins firing. They include defining certain mathematical constants, such as p; predefined functions for operations that occur every time the module fires; and defining useful constants and functional associations
The postlude is a set of predefined functions that runs when the LatFunction module has finished firing. These functions assemble data from known array names and send it to the output ports. They convert Shape data types back into Explorer data types.
These prelude functions run only once, when the LatFunction module is first launched. They are:
Mathematical constants:
pi := 3.14159265358979323844; e := 2.71828182845904523536; i := make_complex(0, 1); NaN := sqrt(-1); |
Allocation fill mode indicators:
fill_NaN fill_none fill_index fill_zero |
Extract the data portion of a lattice:
function lat_data(lat) |
Extract the coordinate portion of a lattice:
function lat_coord(lat) |
Combine two arrays into coordinate data portions and return a lattice:
function make_lat(coord, data) |
Predefined Shape functions include.
Fill an array with 0, NaN, or indices:
function index_fill(s) {allocate(1L, s, fill_index);}
|
Return the transpose or diagonal of a 2D array:
function transpose(m) {send_indices([1, 0], m);}
function diagonal(m) {send_indices([0, 0], m);}
|
Convert a scalar nD lattice from or to a Shape nD array:
function scalar_lattice_out(x) {inside([x]);}
function scalar_lattice_in(x) {inside(x[0]);}
|
All nD lattices in Explorer are (n+1)D Shape arrays by default, with the nDataVar size taken as the extent of the fastest varying, n+1, dimension.
These prelude functions run every time the module fires, to organize Explorer lattice data into Shape arrays. The example shows prelude functions for the LatFunction ports “First In” and “First Out”. Similar operations occur for the other two ports and for user-defined lattice ports in a LatFunction-based module.
Define names for the input and output ports, as well as for their input lattices, their data parts, and their coordinates:
iport_First_In := open_input_lattice("First In");
oport_First_Out := open_output_lattice("First Out");
First_In_lat := read(iport_First_In);
First_In := lat_data(First_In_lat);
First_In_coord := lat_coord(First_In_lat);
|
The postlude functions run as the module completes firing. They convert Shape arrays back into Explorer lattice data for each output lattice port.
Send the output data onto a known port, for instance:
write(First_Out, oport_First_Out); |
The implication of the prelude section is that First_In, Second_In, and Third_In are Shape variables that contain the input port data for the module. On output, variables First_Out, Second_Out, and Third_Out automatically have their data flushed to output ports.
Table 1-2 summarizes the Shape functions that resemble C language functions.
Table 10-2. C-like Functions in Shape
Operation | Function Name/Syntax | Notes |
|---|---|---|
Arithmetic |
|
|
Addition | array + array |
|
Subtraction | array – array |
|
Division | array / array |
|
Tensor |
|
|
Boolean Matrix Multiplication | array |.& array | See Matrix Multiplication |
Matrix Multiplication | array +.* array | Performs a tensor product |
Casting |
|
|
To Byte | (byte) array | For example,((byte)1.1) |
To Short | (short) array |
|
To Long | (long) array | int is a synonym for long |
To Single | (complex) array |
|
To Double Complex | (double complex) array |
|
Integer |
|
|
Modulo | array % array |
|
Bitwise And | array & array |
|
Bitwise Or | array | array |
|
Bitwise Exclusive Or | array ^ array |
|
Left Shift | array << array |
|
Right Shift | array >> array |
|
Logical And | scalar && scalar | Scalar only |
Logical Or | scalar || scalar | Scalar only |
Logical Not | ! array |
|
Preincrement | ++ array | Not yet implemented |
Postincrement | array ++ | Not yet implemented |
Predecrement | –– array | Not yet implemented |
Postdecrement | array –– | Not yet implemented |
Relational |
|
|
Equal To | array == array |
|
Not Equal To | array != array |
|
Greater Than | array > array |
|
Less Than | array < array |
|
Greater Than or Equal | array >= array |
|
Less Than or Equal | array <= array |
|
Complex |
|
|
Real Part | re(array) |
|
Imaginary Part | im (array) |
|
Make Complex | make_complex(arra y, array) | re(make_complex (a, b)) |
Language Statements |
|
|
Conditional statement | if( expr) Block else | else clause is optional |
Block | Block statement
; or |
|
Function Declaration | function ( array, array,...) Block |
|
For Loop | for (expr; expr; expr) Block |
|
While Loop | while (expr) Block |
|
Define | array := expr |
|
Assign | array = expr |
|
Conditional array value | expr? expr: expr | Evaluates its arguments completely, unlike the C function with same syntax |
Return | return expr; return; |
|
Continue | continue; |
|
Break | break; |
|
Comment | // Comment text | Precedes a comment (terminated by end-of-line) (does not use /* as in C) |
List | (array) or (list, list, ...) | c ordered list of arrays and/or lists (s-expression) |
Math Library |
|
|
Square Root | sqrt | (array) |
Sine | sin | (array) |
Cosine | cos | (array) |
Tangent | tan | (array )no complex |
Arc Sine | asin | (array )no complex |
Arc Cosine | acos | (array )no complex |
Arc Tangent | atan | (array )no complex |
Hyperbolic Sine | sinh | (array) |
Hyperbolic Cosine | cosh | (array) |
Hyperbolic Tangent | tanh | (array)no complex |
Exponentiation | exp | (array) |
Logarithm | log | (array) |
Arc Tangent 2 | atan2 | (array, array )no complex |
Power | pow | (array ) no complex, also infix(**) |
Modulo (floating) | fmod | (array, array )no complex |
Remainder | drem | (array, array )no complex |
Conjugate | conj | (array) |
Ceiling | ceil | (array)no complex |
Absolute value | abs | (array) |
Truncation | floor | (array )no complex |
Rounding | nint | (array)no complex: nint (a) returns nearest integer rounding |
Sign | sign | (array )no complex, -1, 0, or 1 |
Minimum | min | (array, array, ...)takes 1 or more arguments |
Maximum | max | (array, array, ...) see Min |
Table 1-3 lists the functions specific to Shape.
Table 10-3. Functions Specific to Shape
Operation | Function Name/Syntax | Notes |
|---|---|---|
Lattice |
|
|
Copy | copy(array) |
|
Scatter | scatter (dest array, index array, source array) |
|
Gather | gather (index array, source array) |
|
ShapeOf | shape (array) |
|
Allocate | allocate(base type array, shape array, fill type token) |
|
Slice | array [index scalar] |
|
SubShape | subshape
(start array,
end array, source
array) |
|
Stack | [expr, expr, ...] |
|
SendIndices | send_indices (perm utation array, source array |
|
List Operators |
|
|
List | list_first
(list) | First element of a list |
Map | map(function, array, ...) |
|
Apply | apply(function, array, ...) |
|
List | list(array, array, ...) | Creates a list |
PairP | pair_p(list) | A pair or a single array? |
NullP | null_p (list) | The nil array? |
Miscellaneous |
|
|
Inside | inside (expr) |
|
Outside | outside (expr) |
|
InsideOut | insideout (expr ) |
|
Port IO |
|
|
OpenOutputFile | portname := open_output_file(f ilename) |
|
FlushOutputPort | flush_output_port (portname) | Calls cxOutputDataFlush |
Write | write(expr, portname) | Sends ASCII to port |
Dump | dump( array ) | Prints array debugging information |
WriteChar | write_char(portnam e) | Writes one binary byte |
Module Prelude |
|
|
Constants | pi | 3.14 |
Values from Predefined Operations | iport_<portname> | iport_First_In List terminator value |
Predefined Functions | lat_data
(lattice)
| Gets data part of lattice coordinate of length 1 |
Here are three sample programs for the LatFunction module.
You can send this program into VolumeToGeom and IsosurfaceLat, which both feed Render, to get an interesting data set; otherwise, look at the map testVol.
function chebychev(x) {
xx := x * x;
return xx * xx - 2 * xx + 1;
}
n := 20;
// Allocate vol to be a cube, then fill with 0..n-1 uniform
// coordinate indices in 3D.
vol := (short)index_fill([n, n, n, 3]);
float res[n, n, n];
// Scale vol to lie in the range -2..2 and apply the
// chebychev() function pointwise throughout vol.
// Perform a sum-reduction over the XYZ vector to compute
// the result, which is returned as a scalar lattice.
res += chebychev(vol * (4.0/n) - 2);
First_Out := scalar_lattice_out(res);
|
Figure 1-3 shows a set of contours displaced from a volume in the Render module. The program script above produces this volume.
This script allows direct manipulation of the coloring of an RGB image. Use the map color-xform to view the image.
// Create a gray-scale image by taking a linear // combination of the RGB values of the input image // as a gray-level intensity. // // Port First_In: 2D Image to transform // Port Second_In: 4x4 matrix to provide colormap transform // // Port First_Out: Transformed 2D byte Image // // Extract input transform matrix mat := scalar_lattice_in(Second_In); // Take linear combination of input matrix channels using // tensor product. v := (double) [mat[0][0], mat[0][1], mat[0][2]]; // Should be mat[0,0:2]; gray := First_In +.* v; // Scales the range to 0..255 mx := max(max(gray)); mn := min(min(gray)); if (mx == mn) mn -= 1; gray = (gray - mn) * (255 / (mx - mn)); // Assign to known output name First_Out := (byte) inside([gray, gray, gray]); |
Figure 1-4 shows the results of the color manipulation in the color-xform map. The two images shown in DisplayImg and DisplayImg<2> can be compared.
Use the heat-flux map to view this explicit time-stepping simulation of a heat flow problem.
// Animate a heat flow equation for 100 time steps and
// produce an output lattice each 5 time steps. The
// equation iterated at each timestep is
// T += 4 h^2 (1-k) Laplacian(T);
// where h is the grid spacing and k is a relaxation
// parameter. T is the temperature.
//
// No input ports
// Port First_Out: Temperatures on square grid every 5
// iterations
n := 50;
float temps[n, n]; // temperature values
float next[n, n]; // space for next iteration
// current temperatures starting at 0
// Alias a heat source and sink in the grid
source := subshape([20, 30], [29, 44], temps);
sink := subshape([30, 20], [39, 24], temps);
// Alias neighboring cells in the four directions plus
/ center and the current temperatures.
M := subshape([1, 1], [n-2, n-2], temps); // interior of mesh
// all but edge of temps
N := subshape([0, 1], [n-3, n-2], next);// north
S := subshape([2, 1], [n-1, n-2], next); // south
E := subshape([1, 0], [n-2, n-3], next); // east
W := subshape([1, 2], [n-2, n-1], next);// west
C := subshape([1, 1], [n-2, n-2], next);// center
// The iteration is T += 4 h^2 (1-k) Laplacian(T);
k := (float) 0.3;
// weight of C
int i;
for (i = 0; i < 100; i+=1) {
source += (float) 10;
sink -= (float) 10;
next = 0;
N += M; S += M; E += M; W += M;
next *= (float) 0.25 * (1 - k);
C += M * k;
temps = next;
if (i % 5 == 0) {
First_Out := scalar_lattice_out(temps);
write(First_Out, oport_First_Out);
flush_output_port(oport_First_Out);
}
}
|
Figure 1-5 shows what the output of the heat-flux program looks like in the Render module.