FISH Tutorial

This tutorial is intended for people who have run an Itasca numerical modeling code (FLAC2D, FLAC3D, UDEC, 3DEC, PFC2D, or PFC3D) at least for simple problems but have not used the FISH language. No programming experience is assumed. To get the maximum benefit from the examples given here, try them out with software running interactively. The short programs may be typed directly into the command line, accessed via the i Console tab in the Commands area. After running an example, give the command model new, to “wipe the slate clean,” ready for the next example. Alternatively, more lengthy FISH functions may be typed into data files using the the built-in editor; these can be executed directly in the editor or called (program call) in the console.

Type the lines in the example below at the command prompt, pressing <Enter> at the end of each line.

Defining a FISH Function

fish define abc
    abc = 22 * 3 + 5
end

Note

In examples orchid is used to highlight FISH symbols (variables and user-defined functions); bold violet is used to highlight FISH statement words and intrinsic functions.

Note that the command prompt changes to Fish> after the first line has been typed in; it changes back to the program prompt when the end statement is entered. This change in prompt indicates whether commands are being issued or whether FISH statements are in use. All lines following the fish define command are taken as part of the definition of a FISH function (until the end statement is entered). However, if a line that contains an error is typed (e.g., type the \(=\) sign on an empty line), then the program reverts to the program prompt (e.g., 3dec>) again with an error message. In this case, one must redefine the FISH function again from scratch, starting with the fish define statement. Because it is very easy to make mistakes, FISH functions are often typed into data files using the built-in editor. These can either be executed directly from the editor or can be executed via the program call command just like a data file that does not contain FISH. This process is described later. For now, work will continue from the command line. Assuming that the preceding lines were typed without error, a main program prompt should be visible. “Execute” the function named fname defined in the example above by typing the line

[abc]

This line causes the arithmetic operations defined in the function to be executed and returns the result (71).

The line

abc = 22 * 3 + 5

is an “assignment statement.” If an equal sign is present, the expression on the right-hand side of the equal sign is evaluated and given to the variable on the left-hand side. Note that arithmetic operations follow the usual conventions: addition, subtraction, multiplication and division are done with the signs +, -, *, and /, respectively. The sign ^ denotes “raised to the power of.”

The actions above create a FISH symbol: in this case, a FISH function. One can see the abc FISH function in the FISH Global Symbols panel in the Tools area. In addition, one may use abc in a command by invoking an inline FISH expression via square brackets.

fish list [abc]

The output

71

appears in the console. This illustrates that once the symbol abc is defined, it may be used in program commands using inline FISH syntax, described below.

Variables

Now start a slightly different program (first using the model new command to erase the old function and reset the model state).

Using a variable

model new
fish define abc
    hh = 22
    abc = hh * 3 + 5
end

Here the variable hh is introduced and assigned the value of 22 and then used in the next line. Notice that as in the previous case, the FISH function name abc is identical to the FISH variable abc defined in the function. However, in this case there are two FISH symbols. The two symbols both have values, but one (abc) is known as a “function,” and the other (hh) as a “variable.” The distinction is as follows.

When a FISH symbol name is mentioned, the associated function is executed if the symbol corresponds to a function. However, if the symbol is not a function name, then the current value of the symbol is used.

A FISH function can have any name. However, naming a FISH function to be identical to a variable set inside the function creates a return value from the execution of the FISH function. This mechanism is frequently used, especially when FISH histories (using the fish history command) are needed or when FISH callbacks (using the fish callback command) are used. Another way to set a return value is to exit the function using the c return statement.

The following experiment may help to clarify the distinction between variables and functions. Before doing the experiment, note that inline FISH can be used to set the value of any user-defined FISH symbol, independent of the FISH function in which the symbol was introduced. Type the following lines without giving the model new command, keeping the previously entered program in memory:

[abc = 0] 
[hh = 0]
[hh]
[abc]
[hh]

The values of abc and hh are set to 0 in the first two lines. The function abc is not executed, as the first line is an assignment operation. Note that abc still refers to the same FISH function even though its value has been modified. abc will always refer to the defined FISH function unless it is redefined with the fish define command. Following the two assignment operations, the value of hh is printed. The line [abc] executes the FISH function and prints the updated value corresponding to that execution on the command line. The function is executed, as abc refers to a function and the value of abc is not being assigned. Thus the value of abc has been changed along with the value of hh, as the last line demonstrates.

Using the inline FISH syntax to manipulate FISH variables is recommended. However, an alternate syntax using the fish list command can achieve the same result as above.

[global abc=0, hh=0]
fish list [hh]
fish list [abc]
fish list [hh]

In this case, the second fish list command causes abc to be executed, as abc is the name of a function, causing the values of both hh and abc to be updated.

As an exercise, type in the slightly modified sequence shown in the next example and figure out why the displayed answers are different.

Exercise: understanding function and variable names

model new
[hh = 22]
fish define abc
    abc = hh * 3 + 5
end
[abc]
[abc = 0] 
[hh = 0]
[hh]
[abc]
[hh]

Using FISH for Custom Calculations

▸ Monitoring load on a zone (FLAC3D)

The following example shows how the total load on a side platen of a uniaxial test sample can be stored as a history for later inspection.

Capturing the history of a FISH variable

model new
model large-strain off
zone create brick size 1 2 1
zone face skin
zone cmodel assign mohr-coulomb
zone property shear=1e8 bulk=2e8 cohesion=1e5 tension=1e10
zone face apply velocity (0,0,0) range group "South"
zone face apply velocity-y -1e-5 range group "North"
[ad1 = gp.near(0,2,0)]
[ad2 = gp.near(1,2,0)]
[ad3 = gp.near(0,2,1)]
[ad4 = gp.near(1,2,1)]
fish define load
    load = gp.force.unbal(ad1)->y + gp.force.unbal(ad2)->y  + ...
           gp.force.unbal(ad3)->y + gp.force.unbal(ad4)->y
end
fish history load
zone history displacement-y position (0,2,0)
model step 1000
plot item create chart-history history '1' vs '2' reversed on

Note that the FISH variable load is equal to the sum of four other variables, gp.force.unbal(index)->y. The FISH grid variable gp.force.unbal is an example of a grid quantity that is available within a FISH program and is termed a FISH intrinsic; see Zone Gridpoint Functions for a complete list of the grid-related intrinsics. Often times, FISH intrinsics are predefined model quantities but, in other cases, they define more complex model operations.

In the example, the FISH intrinsic gp.force.unbal provides the unbalanced gridpoint force; the \(y\)-component specifically is obtained using ->y; each instance of gp.force.unbal must be provided a grid point memory address (or pointer). The address is found by defining four variables (ad1, ad2, ad3, and ad4) using the FISH grid point intrinsic gp.near to find, respectively, the address of the gridpoints closest to the \(x,y,z\) global coordinates (0,2,0), (1,2,0), (0,2,1), and (1,2,1). By creating a history, the FISH function load is executed when histories are updated during cycling (see the history interval command to change the history update frequency). Since the value of load is updated in the function (i.e., the function name and update variable name are the same), the history obtains an updated value each time it is called. At the end of the run, one can simply plot the history of load (history 1) just like a predefined FLAC3D history. this line STRONGLY implies the need for a topic on plotting histories Similarly, FISH functions may be used to post-process or output the computed history for further analysis.

In addition to the above-mentioned FISH intrinsics for grid points, there are a plethora of intrinsics available for use in FISH. Zone-specific intrinsics and structural element intrinsics are indexed in the sections on those model objects.

▸ Monitoring load on a zone (FLAC2D)

The following example shows how the total load on a side platen of a uniaxial test sample can be stored as a history for later inspection.

Capturing the history of a FISH variable

model new
model large-strain off
zone create2d quadrilateral size 2 1
zone cmodel assign mohr-coulomb
zone property shear 1e8 bulk 2e8 cohesion 1e5 tension 1e10
zone gridpoint fix velocity range position-x 0
zone gridpoint fix velocity-x range position-x 2
zone gridpoint initialize velocity-x -1e-5 range position-x 2
fish define load
    load = gp.force.unbal.x(gp.near(2,0)) + gp.force.unbal.x(gp.near(2,1))
end
fish history load
zone history displacement-x position (2,0)
model step 1000
plot item create chart-history history '1' vs '2' reversed on

Note that the FISH variable load is equal to the sum of four other variables, gp.force.unbal(index)->x. The FISH grid variable gp.force.unbal is an example of a grid quantity that is available within a FISH program and is termed a FISH intrinsic; see Zone Gridpoint Functions for a complete list of the grid-related intrinsics. Often times, FISH intrinsics are predefined model quantities but, in other cases, they define more complex model operations.

In the example, the FISH intrinsic gp.force.unbal.x provides the unbalanced gridpoint force (the \(x\)-component specifically is obtained). Each instance of gp.force.unbal.x must be provided a grid point memory address (or pointer). The address is found with the intrinsic gp.near to obtain the gridpoints closest to the (\(x,y\)) global coordinates (2,0) and (2,1). By creating a history, the FISH function load is executed when histories are updated during cycling (see the history interval command to change the history update frequency). Since the value of load is updated in the function (i.e., the function name and update variable name are the same), the history contains updated values each time it is called. At the end of the run, one can simply plot the history of load (history 1) just like a predefined FLAC2D history. this line STRONGLY implies the need for a topic on plotting histories In a similar vein, one may use FISH functions to post-process or output the computed history for further analysis.

▸ Monitoring stress in a zone (3DEC)

The following example shows how stress in a particular zone can be monitored.

Capturing the history of a FISH variable

model new
model large-strain on

block create brick 0 10 0 10 0 10
block zone generate edgelength 10
block zone cmodel assign elastic
block zone property density 1000 bulk 1e9 shear 0.7e9 
block gridpoint apply velocity-z 0.0 range position-z -0.01 0.01
model gravity 0 0 -10
fish define stress_z
  zoneIdx = block.zone.near(5,5,5)
  stress_z = block.zone.stress.zz(zoneIdx)
end
fish history stress_z
model cycle 200
plot item create chart-history history '1' vs '2' reversed on

In the example, a pointer to the zone with a centroid closest to (5,5,5) is found with the FISH intrinsic block.zone.near. The FISH intrinsic block.zone.stress.zz then provides the \(zz\) stress for the zone. By creating a history, the FISH function stress_z is executed when histories are updated during cycling (see the history interval command to change the history update frequency). Since the value of stress_z is updated in the function (i.e., the function name and update variable name are the same), the history contains updated values each time it is called. At the end of the run, one can simply plot the history of stress_z (history 1) just like a predefined the program history. this line STRONGLY implies the need for a topic on plotting histories Similarly, we may use FISH functions to post-process or output the computed history for further analysis.

In addition to the above-mentioned FISH intrinsics for zones, there are a plethora of intrinsics available for use in FISH. Block-specific intrinsics are and structural element intrinsics are indexed in the sections on those model objects.

▸ Monitor the value of a FISH variable (PFC)

model new
model configure dynamic
model large-strain on
domain extent -10 10
wall generate id 5 polygon (-2,0,2) (2,0,2) (2,0,-2) (-2,0,-2)
wall property 'kn' 1e8
ball create id 1 position (0, 1.1, 0) radius 1
ball attribute density 1000
ball property 'kn' 1e8
model gravity 0 -10 0
fish define down_force
   down_force = ball.force.unbal.y(ball.find(1))
end
fish history down_force
model cycle 500

General FISH Functions

The FISH Functions page in the FISH Scripting Reference provides an index for all general FISH intrinsics. Using math, for instance, enables things like sines and cosines to be calculated from within a FISH function. The list below samples some of the functions available from the set of math intrinsics.

math.abs(n)

absolute value of n

math.cos(n)

cosine of n (n is in radians)

math.log(n)

base-ten logarithm of n

math.max(n1, n2)

returns maximum of n1, n2

math.sqrt(n)

square root of n

A more complex example using a number of intrinsics will be presented later, but here observe one more way a program data file can make use of user-defined FISH names:

Substituting the name of a FISH variable or function using the inline FISH notation [ ] is acceptable wherever a value is expected in a program command

This simple statement is the key to a very powerful feature of FISH that allows such things as ranges, applied stresses, properties, etc., to be computed in a FISH function and used in program commands in symbolic form. Hence, parameter changes can be made very easily, without the need to change many numbers in an input file.

As an example, assume the Young’s modulus and Poisson’s ratio of a material are known. If the user wishes to use the bulk and shear moduli, these may be derived with a FISH function, using the equations below:

\[G={E\over2(1+\nu)}\]
\[K={E\over3(1-2\nu)}\]

Coding these equations into a FISH function (called derive) can then be done as shown in this example:

FISH functions to calculate bulk and shear moduli

model new
fish define derive(y_mod,p_ratio)
    s_mod = y_mod / (2.0 * (1.0 + p_ratio))
    b_mod = y_mod / (3.0 * (1.0 - 2.0 * p_ratio))
end
[derive(5e8,0.25)]
[b_mod] 
[s_mod]

Note that we execute the function derive by giving its name with arguments inside parenthesis. In this case the arguments, the Young’s modulus and Poisson’s ratio, respectively, are used as temporary variables to compute the bulk and shear moduli; as a result, they do not appear as FISH variables after derive has been executed. The computed values of the bulk and shear moduli (b_mod and s_mod, respectively) can subsequently be used, in symbolic form, in program commands as shown in the following examples.

▸ Using symbolic variables in FLAC3D input

zone create brick size (2,2,2)
zone cmodel assign elastic
zone property bulk [b_mod] shear [s_mod]
zone list property bulk
zone list property shear

The result of these operations can be checked by printing bulk and shear in the usual way (e.g., using the zone list property command).

▸ Using symbolic variables in FLAC2D input

zone create2d quadrilateral size 2,2
zone cmodel assign elastic
zone property bulk [b_mod] shear [s_mod]
zone list property bulk
zone list property shear

The result of these operations can be checked by printing bulk and shear in the usual way (e.g., using the zone list property command).

▸ Using symbolic variables in 3DEC input

block create brick -1 1 -1 1 -1 1
block zone generate edgelength 1
block zone cmodel assign elastic
block zone property bulk [b_mod] shear [s_mod]
block zone list property bulk
block zone list property shear

The result of these operations can be checked by printing bulk and shear in the usual way (e.g., using the block zone list property command).

▸ Using symbolic variables in PFC input

Note the example that follows does not use the bulk and shear modulii material as in the preceding examples. The use of FISH variables where values are expected in commands, however, is still illustrated in this self-contained example.

model new
model large-strain on
domain extent -10 10
[n_stiff = 2e8]
[s_stiff = 1e8]
[lx = 5.0]
[ly = 3.0]
[lz = 2.0]

wall generate id 1 polygon (0,0,0)([lx],0,0)([lx],[ly],0)(0,[ly],0)
wall generate id 2 polygon (0,0,[lz])(0,[ly],[lz])([lx],[ly],[lz])([lx],0,[lz])
wall generate id 3 polygon (0,0,0)(0,0,[lz])([lx],0,[lz])([lx],0,0)
wall generate id 4 polygon (0,[ly],0)([lx],[ly],0)([lx],[ly],[lz])(0,[ly],[lz])
wall generate id 5 polygon (0,0,0)(0,[ly],0)(0,[ly],[lz])(0,0,[lz])
wall generate id 6 polygon ([lx],0,0)([lx],0,[lz])([lx],[ly],[lz])([lx],[ly],0)
wall property 'ks'=[s_stiff] 'kn'=[n_stiff]
plot create
plot view projection perspective magnification 1 ...
    center (2,2,0.5) eye (8.3,-4.8,4.6)
plot item create wall active on
wall list property

FISH Rules, Syntax, and Statements, Illustrated

There is great flexibility in choosing names for FISH variables and functions. The underscore character ( _ ) may be included in a name. Names must begin with an alphabetical letter (i.e., cannot be a number or a special character) and must not contain any of the arithmetic operators (+, -, /, *, or ^). A chosen name cannot be the same as the name of one of the built-in FISH intrinsics. FISH variable names are not case sensitive, so that aVariable and AVARiable are the same variable.

In addition to inspecting FISH variable/function names in the global symbols Tools panel, one can also obtain a list of all current variables and functions using the fish list command. A printout of all current values, sorted alphabetically by name, is produced as a result of this command.

fish list

Now consider ways that decisions can be made, and repeated operations done, via FISH programming. The following FISH statements allow specified sections of a program to be repeated many times.

loop var (a1, a2)

endloop

The words c loop and c endloop are FISH statements, the symbol var stands for the loop variable, and a1, a2 stand for expressions (or single variables) that bound the loop. The next example shows the use of a loop (or repeated sequence) to produce the sum and product of the first ten integers.

Controlled loop in FISH

model new
fish define xxx
    sum  = 0
    prod = 1
    loop n (1,10)
        sum  += n
        prod *= n
    endloop
    io.out('The sum is ' + string(sum) + ' and the product is ' + string(prod))
end
[xxx]

In this case, the loop variable n is given successive values from 1 to 10, and the statements inside the loop (between the loop and endloop statements) are executed for each value. As mentioned, variable names or an arithmetic expression could be substituted for the numbers 1 or 10. Note that the exitloop statement can be used to break out of a FISH loop and the continue statement can be used to skip the remaining instructions in the loop, moving to the next iteration of the loop.

Traditional for loop in FISH

model new
fish define xxx
    sum  = 0
    prod = 1
    loop for (n = 1, n <= 10, n += 1)
        sum  += n
        prod *= n
    endloop
    io.out('The sum is ' + string(sum) + ' and the product is ' + string(prod))
end
[xxx]

Besides standard looping as depicted above, one can easily loop over sets of model objects (i.e., zones, blocks, balls, fractures, structural element nodes, etc.) using the c foreach construct. In this case, a container of objects must be given by a FISH intrinsic (zone.list (FLAC3D) or block.zone.list (3DEC), for example). Practical uses of the c foreach statement are supplied in the following examples.

▸ Install nonlinear distribution of a property in a grid (FLAC3D)

Suppose that the Young’s modulus at a site is given by this equation:

\[E=E_\circ+c\sqrt{z}\]

where \(z\) is the depth below surface, and \(c\) and \(E\) are constants. The FISH function install in the example below example installs appropriate values of bulk and shear modulus in the grid.

model new
zone create brick point 0 (0,0,0) point 1 (-10,0,0) ...
                  point 2 (0,10,0) point 3 (0,0,-10)
zone cmodel assign elastic
fish define install(y_zero,cc)
    loop foreach pnt zone.list
        z_depth = -zone.pos(pnt)->z
        y_mod = y_zero + cc * math.sqrt(z_depth)
        zone.prop(pnt,'young') = y_mod
    endloop
end
[install(1e7,1e8)]
zone property poisson 0.25
plot item create zone contour property name 'young'

One may verify correct operation of the function by printing or plotting shear and bulk moduli.

In the function install, the loop takes place over all zones in the global list of zones. The FISH statement loop foreach is a variation of the loop statement that sets pnt to each zone in zone.list. Inside the loop, the \(z\)-coordinate of each zone centroid is used to calculate the Young’s modulus, given in the equation above. It is assumed that the datum (or ground surface reference point) is at \(z\) = 0. The variables zone.pos(pnt) and zone.prop(pnt, 'young') are zone intrinsics. (Recall previous discussion of the gridpoint intrinsic gp.force.unbal earlier.) Here, properties are set directly from within a FISH function, rather than with a zone property command as in the earlier example “Monitoring Load on a Zone (FLAC3D)”.

▸ Install nonlinear distribution of a property in a grid (FLAC2D)

A practical use of the loop construct is to install a nonlinear initial distribution of elastic moduli in a FLAC grid. Suppose that the Young’s modulus at a site is given by the equation

\[E=E_\circ+c\sqrt{z}\]

where \(z\) is the depth below surface, and \(c\) and \(E\) are constants. The FISH function install in the example below example installs appropriate values of bulk and shear modulus in the grid.

model new
zone create2d quadrilateral point 0 (0,-10) point 1 (10,-10)
zone cmodel assign elastic
fish define install(y_zero,cc)
    loop foreach pnt zone.list
        z_depth = -zone.pos(pnt)->y
        y_mod = y_zero + cc * math.sqrt(z_depth)
        zone.prop(pnt,'young') = y_mod
    endloop
end
[install(1e7,1e8)]
zone property poisson 0.25
plot item create zone contour property name 'young'

One may verify correct operation of the function by printing or plotting shear and bulk moduli.

In the function install, the loop takes place over all zones in the global list of zones. The FISH statement loop foreach is a variation of the loop statement that sets pnt to each zone in zone.list. Inside the loop, the \(z\)-coordinate of each zone centroid is used to calculate the Young’s modulus, given in the equation above. It is assumed that the datum (or ground surface reference point) is at \(z\) = 0. The variables zone.pos(pnt) and zone.prop(pnt, 'young') are zone intrinsics. (Recall previous discussion of the gridpoint intrinsic gp.force.unbal earlier.) Here, properties are set directly from within a FISH function, rather than with a zone property command as in the earlier example “Monitoring Load on a Zone (FLAC2D)”.

▸ Install nonlinear distribution of a property in a grid (3DEC)

Suppose that the Young’s modulus at a site is given by this equation:

\[E=E_\circ+c\sqrt{z}\]

where \(z\) is the depth below surface, and \(c\) and \(E\) are constants. The FISH function install in the example below example installs appropriate values of bulk and shear modulus in the grid.

model new
block create brick 0 30 0 30 -30 0
block cut joint-set dip 36 dip-direction 270 spacing 6 number 20
block cut joint-set dip -58 dip-direction 270 spacing 6 number 20
block zone generate edgelength 2
block zone cmodel assign elastic

fish define install(y_zero,cc)
    loop foreach pnt block.zone.list
        z_depth = -block.zone.pos.z(pnt)
        y_mod = y_zero + cc * math.sqrt(z_depth)
        block.zone.prop(pnt,'young') = y_mod
    end_loop
end
[install(1e7,1e8)]
block zone property poisson 0.25
;plot item create block contour value property name 'young'

One may verify correct operation of the function by printing or plotting shear and bulk moduli.

In the function install, the loop takes place over all zones in the global list of zones. The FISH statement loop foreach is a variation of the loop statement that sets pnt to each zone in block.zone.list. Inside the loop, the \(z\)-coordinate of each zone centroid is used to calculate the Young’s modulus, given in the equation above. It is assumed that the datum (or ground surface reference point) is at \(z\) = 0. The variables block.zone.pos(pnt) and block.zone.prop(pnt, 'young') are zone intrinsics. Here, properties are set directly from within a FISH function, rather than with a block zone property command as in the earlier example “Monitoring stress in a zone (3DEC)”.

▸ Randomly vary a property of the balls in a model (PFC)

A practical use of the loop in PFC would be to select each ball in a model in turn and access and/or change some property. The example below contains the function varyballs, which uses a FISH loop to (randomly, in this case) set the radius of each ball in the model. The function sets ball radii to integer values from 1 to 3. Compare this to the command ball generate radius, which would, given similar input values, set ball radii (randomly) to float values between 1 and 3.

model new
model large-strain on
domain extent -10 10
ball generate box -10 10 id 1 20
fish define varyballs
   loop foreach bp ball.list
      xx = int((math.random.uniform()*3)+1)
      ball.radius(bp) = xx
   endloop
end
[varyballs]
plot create
plot item create ball
ball list attribute radius

Note that loop foreach will work on any indexible FISH type, which includes lists, map, pointers to containers of model objects, and even strings, vectors, and tensors.

Having seen several examples of FISH functions, it is advisable to briefly examine the question of FISH syntax and style. A complete FISH statement occupies one line. However, a line may be typed across two or more lines as long as each line but the ultimate is terminated with the continuation character ( … ). Use of temporary variables as hinge points to concatenate lengthy formulas can also be handy. The following example shows how this can be done.

Splitting lines

model new
fish define long_sum  ;example of a sum of many things
    global v1,v2,v3,v4,v5,v6,v7,v8,v9,v10,v11,v12,v13,v14,v15
    
    local temp = v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10
    long_sum = temp + v11 + v12 + v13 + v14 + v15
    
    temp = v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10 ...
              + v11 + v12 + v13 + v14 + v15
    
end

In this case, the sum of fifteen variables is split into two parts. The local designation for temp means that it is used just during the execution of long_sum and discarded afterward.

By default, FISH variables are global, meaning that they stick around in memory space until a model new command is given. One can specify a variable as global with the global statement. It is good practice, and may be more efficient, to designate all FISH variables that will not be used by other functions or commands as local. For instance, one can make a loop using a local variable as follows (in FLAC3D):

loop foreach local pnt zone.list

or (in 3DEC)

loop foreach local pnt block.zone.list

or (general)

loop local n (1,10)

Also note the use of the semicolon after the definition of long_sum; this indicates a comment. Any characters that follow a semicolon are ignored by the FISH compiler, but they are echoed to the log file. It is good programming practice to annotate programs with informative comments. Some of the programs have been shown with indentation (i.e., space inserted at the beginning of some lines to denote a related group of statements). Any number of space characters may be inserted (optionally) between variable names and arithmetic operations to make the program more readable. Again, it is good programming practice to include indentation to indicate things like loops, conditional clauses, and so on. Spaces in FISH are “significant” in the sense that space characters may not be inserted into a variable or function name.

One other topic that should be addressed now is that of variable type. It may be been noticed, when printing out variables from the various program examples, that numbers are either printed without decimal points or in “E-format” (i.e., as a number with an exponent denoted by “E”). At any instant in time, a FISH variable or function name is classified as one of various types: Boolean, integer, matrix, pointer, real, string, list, 2D vector, 3D vector, tensor, map, or structure. These types may change dynamically, depending on context, but the casual user should not normally have to worry about the type of a variable, since it is set automatically. Consider the following example:

Variable types

model new
fish define types
    v1 = 2
    v2 = 3.4
    v3 = 'Have a nice day'
    v4 = v1 * v2
    v5 = v3 + ', old chap'
    v6 = vector(1,2,3)
    v7 = matrix(vector(1,1,1))
    v8 = true
    v9 = list.range(1,15)
end
[types]
fish list

The resulting screen display looks like this:

Name              Type Value
----- -------- ------- --------------------
types  Func(0) Integer 0
v1             Integer 2
v2               Float 3.4
v3              String Have a nice day
v4               Float 6.8
v5              String Have a nice day, old chap
v6             Vector3 (1,2,3)
v7              Matrix Size (3,1)
v8             Boolean true
v9                List Size 15

The variables v1, v2, and v3 are converted to integer, float (or real), and string, respectively, corresponding to the numbers (or strings) that were assigned to them. Integers are exact numbers (without decimal points) but are of limited range; floating-point numbers have limited precision (about 15 decimal places), but are of much greater range; string variables are arbitrary sequences of characters; and pointers are used to address model components or other internal objects. There are various rules for conversion between the types. For example, v4 becomes a floating-point number, because it is set to the product of a floating-point number and an integer; the variable v5 becomes a string because it is the sum (concatenation) of two strings. The topic can get quite complicated, but it is fully explained in Operators in FISH.

There is another language element in FISH that is commonly used: the if then else statement. The following three statements allow decisions to be made within a FISH program:

if [expression] then
   ...
else if [expression] then
   ...
else
   ...
endif

These statements allow conditional execution of FISH function segments; else if, else, and then are optional. The expression must evaluate to a Boolean type or a value convertible to a Boolean type (for example an integer is considered false if it is identically zero and true otherwise). There are a number of boolean operator types, these include:

==

#

!=

>

<

>=

<=

The meanings are standard except for #, which means “not equal.” and is a synonym for !=. If the expression evaluates to true, then the statements immediately following if are executed until else if, else, or endif is encountered. If the test is false, then any else if statment is evaluated. Otherwise the statements between else and endif are executed if the else statement exists; otherwise, the program jumps to the first line after endif. The action of these statements is illustrated in the next example:

Action of the if else endif construct

model new
fish define abc(xx)
    if xx > 0 then
        abc = 33
    else
        abc = 11
    endif
end
[abc(1)]
[abc(-1)]

The displayed value of abc in this example depends on the argument provided to abc when it is executed. Try experimenting with different test symbols (e.g., replace > with <).

Until now, FISH functions have been invoked adjacent to or within program commands by using the square brackets [ ] of inline FISH. It is also possible to do the inverse, to give program commands from within FISH functions. Most valid program commands can be embedded between the following FISH statements:

command

endcommand

There are two main reasons for eliciting program commands from a FISH program. First, it is possible to use a FISH function to perform operations that are not possible using the predefined FISH intrinsics mentioned above. Second, it becomes possible to control a complete run with FISH.

▸ Using FISH to place structural elements (FLAC3D, FLAC2D, and 3DEC)

As an illustration of the first use of the command endcommand statement mentioned above, this example shows a FISH function to install a number of cable elements at different depths in a material (for FLAC3D, FLAC2D, and 3DEC). The number of cables and number of segments are provided as arguments to the function. When many cable elements are required, it becomes tedious to type many separate structure cable create commands, each with different grid values. However, FISH can elicit program commands from within a loop and assign the location of the cable ends automatically during the loop, as the example illustrates.

▸ Automated placing of cable elements (FLAC3D)

model new
zone create brick size 10 3 5
fish define place_cables(num,segs)
    loop local n (1,num)
        local z_d = n - 0.5
        command
            structure cable create by-line 0.0 1.5 [z_d] 7.0 1.5 [z_d] ...
                                   segments [segs]
        endcommand
    endloop
end
[place_cables(5,7)]

▸ Automated placing of cable elements (FLAC2D)

model new
zone create2d quadrilateral size 10 5
fish define place_cables(num,segs)
    loop local n (1,num)
        local z_d = n - 0.5
        command
            structure cable create ...
                            by-line 0.0 [z_d] 7.0 [z_d] ...
                            segments [segs]
        endcommand
    endloop
end
[place_cables(5,7)]

▸ Automated placing of cable elements (3DEC)

model new
block create brick 0 10 0 3 0 5
block zone generate edgelength 1
fish define place_cables(num,segs)
    loop local n (1,num)
        local z_d = float(n) - 0.5
        command
            structure cable create by-line 0.0 1.5 [z_d] 7.0 1.5 [z_d] ...
              segments [segs]
        end_command
    end_loop
end
[place_cables(5,7)]

After entering these statements, list and plot the cable data to verify that five cables have been created, and that they are located at the appropriate positions in the grid. In the example, the variable z_d is used as a parameter in the function place_cables; z_d is the \(z\)-coordinate (FLAC3D, 3DEC) or \(y\)-coordinate (FLAC2D) of the endpoints of each cable. Neither the loop index n or z_d are needed in future computations and are, therefore, designated as local.

This example can be modified to perform a construction sequence of excavation and installation of cables. This illustrates the second use of command endcommand previously mentioned. The zone gridpoint free (FLAC3D and FLAC2D) or block gridpoint apply-remove (3DEC) and model solve commands are used to “excavate” the boundary plane at \(x\) = 0 in five steps. At the end of each step, a row of three cables is installed and then the next section of the boundary is excavated.

▸ Sequence of excavation and cable placement (FLAC3D)

model new
zone create brick size 10 3 5
zone face skin
zone cmodel assign mohr-coulomb
zone property density 1000 bulk 1e8 shear 0.3e8 ...
              friction 35 cohesion 1e3 tension 1e3 
model gravity 0 0 -10
zone gridpoint fix velocity (0,0,0) range group "Bottom"
zone gridpoint fix velocity-y 0.0 range union group "South" group "North"
zone gridpoint fix velocity-x 0.0 range union group "West" group "East"
model large-strain on
model history mechanical unbalanced-maximum
model solve
model save 'cab_str'
zone gridpoint initialize displacement (0,0,0)
zone history displacement-x position 0 1 5
fish define place_cables(num,segs)
    loop local n (1,num)
        local z_d = 5.5 - n
        local z_t = z_d + 0.5
        local z_b = z_d - 0.5
        command
            zone gridpoint free velocity-x ...
                           range position-x 0.0 position-z [z_b] [z_t]
            model solve
            structure cable create by-line 0.0 0.5 [z_d] 7.0 0.5 [z_d] ... 
                                   segments [segs]
            structure cable create by-line 0.0 1.5 [z_d] 7.0 1.5 [z_d] ... 
                                   segments [segs]
            structure cable create by-line 0.0 2.5 [z_d] 7.0 2.5 [z_d] ... 
                            segments [segs]
            structure cable property young 2e10 yield-tension 1e8 ...
                                     cross-sectional-area 1.0 
            structure cable property grout-stiffness 2e10 ...
                                     grout-cohesion 1e10 grout-perimeter 1.0
        endcommand
    endloop
end
[place_cables(5,7)]
model save 'cab_end'

▸ Sequence of excavation and cable placement (FLAC2D)

model new
zone create2d quadrilateral size 10 5
zone face skin
zone cmodel assign mohr-coulomb
zone property density 1000 bulk 1e8 shear 0.3e8 ...
              friction 35 cohesion 1e3 tension 1e3
model gravity 0 -10
zone gridpoint fix velocity (0,0) range group "Bottom"
zone gridpoint fix velocity-x 0.0 range union group "West" group "East"
model large-strain on
model history mechanical unbalanced-maximum
model solve
model save 'cab_str'
zone gridpoint initialize displacement (0,0)
zone history displacement-x position 0 4.999
fish define place_cables(num,segs)
    loop local n (1,num)
        local z_d = 5.5 - n
        local z_t = z_d + 0.5
        local z_b = z_d - 0.5
        command
            zone gridpoint free velocity-x ...
                           range position-x 0.0 position-y [z_b] [z_t]
            model solve
            structure cable create by-line 0.0 [z_d] 7.0 [z_d] ...
                                   segments [segs]
            structure cable property young 2e10 yield-tension 1e8 ...
                                     cross-sectional-area 1.0
            structure cable property grout-stiffness 2e10 ...
                                     grout-cohesion 1e10 grout-perimeter 1.0
        endcommand
    endloop
end
[place_cables(5,7)]
model save 'cab_end'

▸ Sequence of excavation and cable placement (3DEC)

model new
model large-strain on

block create brick 0 10 0 3 0 5
block zone generate edgelength 1
block zone cmodel assign mohr-coulomb
block zone property density 1000 bulk 1e8 shear 0.3e8 friction 35
block zone property cohesion 1e3 tension 1e3
model gravity 0 0 -10

block gridpoint apply velocity-x 0 range position-z 0.0
block gridpoint apply velocity-y 0 range position-z 0.0
block gridpoint apply velocity-z 0 range position-z 0.0
block gridpoint apply velocity-y 0 range position-y 0.0
block gridpoint apply velocity-y 0 range position-y 3.0
block gridpoint apply velocity-x 0 range position-x 0.0
block gridpoint apply velocity-x 0 range position-x 10.0
model history mechanical unbalanced-maximum
model solve
model save 'cab_str'

model restore 'cab_str'

block gridpoint initialize displacement 0 0 0
block history displacement-x position 0 1 5

fish define place_cables(num,segs)
    loop local n (1,num)
        local z_d = 5.5 - float(n)
        local z_t = z_d + 0.5
        local z_b = z_d - 0.5
        command
            block gridpoint apply-remove velocity-x ...
                                range position-x 0.0 position-z [z_b] [z_t]
            model solve
            structure cable create by-line 0.0 0.5 [z_d] 7.0 0.5 [z_d] ...
                                   segments [segs] 
            structure cable create by-line 0.0 1.5 [z_d] 7.0 1.5 [z_d] ...
                                   segments [segs] 
            structure cable create by-line 0.0 2.5 [z_d] 7.0 2.5 [z_d] ...
                                   segments [segs] 
            structure cable property young 2e10 yield-tension 1e8 ...
                                     cross-sectional-area 1.0 ...
                                     grout-stiffness 2e10 ...
                                     grout-cohesion 1e10 
        end_command
    end_loop
end
[place_cables(5,7)]
model save 'cab_end'

▸ Using FISH to generate walls (PFC)

A simple illustration of the first use of FISH delineated above is provided here.

model new
model large-strain on
domain extent -10 10
fish define make_walls
  command
    wall generate id 1 polygon (0, 0, 0) (0, 2, 0) (0, 0, 2)
    wall generate id 2 polygon (0, 0, 0) (0, 0, 2) (2, 0, 0)
  endcommand
end
[make_walls]
plot create
plot item create wall
plot view projection perspective magnification 1 ...
    center (2,2,0.5) eye (8.3,-4.8,4.6)

Lists

It is often the case that one would like to store a list of objects that they will loop over in the future. These may be computed values from zones, for instance, or specific gridpoint pointers themselves. FISH has three containers to use in these circumstances, termed lists, maps, and arrays. Of the three of them, the most commonly used and most generally useful is the list.

A list holds a number of FISH variables of any type that can be looped over or accessed by an integer index. Lists can be resized, values inserted, and values appended and prepended. The simple example below shows how one can create a list of integers and then sum the values.

List example

model new
fish define list_operation
    ;create and populate an array with products of 2
    arr = list.create(10)
    loop local n (1,10)
        arr(n) = 2*n
    endloop
      
    ;compute the sum and product of elements in the array
    sum = 0
    prod = 1
    local i = 1
    loop while (i <= list.size(arr))
        sum = sum + arr(i)
        prod = prod * arr(i)
        i = i + 1
    endloop
    io.out('The sum is ' + string(sum) + ' and the product is ' + string(prod))
end
[list_operation]

In this example, a list is created and filled with numbers. The loop while construct is used to loop over the list entries and the sum and product are computed and output.

Lists can also be used to do repeated calculations on each element. The mathematical operators, when acting on a list, will instead apply the operator to each element in the list separately. When member access operator -> is applied to the list, it returns a list of the results of operator action on each element in the list. An alternative to the list example above looks like:

List example (alternative)

model new
fish define list_operation
    ;create and populate an array with products of 2
    arr = list.range(1,10)*2
    ;compute the sum and product of elements in the array
    sum = list.sum(arr)
    prod = 1
    loop foreach i arr
        prod *= i
    endloop
    io.out('The sum is ' + string(sum) + ' and the product is ' + string(prod))
end
[list_operation]

Maps, and Arrays

A map is an associative container, meaning that one can access the members of a map by an associated key (which can be any acceptable simple type, e.g., integer, float, string, etc.) A new member can be added to a map or existing member can be modified using key-value pair information. Maps can dynamically be resized and added to one another (appending maps together). Maps are useful to allow fast lookup of values based on a known key.

Map example

model new
fish define map_operation
    ; create and populate a map
    my_map = map('key1',1,  'key2',2,  3,3) ; Three initial key,value pairs
    my_map('key4') = 4
    my_map(3.94) = 5
    
    ;compute the sum and product of elements in the map
    value = my_map('key4')
    io.out('The value associated with key4 is ' + string(value))
end
[map_operation]

An array is similar to a list, with two important distinctions. First, an array can have multiple dimensions assigned on creation. There is no limit to the number of dimensions an array can be created with, up to memory availability. Second, arrays (which can be very large) are passed by reference, not by value. This means that if a is made equal to b and b is an array, they both point to the same array.

Array example

model new
fish define array_operation
    ; create a 3 dimensional array
    arr = array.create(4,5,6)
    arr(2,3,4) = 3.0
    arr(1,1,1) = 1.0
    
    brr = arr
    brr(4,4,4) = 4.0   ; brr and arr are the same array!
    io.out(arr(4,4,4))
end
[array_operation]

Lists, maps, and arrays can all be looped through using the loop foreach construct. In the case of maps the loop variable is the value held in each map entry, not the key value. Use the map.keys intrinsic to loop through the key values of a map.

List Splitting and Filtering

As mentioned above, list types are somewhat special in FISH. They can be used to allow repeated calculations using just a few lines of FISH code. In addition those operations may, if the list is large enough, be automatically executed on all the threads in the system allowing for transparently threaded calculations.

Splitting refers to the option of making repeated calls to FISH intrinsic or user-defined functions. The special argument prefix operator :: indicates that that argument is an iterable type and its contents will be split so that a separate call is made for each value. The return values from each call are assembled into a list type — in the same order as the original.

As with list operators, this will automatically happen on all threads available if the list is large enough and the function is tagged as being thread-safe. FISH functions created using the fish define command are never considered thread-safe. If function is not thread-safe, splitting will still occur, just sequentially on one thread.

A special class of FISH functions called operators have been added that allow user-written FISH functions to be run on multiple threads when split. FISH operators have more restrictions than normal functions, designed to make them safe to use in a threaded context. However they offer the fastest option available for customized model behavior, especially when used during cycling.

In addition the list type supports filtering. This allows the automatic selection of a sub-set of a list by passing a list of booleans to it instead of an integer index. List elements that have an index corresponding to the index of the true values will be output as a result of filtering, and elements associated with the false values will be ignored. This makes it possible to quickly and efficiently find a sub-list of items that match a given criteria.

A simple example is given below, as an alternative implementation of the history example previously shown. This implementation is much more flexible and much faster:

▸ Calculating the load on a surface using splitting (FLAC3D)

model new
model large-strain off
zone create brick size 1 2 1
zone face skin
zone cmodel assign mohr-coulomb
zone property shear=1e8 bulk=2e8 cohesion=1e5 tension=1e10
zone face apply velocity (0,0,0) range group "South"
zone face apply velocity-y -1e-5 range group "North"
[ad = list(gp.list)(gp.isgroup(::gp.list,"North"))]
fish define load
    load = list.sum(gp.force.unbal(::ad)->y)
end
fish history load
zone history displacement-y position (0,2,0)
model step 1000
plot item create chart-history history '1' vs '2' reversed on

First the

list(gp.list)

statement converts the pointer to the container of all grid points in the model returned by gp.list into a list of all grid points in the model. Then the function gp.isgroup is called and split so that a separate call is made for each grid point in the model and the results returned as a list of booleans.

gp.isgroup(::gp.list,"North")

That result is passed as an argument to the list of grid points, returning a list of grid points that are connected to the surface face group labeled “North”.

The fish functions takes that reduced list of pointers and calls

gp.force.unbal(::ad)

which again makes a separate call for every grid point pointer in the list ad, and returns a list or the results — in this case a list of unbalanced force vectors. The ->y operator applied to that list returns a list of the \(y\)-components of those vectors. Finally the list.sum intrinsic is used to sum all of those values into the final total force.

As a simple, one-line example of this utility, the following inline FISH increments the xx-stresses of all zones by 500:

[ zone.stress(::zone.list)->xx += 500 ]

This line adds a different random value to the \(x\)-component of the position of each grid point in the model:

[ gp.pos(::gp.list)->x ::+= math.random.uniform(gp.num) ]

The math.random.uniform function, if given an argument, will return a list with that many random values between 0.0 and 1.0. The gp.num function will return the number of grid points in the model. The use of the split operator :: in conjunction with the += operator is the only time the split operator can appear outside a function argument, and indicates that the right-hand assignment is itself to be split so that each assignment function call is given a different value.

▸ Calculating the load on a surface using splitting (FLAC2D)

model new
model large-strain off
zone create2d quadrilateral size 2 1
zone face skin
zone cmodel assign mohr-coulomb
zone property shear 1e8 bulk 2e8 cohesion 1e5 tension 1e10
zone face apply velocity (0,0) range group "West"
zone face apply velocity-x -1e-5 range group "East"
[ad = list(gp.list)(gp.isgroup(::gp.list,"East"))]
fish define load
    load = list.sum(gp.force.unbal(::ad)->x)
end
fish history load
zone history displacement-x position (2,0)
model step 1000
plot item create chart-history history '1' vs '2' reversed on

First the

list(gp.list)

statement converts the pointer to the container of all grid points in the model returned by gp.list into a list of all grid points in the model. Then the function gp.isgroup is called and split so that a separate call is made for each grid point in the model and the results returned as a list of booleans.

gp.isgroup(::gp.list,"East")

That result is passed as an argument to the list of grid points, returning a list of grid points that are connected to the surface face group labeled “East”.

The FISH function takes that reduced list of pointers and calls

gp.force.unbal(::ad)

which again makes a separate call for every grid point pointer in the list ad, and returns a list of the results — in this case a list of unbalanced force vectors. The ->x operator applied to that list returns a list of the \(x\)-components of those vectors. Finally the list.sum intrinsic is used to sum all of those values into the final total force.

As a simple, one-line example of this utility, the following inline FISH increments the xx-stresses of all zones by 500:

[ zone.stress(::zone.list)->xx += 500 ]

This line adds a different random value to the \(x\)-component of the position of each grid point in the model:

[ gp.pos(::gp.list)->x ::+= math.random.uniform(gp.num) ]

The math.random.uniform function, if given an argument, will return a list with that many random values between 0.0 and 1.0. The gp.num function will return the number of grid points in the model. The use of the split operator :: in conjunction with the += operator is the only time the split operator can appear outside a function argument, and indicates that the right-hand assignment is itself to be split so that each assignment function call is given a different value.

▸ Calculating the load on a surface using splitting (3DEC)

model new
model large-strain off
block create brick 0 1 0 2 0 1
block zone generate edgelength 0.5
block zone cmodel assign mohr-coulomb
block zone property density 2000 shear=1e8 bulk=2e8 cohesion=1e5 tension=1e10
block gridpoint group 'North' range position-y 2
block gridpoint apply velocity (0,0,0) range position-y 0
block gridpoint apply velocity-y -1e-2 range group 'North'

[ad = list(block.gp.list)(block.gp.isgroup(::block.gp.list,"North"))]

fish define load
    load = list.sum(block.gp.force.reaction.y(::ad))
end

fish history load
block history displacement-y position (0,2,0)
model step 1000
plot item create chart-history history '1' reversed-x on reversed-y on vs '2' 

First, a list is created of all gridpoints at y = 2. This is done by checking the group name of all gridpoints using splitting:

gp.isgroup(::gp.list,"East")

This returns a list of booleans that is then turned into a list of gridpoints using

list(block.gp.list)

A fish functions takes that reduced list of pointers and calls

block.gp.force.reaction.y(::ad)

which makes a separate call for every grid point pointer in the list ad, and returns a list of the results – in this case a list of y-reaction forces. Finally the list.sum intrinsic is used to sum all of those values into the final total force.

As a simple, one-line example of this utility, the following inline FISH increments the xx-stresses of all zones by 500:

[ block.zone.stress.xx(::block.zone.list) += 500 ]

In this case, block.zone.stress.xx acts on all zones in the model and 500 is added to the xx component of stress for each zone.

This line adds a different random value to the x component of the position of each grid point in the model:

[ block.gp.pos.x(::gp.list) ::+= math.random.uniform(block.gp.num) ]

The math.random.uniform function, if given an argument, will return a list with that many random values between 0.0 and 1.0. The block.gp.num function will return the number of grid points in the model. The use of the split operator :: in conjunction with the += operator is the only time the split operator can appear outside a function argument, and indicates that the right-hand assignment is itself to be split so that each assignment function call is given a different value.

FISH Automatic-Create

All the examples above use the default FISH symbol creation mode. As soon as an unknown word is encountered on the left hand side of an assignment, a new global FISH symbol is created. Encountering a new symbol on the right hand side is an error.

For small simple introductory FISH functions like everything in this tutorial, that is fine. But as FISH scripts grow larger and greater numbers of variables are used, it becomes problematic. The number of global variables increases, which increases risk of accidental use of the same name in two different places, which would unknowingly overwrite values. Also, making a typo in a variable name will create a new variable rather than change the value of an old one as intended.

To address this, once a user starts writing FISH of non-trivial size, it is strongly recommended to use the command fish automatic-create off at the start of the data file after model new. Note nearly all example data files supplied with the program do this. This forces explicit declaration of all new variables as either local or global. Local symbols only exist inside the function or operator where they are declared and cannot be referenced outside. Attempt to make all variables local — only create global symbols if necessary.

Further Information

Having explored a range of topics that offer an introductory look at the FISH language, its capabilities, and features, users should be well equipped to engage with the preceding sections of this FISH Scripting Reference—a comprehensive guide to the language, including language rules, statements, intrinsic functions, and examples.