3DEC

Working with FISH

This section is intended for people who have run the program (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 the program running interactively. The short programs may be typed directly into the command line, accessed via the i Console pane. After running an example, give the 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 called (program call) in the console.

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

Defining a FISH Function

fish define fname
    abc = 22 * 3 + 5
end

Note

In examples orchid is used to color 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 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> 3dec> 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 the program 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> 3dec> 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. 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 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

will appear on the command prompt. Both fname and abc have been executed using inline FISH syntax, which is described below. Once the symbol abc is defined, we can now refer to it in the program 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 or the return statement must be return used, 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
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 way 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. Another way is to exit the function using the 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. 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:

[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 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
[hh = 22]
fish define abc
    abc = hh * 3 + 5
end
[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 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' reverse 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 provieded a grid point memory address (or pointer). The address is found with the function get_ad, which uses the FISH grid point 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 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 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 grid points, there are a plethora of intrinsics available for use in FISH. Zone-specific intrinsics are given here and structural element intrinsics are given here.

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' reverse 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 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 the program data file can make use of user-defined FISH names:

Wherever a value is expected in a the program command, you may substitute the name of a FISH variable or function using the inline FISH notation []

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 the 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, 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
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 the program commands as shown in the following example:

Using symbolic variables in the program 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).

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 prop bulk
block 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 block 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
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 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.

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, gridpoints, structural element nodes, etc.) using the loop 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). A practical use of the loop foreach construct is to install a nonlinear initial distribution of elastic moduli in a the program 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
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'
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'

Again, you can verify correct operation of the function by printing or plotting shear and bulk moduli.

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.

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. 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. We assume 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 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. The variables block.zone.pos(pnt) and block.zone.prop(pnt, 'young') are zone intrinsics. Here, we set properties directly from within a FISH function, rather than with a block 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 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:

loop foreach local pnt zone.list
loop foreach local pnt block.zone.list

or

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: 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 the program model components or other internal the program 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. :flag3`reference fixed`

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. You should experiment with different test symbols (e.g., replace > with <).

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

command

endcommand

There are two main reasons for eliciting the 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, we can control a complete the program 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 the program 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
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)]
model new
block create brick 0 10 0 3 0 5
fish define place_cables(num,segs)
    loop local n (1,num)
        local z_d = float(n) - 0.5
        command
            struct 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 block gridpoint apply-remove 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 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'
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 rest '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
            struct cable create by-line 0.0 0.5 [z_d] 7.0 0.5 [z_d] ...
                                   segments [segs] 
            struct cable create by-line 0.0 1.5 [z_d] 7.0 1.5 [z_d] ...
                                   segments [segs] 
            struct cable create by-line 0.0 2.5 [z_d] 7.0 2.5 [z_d] ...
                                   segments [segs] 
            struct 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'

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.

An 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 mathmatical operators, when acting on a list, will instead apply the operator to each element in the list separately. The access operator -> will do this as well, returning a list of the result of -> 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 another general FISH key used to insert a value in the map. 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. You can 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 functions you define. By using the special argument prefix operator ::, you indicate that that argument is an iterable type and it’s 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 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 that 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. The true values will be kept, and the false values will be removed. 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

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' reverse on
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 dens 2000 shear=1e8 bulk=2e8 cohesion=1e5 tension=1e10
block gridpoint apply velocity (0,0,0) range pos-y 0
block gridpoint apply velocity-y -1e-2 range pos-y 2

fish define make_list
  ad = list
  loop foreach gp block.gp.list
    if block.gp.pos.y(gp) > 1.99
       ad('end') = gp  ; append to list
    endif
  endloop
end
[make_list]

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 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 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 one line example of this utility you can type in yourself, 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.

First, a list is created of all gridpoints at y = 2. This is done simply by looping through all gridpoints and checking their coordinates, and then adding to the list of the y-coordinate = 2. You can append to a list by using :end as shown:

ad('end') = gp

A second 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 one line example of this utility you can type in yourself, 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 your FISH scripts get larger and larger and introduce more and more variables it becomes problematic. The number of global variables increase to the point where you might accidentally use the same name in two different places and unknowingly overwrite values. Also you might make a typo in the name and not realize you created a new variable rather than change the value of an old one.

To address this, once you start writing FISH of non-trivial size, we strongly recommend you issue the command fish automatic-create off a the start of the data file after model new. You will notice that nearly all example data files in the manual do this. This forces you to explicitly declare all new variables as either local or global. Local symbols only exist inside the function or operator they are declared in, and cannot be referenced outside. Attempt to make all variables local – only create global symbols if necessary.

Further Information

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