Using Python with FLAC3D

This section gives an example of using a Python program to interact with FLAC3D. In this example we create a simple FLAC3D model and use Python to apply boundary conditions and explore the model response.

First we import the itasca and module with the import statement. The itasca module defines the interaction between Python and FLAC3D. We import the itasca module as the shortened name it.

import itasca as it
it.command("python-reset-state false")
it.command("""
program load module 'zone'
program load guimodule 'zone'
program load module 'body'
program load guimodule 'body'
program load module 'extruder'
program load guimodule 'extruder'
program load module 'sel'
program load module 'zonesel'
program load module 'selpython'
program load module 'wallsel'
program load guimodule 'sel'
program load module 'pfcsel'
program load module 'rblocksel'
program load module 'dfnzone'
program load module 'ballzone'
program load module 'zonepython'
program load module 'wallzone'
""")

We are going to create some zones to show features of the Python scripting. The it.command function is used to issue a series of FLAC3D commands. Always give the command python-reset-state false at the beginning of a Python program. If this command is not given the Python variables and functions are deleted when the FLAC3D model state is reset via the model new or model restore commands.

These commands create zones and set some properties. The triple quotes are used to define a multi-line string.

it.command("""
model new
zone create brick size 10 10 10
zone cmodel assign elastic
zone property density 2950 young 12e9 poisson 0.25
cycle 1
""")

Working with FLAC3D Zones

We can confirm that 1000 zones were created by using the it.zone.count function. Typing the following expression into the IPython terminal prints the number 1000 to the screen.

it.zone.count()
output:
1000

The itasca module defines functions and classes for interacting with the FLAC3D model. Some of these functions return objects,

z = it.zone.find(1)
print(z)
output:
<itasca.zone.Zone object at 0x0000016F9D56E1F8, ID : 1>

Here it.zone.find(1) returns a Zone object for the zone with id 1. This object is assigned to the Python variable z. Most FLAC3D model items are objects (instances of a class) like this. The Zone object has many methods, for example

z.pos()
output:
vec3(( 5.000000e-01, 5.000000e-01, 5.000000e-01 ))

The variable z is a Zone object which represents a single FLAC3D zone. pos is a method of this object which returns the zone centroid.

Note

The pos method returns a length three vector represented as a vec3 object. See vec module for more information about vec3 objects and working with vectors in Python.

print("zone", z.id(), "with model", z.model(), "at", z.pos())

In Python the for statement is used to iterate over sequences of things,

volume_sum = 0.0
for z in it.zone.list():
    volume_sum += z.vol()

In this example the for statement is used to loop over all the FLAC3D zones. During each loop the variable z is a different Zone object. Finally, we check that the sum of the zone volumes is what we expect.

print(volume_sum)
print(z.vol() * it.zone.count())
assert volume_sum == z.vol() * it.zone.count()
output:
1000.0
1000.0

Let’s find a zone near the center of the model

z = it.zone.near((5,5,5))

and confirm its position with the \(pos\) method.

z.pos()
output:
vec3(( 4.500000e+00, 4.500000e+00, 4.500000e+00 ))

Working with FLAC3D Zones Properties

The props method returns the zone constitutive model properties as a Python dictionary object. The dictionary keys are the property names and the values are the property values.

z.props()
output:
{'bulk': 8000000000.0, 'shear': 4800000000.0, 'young': 12000000000.0, 'poisson': 0.25}

To access the individual parts of a dictionary the [] brackets are used with a string key,

z.props()['bulk']
output:
8000000000.0

or the prop method can be used.

z.prop('shear')
output:
4800000000.0

Zone properties can be set with the set_prop method.

z.set_prop('bulk', 8.5e9)
print(z.prop('bulk'))
output:
8500000000.0

There are two places where Python interacts with FLAC3D zones: (i) the functions that are part of the itasca.zone Python module and (ii) the methods of an individual itasca.zone.Zone object. In Python terminology, itasca.zone is a submodule of the itasca module and Zone is a class defined in the itasca.zone module. For brevity we often import the itasca module by the shortened name it. Above, it.zone.count(), it.zone.list() and it.zone.find() are functions defined in the itasca.zone module and z.pos() and z.vol() are Zone object method calls.

Working with FLAC3D gridpoints

Similarly, Python can interact with FLAC3D gridpoints via

gp = it.gridpoint.near((2,2,2))
print(gp.pos())
output:
vec3(( 2.000000e+00, 2.000000e+00, 2.000000e+00 ))

As with the zones we can iterate over all the FLAC3D gridpoints with a Python for statement.

total_mass = 0
for gp in it.gridpoint.list():
    total_mass += gp.mass_gravity()

print(total_mass, z.vol()*1000*z.density())
output:
2949999.9999999995 2950000.0

Working with Structural Elements

Python can interact with structural elements via the itasca.structure Module module. Structures are added with the following command to demonstrate accessing structural elements from Python.

it.command("""
struct cable create by-line (0.0, 5, 5) (10, 5, 5) id=1 segments=4
zone mechanical damping combined
struct cable property cross-sectional-area=2e-3 young=200e9 yield-tension=1.1e20 grout-stiffness=1e10 grout-cohesion=1e20 grout-friction=0.123
struct beam create by-line (0.0, 2, 2) (10, 2, 2) id=2 segments=4
struct beam property young=1.234e9 poisson=0.223 plastic-moment=23.4e4 cross-sectional-area=1.234 moi-y=55.66 moi-z=3458.33
""")

All types of structural elements are stored in a single list accessible via the it.structure.list() function. Unlike other iterators in the Itasca Python interface, this iterator returns objects of a specific type. There are six structural element types:

for s in it.structure.list():
    if type(s) is it.structure.Cable:
        print("Cable element with id: {} and component id: {}".format(s.id(), s.component_id()))
    elif type(s) is it.structure.Beam:
        print("Beam element with Young's modulus: {}".format(s.young()))
output:
Cable element with id: 1 and component id: 1
Cable element with id: 1 and component id: 2
Cable element with id: 1 and component id: 3
Cable element with id: 1 and component id: 4
Beam element with Young's modulus: 1234000000.0
Beam element with Young's modulus: 1234000000.0
Beam element with Young's modulus: 1234000000.0
Beam element with Young's modulus: 1234000000.0

Similarly, the it.structure.find() and it.structure.near() functions can be used to get specific elements.

s1 = it.structure.find(1)
s2 = it.structure.near((0,2,2))

print(s1)
print(s2)
output:
<itasca.structure.Cable object at 0x0000016F9D56E1F8, ID : 1>
<itasca.structure.Beam object at 0x0000016F9D56E2A0, ID : 5>

Structural element nodes and links are accessible via sub-modules:

s_node = it.structure.node.find(1)
s_link = s_node.links()[0]
print("SEL node {} is attached to zone {}".format(s_node.id(), s_link.target().id()))

it.command("struct cable delete")
it.command("struct beam delete")
output:
SEL node 1 is attached to zone 441

Extra variables

FLAC3D Zones, gridpoints and other Itasca model objects have extra variables. This allows some data to be associated with the model object. Python programs can read and write extra variables with the methods extra and set_extra which are defined on all object types.

z.set_extra(1, 1.23)
z.set_extra(2, "a text string")

print(z.extra(1))
print(z.extra(2))
output:
1.23
a text string
gp.set_extra(1, gp.pos())
gp.set_extra(2, 86856527)

print(gp.extra(1))
print(gp.extra(2))
output:
vec3(( 1.000000e+01, 1.000000e+01, 1.000000e+01 ))
86856527

Typically in Python indices start at 0. The extra variables indices are an exception to allow Python it work well with the Itasca software GUI. Extra variables are stored in the model save file. This is a way to associate Python data in the Itasca model state.

Using FLAC3D Groups and Applying Boundary Conditions

Groups are used to create a list of FLAC3D model objects. Often this is used to set a boundary condition or apply a material property to a specific part of the model. Here we create a two groups of zones representing the upper and lower parts of our model using a FLAC3D command.

it.command("""
zone group "lower" range position-z 0 5
zone group "upper" range position-z 5 10
""")

We can query if a zone is part of a group using the Zone group method.

lower_zones, upper_zones = 0, 0

for z in it.zone.list():
    if z.group("default") == "lower":
        lower_zones += 1
        z.set_prop("young", 1.6e10)
    elif z.group("default") == "upper":
        upper_zones += 1

print(lower_zones, upper_zones)
output:
500 500

The group of an object can be set by the set_group method.

Next we fix the gridpoints on the lower surface of the model and apply a force to the gridpoints in the top surface of the model.

lower_gridpoints, upper_gridpoints = 0, 0

for gp in it.gridpoint.list():
    if gp.pos_z() == 0:
        lower_gridpoints += 1
        gp.set_fix(0, True)
        gp.set_fix(1, True)
        gp.set_fix(2, True)
    if gp.pos_z() == 10:
        upper_gridpoints += 1
        gp.set_force_load((1e6,2e6,1e6))

print(lower_gridpoints, upper_gridpoints)
output:
121 121

Next we cycle the model and inspect the model response to this loading.

it.command("""
model save "before_cycling"
model solve
""")


gp_top_middle = it.gridpoint.near((5,5,10))
gp_middle = it.gridpoint.near((5,5,5))
print(gp_top_middle.disp())
print(gp_middle.disp())
output:
vec3(( 5.336922e-03, 1.067808e-02, 7.595570e-04 ))
vec3(( 1.927919e-03, 3.858029e-03, 3.270009e-04 ))
central_zone = it.zone.near((5,5,5))
stress = central_zone.stress()
strain = central_zone.strain()

print(stress)
print()
print(strain)
output:
stens3([[ 1.342748e+05, -4.252898e+04, 1.755947e+06 ],
        [               1.423834e+05, 3.531708e+06 ],
        [ sym.                        1.938853e+06 ]])

stens3([[ -3.214770e-05, -4.383850e-06, 1.828719e-04 ],
        [               -3.123663e-05, 3.678034e-04 ],
        [ sym.                        1.567569e-04 ]])

Note

The stress and strain zone methods returns symmetric tensors as stens3 objects. See stens3 type for more information about symmetric tensor objects and working with tensors in Python.

Running parameter studies

The Python program and variables are not connected to the FLAC3D model state (provided the command python-reset-state false has been given). This allows parameter studies to be run programmatically. In the following example the same model is run with 4 different values of Young’s modulus and the vertical displacement at the top center of the model is printed.

for modulus in [6e9, 8e9, 10e9, 12e9]:
    it.command("""
    model restore 'before_cycling'
    zone prop young {}
    model solve
    """.format(modulus))
    vertical_disp = it.gridpoint.near((5,5,10)).disp_z()
    print("Modulus: {:.01e} Pa, vertical displacement: {:.03e}".format(modulus, vertical_disp))
output:
Modulus: 6.0e+09 Pa, vertical displacement: 1.754e-03
Modulus: 8.0e+09 Pa, vertical displacement: 1.319e-03
Modulus: 1.0e+10 Pa, vertical displacement: 1.056e-03
Modulus: 1.2e+10 Pa, vertical displacement: 8.812e-04

Handling FLAC3D errors

With Python it is possible to catch and handle errors using the built-in Python exception handling. In Python runtime errors are called exceptions. Exceptions can be caught and handled with try, except blocks.

try:
    it.command("""
    zone prop young 0
    model solve
    """)
except Exception as data:
    print("an error occurred", data)

print("Python program execution continues...")

it.command("zone prop young 8e9")  # fix the problem
output:
an error occurred Error in execution - See the Itasca Console for further details.
Python program execution continues...

Inspecting the FLAC3D model geometry

We can get a list of the gridpoints associated with zone z by calling the gridpoints method. First we print the number of gridpoints associated with zone z.

z = it.zone.near((5,5,5))
len(z.gridpoints())

The built-in function len is used in Python to determine the length of a sequence. The following code loops over all the gridpoints of zone z and prints the gridpoint id and position.

for gp in z.gridpoints():
    print("gridpoint with id: {} at {}".format(gp.id(), gp.pos()))
output:
gridpoint with id: 654 at vec3(( 4.002623e+00, 4.005185e+00, 5.001522e+00 ))
gridpoint with id: 655 at vec3(( 5.002571e+00, 4.005177e+00, 5.001162e+00 ))
gridpoint with id: 665 at vec3(( 4.002600e+00, 5.005141e+00, 5.000804e+00 ))
gridpoint with id: 775 at vec3(( 4.003375e+00, 4.006696e+00, 6.001760e+00 ))
gridpoint with id: 666 at vec3(( 5.002574e+00, 5.005145e+00, 5.000444e+00 ))
gridpoint with id: 786 at vec3(( 4.003356e+00, 5.006658e+00, 6.000944e+00 ))
gridpoint with id: 776 at vec3(( 5.003331e+00, 4.006690e+00, 6.001350e+00 ))
gridpoint with id: 787 at vec3(( 5.003333e+00, 5.006662e+00, 6.000533e+00 ))

Note

The string object format method is used to substitute values into a string. The {} parts of the string are replaced by the arguments to the format method. In this case to show the gridpoint id and position.

We can find the zones attached to zone z by calling the adjacent_zones method.

z = it.zone.near((5,5,5))
print("central zone id: {}, position: {}".format(z.id(), z.pos()))
print()
for i, neighbor in enumerate(z.adjacent_zones()):
    print("neighbor zone {} id: {}, position: {}".format(i,
                                                         neighbor.id(),
                                                         neighbor.pos()))
output:
central zone id: 545, position: vec3(( 4.502970e+00, 4.505919e+00, 5.501065e+00 ))

neighbor zone 0 id: 445, position: vec3(( 4.502243e+00, 4.504463e+00, 4.500890e+00 ))
neighbor zone 1 id: 645, position: vec3(( 4.503749e+00, 4.507478e+00, 6.501215e+00 ))
neighbor zone 2 id: 544, position: vec3(( 3.503012e+00, 4.505915e+00, 5.501469e+00 ))
neighbor zone 3 id: 546, position: vec3(( 5.502939e+00, 4.505913e+00, 5.500683e+00 ))
neighbor zone 4 id: 555, position: vec3(( 4.502958e+00, 5.505893e+00, 5.500301e+00 ))
neighbor zone 5 id: 535, position: vec3(( 4.502977e+00, 3.505965e+00, 5.501866e+00 ))

As we expect this Zone has 6 neighbors. The return value of this function is always of length 6. For zones at the boundary of the model the Python None object is returned to indicate the absence of a specific zone neighbor.

z = it.zone.near((0,0,0))
for neighbor in  z.adjacent_zones():
    print(neighbor)
output:
None
<itasca.zone.Zone object at 0x0000016F9D56E150, ID : 101>
None
<itasca.zone.Zone object at 0x0000016F9D56E210, ID : 2>
<itasca.zone.Zone object at 0x0000016F9D56E228, ID : 11>
None

The faces method returns a list of the zone faces as a list of lists of gridpoint objects. The returned list will always have a length of 6 but will contain the None object to indicate an absent face (for zones with less than 6 faces).

for i,face in enumerate(z.faces()):
    print("face {} contains the gridpoints with IDs: {}".format(i, [gp.id() for gp in face]))
output:
face 0 contains the gridpoints with IDs: [2, 5, 3, 1]
face 1 contains the gridpoints with IDs: [4, 6, 8, 7]
face 2 contains the gridpoints with IDs: [1, 3, 6, 4]
face 3 contains the gridpoints with IDs: [7, 8, 5, 2]
face 4 contains the gridpoints with IDs: [3, 5, 8, 6]
face 5 contains the gridpoints with IDs: [1, 4, 7, 2]

Similarly, the face normal vectors are returned by the face_normals method.

for i,normal in enumerate(z.face_normals()):
    print("face {} has normal vector {}".format(i, normal))
output:
face 0 has normal vector vec3(( -0.000000e+00, -0.000000e+00, -1.000000e+00 ))
face 1 has normal vector vec3(( 4.287976e-04, 6.414650e-04, 9.999997e-01 ))
face 2 has normal vector vec3(( -9.999994e-01, -1.540212e-05, 1.067589e-03 ))
face 3 has normal vector vec3(( 9.999998e-01, 8.794389e-06, -6.673297e-04 ))
face 4 has normal vector vec3(( -1.828395e-05, 9.999997e-01, -8.250305e-04 ))
face 5 has normal vector vec3(( 1.284107e-05, -9.999992e-01, 1.252834e-03 ))

The ordering of the faces is the same in all three of these methods. This is the face index which has a value between 0 and 5.

Given a gridpoint we can loop over the zones attached to this gridpoint with the zones method

gp = it.gridpoint.near((2,2,2))
print(gp.pos())
for z in gp.zones():
    print("gp id: {} is connected to zone id: {}, position: {}".format(gp.id(),
                                                                       z.id(),
                                                                       z.pos()))
output:
vec3(( 2.001022e+00, 2.001654e+00, 2.001699e+00 ))
gp id: 267 is connected to zone id: 223, position: vec3(( 2.501218e+00, 2.502141e+00, 2.501761e+00 ))
gp id: 267 is connected to zone id: 222, position: vec3(( 1.501386e+00, 2.502125e+00, 2.502063e+00 ))
gp id: 267 is connected to zone id: 213, position: vec3(( 2.501254e+00, 1.502321e+00, 2.502343e+00 ))
gp id: 267 is connected to zone id: 212, position: vec3(( 1.501450e+00, 1.502314e+00, 2.502649e+00 ))
gp id: 267 is connected to zone id: 123, position: vec3(( 2.500670e+00, 2.501126e+00, 1.501032e+00 ))
gp id: 267 is connected to zone id: 122, position: vec3(( 1.500803e+00, 2.501116e+00, 1.501217e+00 ))
gp id: 267 is connected to zone id: 113, position: vec3(( 2.500698e+00, 1.501265e+00, 1.501386e+00 ))
gp id: 267 is connected to zone id: 112, position: vec3(( 1.500856e+00, 1.501265e+00, 1.501574e+00 ))

Python Type System

Occasionally, the type of a Python variable needs to be determined by a Python program. The following examples demonstrate the Python type system.

print(type(1.0) is int)
print(type(1.0) is float)
print(type("Itasca") is str)
print(type(gp) is it.gridpoint.Gridpoint)
print(type(gp) is it.zone.Zone)
print(type(z) is it.gridpoint.Gridpoint)
print(type(z) is it.zone.Zone)

Python Callback Functions

Python functions can be run at specific points during the FLAC3D calculation sequence. The itasca.set_callback function registers Python functions to be called during cycling while the itasca.remove_callback removes these function callbacks.

i=0
def my_callback(*args):
    global i
    i += 1
    print("in Python callback function.")

it.set_callback("my_callback", -1)
it.command("cycle 5")
print("The Python callback function was called {} times".format(i))

it.remove_callback("my_callback",-1)
i=0
it.command("cycle 5")
print("The Python callback function was called {} times".format(i))
output:
in Python callback function.
in Python callback function.
in Python callback function.
in Python callback function.
in Python callback function.
The Python callback function was called 5 times
The Python callback function was called 0 times

Note

Callback function registration is cleared when the model new or model restore commands are given. Be careful not to register the same Python function to be called more than once per cycle. Use the list fish callback command to see which functions have already been registered.

There are three callback events which have special behavior in Python. The “save”, “restore” and “new” callback events are not reset when the model new or model restore commands are given. These callbacks allow Python programs to manually save or read data associated with a model state. The function it.state_callbacks can be used to see which Python functions are defined at these special callback events.