FLAC3D Modeling • Tutorials

Tutorial: Working with FISH

This section is intended for people who have run FLAC3D (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, you should try them out with FLAC3D running interactively. The short programs may be typed directly into the command line, accessed via the iConsole pane. After running an example, give the FLAC3D command model new, to “wipe the slate clean,” ready for the next example. Alternatively, the 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 program called in the console.

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

Defining a FISH Function

fish define fname
    abc = 22 * 3 + 5
end

Note

We will use purple courier boldface to identify user-defined FISH functions and declared variables; FISH statements are similar, but bold and darker.

Note that the command prompt changes to Def> after the first line has been typed in; it changes back to the usual prompt when the end statement is entered. This change in prompt lets you know whether you are using commands or if you are defining a FISH function. All lines following the fish define statement are taken as part of the definition of a FISH function (until the end statement is entered). However, if you type in a line that contains an error (e.g., you type the = sign on an empty line), then you will get the Flac3d> prompt 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 FLAC3D data file not containing FISH. We will describe this process later; for now, we’ll continue to work from the command line. Assuming that you typed in the preceding lines without error, and that you now see the Flac3d> prompt, you can “execute” the function named fname defined in the example above by typing the line

@fname

This line causes the arithmetic operations defined in the function to be executed. This 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 two objects: a function object that can be called at the user’s discretion, and a variable called abc, where abc = 71 after the function has been executed. One can see both the fname FISH function and the variable abc in the global symbols control set. In addition, one can print the value of abc to the command line by invoking an inline FISH expression via square brackets:

[abc]

The message

71 (integer)

will appear on the command prompt. The function fname can also be executed with the inline FISH syntax as outlined below. Once the symbol abc is defined, we can now refer to it in many ways using FLAC3D commands. A FISH function can have any name, but in order for a value to be returned by the function, the function name should be identical to a value name updated inside the function, as outlined below.

Variables

We now type a slightly different program (using the model new command to erase the old function and reset the model state).

Using a variable

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

Here we introduce a “variable,” hh, which is given the value of 22 and then used in the next line. Notice that the FISH function name has been changed to abc, identical to the FISH variable defined in the function. If we execute this function via inline FISH (i.e., [abc]), then exactly the same output as in the previous case appears. However, we now have 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.

The mechanism of naming a FISH function to be identical to a variable set inside the function allows one to provide 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.

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. Now type the following lines without giving the model new command, since we want to keep our 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.

Though we highly recommend using the inline FISH syntax to manipulate FISH variables, an alternate syntax to achieve the same effect is given below:

fish set @abc = 0 @hh = 0
list @hh
list @abc
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 a test of your understanding, type in the slightly modified sequence shown in the next example and figure out why the displayed answers are different.

Test your understanding of function and variable names

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

Using FISH for Custom Calculations

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

Capturing the history of a FISH variable

model new
model largestrain off
zone create brick size 1 2 1
zone cmodel assign mohr-coulomb
zone property shear=1e8 bulk=2e8 cohes=1e5 tens=1e10
zone gridpoint fix velocity range position-y 0.0
zone face apply velocity-y -1e-5 range position-y 2.0
fish define get_ad
    ad1 = gp.near(0,2,0)
    ad2 = gp.near(1,2,0)
    ad3 = gp.near(0,2,1)
    ad4 = gp.near(1,2,1)
end
@get_ad
fish define load
    load = gp.force.unbal.y(ad1) + gp.force.unbal.y(ad2) + gp.force.unbal.y(ad3) + gp.force.unbal.y(ad4)
end
fish history load
zone history displacement-y position (0,2,0)
model step 1000
plot item create chart-history history 1 vs 2 reverse on

Note that the FISH variable load is equal to the sum of four other variables, gp.force.unbal.y(index). The FISH grid variable gp.force.unbal (which may be accessed by component or as a vector) 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.y provides the \(y\)-component of the unbalanced gridpoint force; each instance of gp.force.unbal must be followed by a gridpoint memory address (or pointer). The address is found with the function get_ad, which uses the FISH gridpoint intrinsic gp.near to find 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 model update-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 FLAC3D 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 gridpoints, there are a plethora of intrinsics available for use in FISH. Zone-specific intrinsics are given here and structural element intrinsics are given here. A catalog of general intrinsics is given here. 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 now we must discuss one more way a FLAC3D data file can make use of user-defined FISH names:

Wherever a value is expected in a FLAC3D command, you may substitute the name of a FISH variable or function using inline FISH notation ([]) or prefixed by @.

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 FLAC3D 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, let us assume that we know the Young’s modulus and Poisson’s ratio of a material. 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
model largestrain off
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 FLAC3D commands as shown in the following example:

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 prop bulk
zone list prop 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).


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 control set, 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

We now examine ways 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 loop and 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
model largestrain off
fish define xxx
    sum  = 0
    prod = 1
    loop n (1,10)
        sum  = sum  + n
        prod = prod * n
    end_loop
    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 exit 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 sequence of the loop.

It is important to note that this formulation of looping is different from a for loop in most high-level programming languages. For instance, one cannot easily control the ending condition (i.e., loop from 1 to 10 excluding 10) or the incrementing mechanism (i.e., loop from 1 to 10 by twos or loop backward). A standard for loop is also available in FISH to provide for additional loop control.

Traditional for loop in FISH

model new
model largestrain off
fish define xxx
    sum  = 0
    prod = 1
    loop for (n = 1, n <= 10, n = n + 1)
        sum  = sum  + n
        prod = prod * n
    end_loop
    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, gridpoints, structural element nodes, etc.) using the loop foreach construct. In this case, a container of objects must be given by a FISH intrinsic such as zone.list. A practical use of the loop foreach construct is to install a nonlinear initial distribution of elastic moduli in a FLAC3D grid. 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. We write a FISH function to install appropriate values of bulk and shear modulus in the grid, as in this example:

Applying a nonlinear initial distribution of moduli

model new
model largestrain off
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.z(pnt)
        y_mod = y_zero + cc * math.sqrt(z_depth)
        zone.prop(pnt,'young') = y_mod
    end_loop
end
@install(1e7,1e8)
zone property poisson 0.25
plot item create zone contour property name 'young'

Again, you can 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 c 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. We assume that the datum (or ground surface reference point) is at \(z\) = 0. The variables zone.pos.z(pnt) and zone.prop(pnt, 'young') are zone intrinsics. (Recall that we talked about the gridpoint intrinsic gp.force.unbal earlier.) Here, we set properties directly from within a FISH function, rather than with a zone property command as in an earlier example.

Having seen several examples of FISH functions, let’s briefly examine the question of FISH syntax and style. A complete FISH statement must occupy one line; there are no continuation lines. Formulas may be broken into multiple lines for ease of presentation using temporary variables. The following example shows how this can be done:

Splitting lines

model new
model largestrain off
fish define long_sum  ;example of a sum of many things
    local temp = v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10
    long_sum = temp + 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:

loop foreach local pnt zone.list

of

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. You may have 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 (and growing) types: array, Boolean, integer, matrix, pointer, real, string, structure, tensor, 2D vector, or 3D vector. 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
model largestrain off
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
end
@types
fish list

The resulting screen display looks like this:

           Name    Value
           ------- --------------------
(function) types 0 (integer)
           v1    2 (integer)
           v2    3.400000000000000e+00 (real)
           v3    'Have a nice day' (string)
           v4    6.800000000000000e+00 (real)
           v5    'Have a nice day, old chap' (string)
           v6    (1.000000000000000e+00,2.000000000000000e+00,3.000000000000000e+00) (vector3)
           v7    3 x 1 (matrix)
           v8    true (boolean)

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 FLAC3D model components or other internal FLAC3D 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. [CS: the link was kind of dumb, and in current doc set can’t be built]The topic can get quite complicated, but it is fully explained in -used to link to section 2.3 and 2.3.1 of fish reference here-.

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 a1 test a2 then

else

endif

These statements allow conditional execution of FISH function segments; else and then are optional. The item test consists of one of the following symbols or symbol pairs:

=     #     >     <     >=     <=

The meanings are standard except for #, which means “not equal.” The items expr1 and expr2 are any valid expressions or single variables. If the test is true, then the statements immediately following if are executed until else or endif is encountered. If the test is false, 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
model largestrain off
fish define abc(xx)
    if xx > 0 then
        abc = 33
    else
        abc = 11
    end_if
end
[abc(1)]
[abc(-1)]

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

Until now, our FISH functions have been invoked from FLAC3D, either by using the sqaure brackets [] of inline FISH, by giving the function name prepended with the the @ character, or by using the fish list command. It is also possible to do the reverse, to give FLAC3D commands from within FISH functions. Most valid FLAC3D commands can be embedded between the following FISH statements:

command

endcommand

There are two main reasons for eliciting FLAC3D 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, we can control a complete FLAC3D run with FISH. As an illustration of the first use of the command endcommand statement, we can write a FISH function to install a number of cable elements at different depths in a material. 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, with FISH, we can elicit FLAC3D commands from within a loop and assign the location of the cable ends automatically during the loop, as illustrated in this example:

Automated placing of cable elements

model new
model largestrain off
zone create brick size 10 3 5
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, you should 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, we use variable z_d as a parameter in the function place_cables; z_d is the \(z\)-coordinate 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.

We can modify this example to perform a construction sequence of excavation and installation of cables. This illustrates the second use of command endcommand. We use the zone gridpoint free and model solve commands to “excavate” the boundary plane at \(x\) = 0 in five steps. At the end of each step, we install a row of three cables and then excavate the next section of the boundary.

Sequence of excavation and cable placement

model new
zone create brick size 10 3 5
zone cmodel assign mohr-coulomb
zone property bulk 1e8 shear 0.3e8 friction 35
zone property cohesion 1e3 tension 1e3
zone initialize density 1000
model gravity 0 0 -10
zone gridpoint fix velocity-x range position-z 0.0
zone gridpoint fix velocity-y range position-z 0.0
zone gridpoint fix velocity-z range position-z 0.0
zone gridpoint fix velocity-y range position-y 0.0
zone gridpoint fix velocity-y range position-y 3.0
zone gridpoint fix velocity-x range position-x 0.0
zone gridpoint fix velocity-x range position-x 10.0
model largestrain on
model history mechanical unbalanced-maximum
model solve
model save 'cab_str'
zone gridpoint initialize displacement-x 0
zone gridpoint initialize displacement-y 0
zone gridpoint initialize displacement-z 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 - float(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 grout-stiffness 2e10 grout-cohesion 1e10 grout-perimeter 1.0
        end_command
    end_loop
end
@place_cables(5,7)
model save 'cab_end'

Arrays and Maps

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 two containers to use in these circumstances, termed arrays and maps.

An array holds a list of FISH variables of any type that can be looped over or accessed by the integer index of the element of the array. Arrays can be multidimensional and do not resize dynamically. The simple example below shows how one can create an array of integers and then sum the values.

Array example

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

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

A map, on the other hand, is an associative container, meaning that one can access the members of a map by an integer or string used to insert a value in the map. Maps can dynamically be resized and added to one another (appending maps together), and are the preferred constructs for storing lists of FISH variables for later access.

Map example

model new
model largestrain off
fish define map_operation
    ;create and populate a map with products of 2
    my_map = map(1,2)
    loop local n(2,10)
        map.add(my_map,n,2*n)
    end_loop
      
    ;compute the sum and product of elements in the map
    sum = 0
    prod = 1
    loop foreach n my_map
        sum = sum + n
        prod = prod * n
    end_loop
    io.out('The sum is ' + string(sum) + ' and the product is ' + string(prod))
end
@map_operation

Unlike with arrays, maps can be looped through using the loop foreach construct. In this case, n is the value held in each map entry, not the integer name of the object in the map. Likewise, instead of using integers to insert objects into the map, one could use strings such as first, second, etc. This allows one to easily and efficiently store and access FISH variables by a user-defined name.

Further Information

We have now covered some aspects of the FISH language and how it interacts with FLAC3D. A complete guide to the language, including language rules, statements, intrinsic functions, and examples, is provided in the FISH Scripting section of this Help file.