# Array Style Programming with FLAC3D and NumPy

import functools
import numpy as np
np.set_printoptions(threshold=20)


Note

For the purpose of this tutorial, the amount of data shown in the terminal when a large array is printed has been reduced. The set_printoptions function of the numpy module is used for this. This step is not needed for most use cases.

import itasca as it
it.command("python-reset-state false")
from itasca import zonearray as za
from itasca import gridpointarray as gpa


First, we import the itasca, zonearray, gridpointarray and numpy modules. Note that the zonearray and gridpointarray modules are submodules of the itasca module. To shorten the typing required we import these modules as a shortened names.

In this tutorial we will do the same modeling as the previous example but using the array interface.

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


First we create a simple model.

## Zone array functions

za.pos()

output:
[[ 0.5  0.5  0.5]
[ 1.5  0.5  0.5]
[ 2.5  0.5  0.5]
...,
[ 7.5  9.5  9.5]
[ 8.5  9.5  9.5]
[ 9.5  9.5  9.5]]


Calling the itasca.zonearray.pos function (za.pos()) returns a NumPy array of the zone centroids.

Note

When a large array is printed out in the terminal, only the beginning and ending of the array is shown. The ellipsis shows the gap.

p = za.pos()

type(p)

output:
<class 'numpy.ndarray'>

p.shape

output:
(1000, 3)

it.zone.count()

output:
1000


The array is assigned to the variable p. The shape of the array depends on the number of zones.

x,y,z = p.T

print(x)
print(x.shape)

output:
[ 0.5  1.5  2.5 ...,  7.5  8.5  9.5]
(1000,)


The individual zone centroid components can be unpacked into separate variables. The .T is the transpose operator. p is a multi-dimensional array while x, y and z are scalar arrays.

Following the previous example we will set a different modulus in the upper and lower layers of the model.

z > 5

output:
[False False False ...,  True  True  True]


Above we use the > operator on the array z. The return value is an array of True or False values. The model is 10 m tall, so the value is True if the zone centroid is in the upper half of the model.

upper_zones = z > 5
print((upper_zones).sum())

output:
500


The sum method of the array can be used to count how many zones have a True value in this array. Arrays of True or False values are called masks and can be used to set parts of an array.

modulus = np.zeros_like(x)
modulus[upper_zones] = 1.2e10
lower_zones = np.logical_not(upper_zones)
modulus[lower_zones] = 1.6e10


A new array is created which is the same shape as the x-component of the zone position. The parts of this array which correspond to the zones in the upper half of the model are given a different value from the zones in the lower half.

za.set_prop_scalar("young", modulus)


The itasca.zonearray.set_prop_scalar function (za.set_prop_scalar()) sets the Youngâ€™s modulus of each zone in the model.

## FLAC3D Groups and Mask Arrays

The concept of mask arrays, a True or False value for each zone, is similar to the concept of groups in FLAC3D. It is possible to get a mask array from a FLAC3D group and vice-versa.

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

za.in_group("lower")

output:
[ True  True  True ..., False False False]

za.in_group("lower").sum(), "zones in lower group."

output:
(500, 'zones in lower group.')

from functools import reduce
corner_mask = reduce(np.logical_and, (x<3, y<3, z<3))
print(za.in_group("corner", "geometry").sum(), "zones in corner group.")

output:
27 zones in corner group.


Above, a mask array is created from a group and a group is created from a mask array.

## Gridpoint Array Functions

Next we set the boundary conditions with the gridpointarray module functions.

gpos = gpa.pos()
gx, gy, gz = gpos.T
print(gz)

output:
[  0.   0.   0. ...,  10.  10.  10.]


First, the individual components of the gridpoint positions are assigned to the variables gx, gy and gz.

f = gpa.fixity()
print(f)

output:
[[False False False]
[False False False]
[False False False]
...,
[False False False]
[False False False]
[False False False]]


Next the itasca.gridpointarray.fixity function (gpa.fixity()) is called to get an array describing the current gridpoint fixity.

f[:,][gz==0] = True, True, True
print(f)

output:
[[ True  True  True]
[ True  True  True]
[ True  True  True]
...,
[False False False]
[False False False]
[False False False]]


The f array has the entries which correspond to gridpoints on the lower surface set to True. The values returned by the array interface functions are copies of the data in the FLAC3D model. Just changing the values in is array does not effect the FLAC3D model.

gpa.set_fixity(f)


Finally, the itasca.gridpointarray.set_fixity function (gpa.set_fixity()) is called with the array f to set the gridpoint fixity in the FLAC3D model.

top_gridpoints = gz==10

output:
boundary load applied to 69 gridpoints

fapp = gpa.force_app()
print(fapp)
gpa.set_force_app(fapp)

output:
[[ 0.  0.  0.]
[ 0.  0.  0.]
[ 0.  0.  0.]
...,
[ 0.  0.  0.]
[ 0.  0.  0.]
[ 0.  0.  0.]]


A load is applied to a circular region on the top surface of the model. The load is 1e6 Pa in the middle of the model and falls to zero at a radial distance of 5 m from the middle of the model.

it.command("model solve")


Once the model is solved we can use the zone and gridpoint array interfaces to inspect the model response. First gridpoint displacements are shown:

print("gridpoint displacements:")
print(gpa.disp())

output:
gridpoint displacements:
[[  0.00000000e+00   0.00000000e+00   0.00000000e+00]
[  0.00000000e+00   0.00000000e+00   0.00000000e+00]
[  0.00000000e+00   0.00000000e+00   0.00000000e+00]
...,
[  1.00817672e-05   1.34893133e-05   9.87057263e-05]
[  1.05750220e-05   1.17518780e-05   8.43147596e-05]
[  1.07288134e-05   1.07309207e-05   6.84082122e-05]]

print("gridpoint displacement magnitudes: ")
mag = np.linalg.norm(gpa.disp(), axis=1)
print(mag)

output:
gridpoint displacement magnitudes:
[  0.00000000e+00   0.00000000e+00   0.00000000e+00 ...,   1.00132033e-04
8.57841268e-05   7.00709896e-05]

max_index = np.argmax(mag)
print("Maximum displacement: {} at location {}".format(gpa.disp()[max_index],
gpa.pos()[max_index]))

output:
Maximum displacement: [ -4.52702458e-10   1.86453593e-09   3.48017689e-04] at location [  5.   5.  10.]

print("Vertical displacement along the vertical line x=5, y=5: from z=0 to z=10")
print(gpa.disp()[np.logical_and(gx==5, gy==5)][:,2])

output:
Vertical displacement along the vertical line x=5, y=5: from z=0 to z=10
[  0.00000000e+00   1.32736099e-05   2.90161310e-05   4.73383602e-05
6.85192220e-05   9.30672832e-05   1.29455954e-04   1.72421045e-04
2.22942342e-04   2.81949365e-04   3.48017689e-04]


Next zone stresses are shown.

za.stress()

output:
[[[  6.17465910e+04   1.01416448e+02   4.89994456e+04]
[  1.01416448e+02   6.17465236e+04   4.89997227e+04]
[  4.89994456e+04   4.89997227e+04   3.27999527e+05]]

[[  6.78059670e+04  -2.06730704e+02   3.86946869e+04]
[ -2.06730704e+02   5.38905523e+04   4.87472043e+04]
[  3.86946869e+04   4.87472043e+04   2.89819206e+05]]

[[  7.09115883e+04  -1.28065496e+01   2.76193649e+04]
[ -1.28065496e+01   5.40605685e+04   4.85390854e+04]
[  2.76193649e+04   4.85390854e+04   2.85324217e+05]]

...,
[[  9.55693655e+03  -2.96732631e+03  -1.10777147e+04]
[ -2.96732631e+03   4.08106384e+03   5.84626994e+02]
[ -1.10777147e+04   5.84626994e+02   2.43435143e+04]]

[[ -2.75298024e+02  -3.62867135e+03   6.97528022e+03]
[ -3.62867135e+03  -1.55273155e+03   7.15403465e+03]
[  6.97528022e+03   7.15403465e+03  -2.14997007e+03]]

[[  1.49396407e+02  -4.59852072e+03   8.30887546e+03]
[ -4.59852072e+03   1.49541790e+02   8.30883923e+03]
[  8.30887546e+03   8.30883923e+03  -1.19746482e+04]]]


The itasca.zonearray.stress function (za.stress()) returns the stress tensor in each zone as a multidimensional array. These arrays are symmetric so a more compact representation is also available:

za.stress_flat()

output:
[[  6.17465910e+04   6.17465236e+04   3.27999527e+05   1.01416448e+02
4.89997227e+04   4.89994456e+04]
[  6.78059670e+04   5.38905523e+04   2.89819206e+05  -2.06730704e+02
4.87472043e+04   3.86946869e+04]
[  7.09115883e+04   5.40605685e+04   2.85324217e+05  -1.28065496e+01
4.85390854e+04   2.76193649e+04]
...,
[  9.55693655e+03   4.08106384e+03   2.43435143e+04  -2.96732631e+03
5.84626994e+02  -1.10777147e+04]
[ -2.75298024e+02  -1.55273155e+03  -2.14997007e+03  -3.62867135e+03
7.15403465e+03   6.97528022e+03]
[  1.49396407e+02   1.49541790e+02  -1.19746482e+04  -4.59852072e+03
8.30883923e+03   8.30887546e+03]]


## FLAC3D Model Geometry Array Data

It is sometimes useful to get an array of the IDs of the zones or gridpoints. This can be accomplished with the ids functions of the array modules.

za.ids()

output:
[   1    2    3 ...,  998  999 1000]

gpa.ids()

output:
[   1    2    3 ..., 1329 1330 1331]

The gridpoints attached to each zone are given by the itasca.zonearray.gridpoints
function (za.gridpoints()).
za.gridpoints()

output:
[[   0    1    2 ...,    5    6    7]
[   1    8    4 ...,    7   10   11]
[   8   12    9 ...,   11   14   15]
...,
[1195 1196 1206 ..., 1327 1317 1328]
[1196 1197 1207 ..., 1328 1318 1329]
[1197 1198 1208 ..., 1329 1319 1330]]


The return values are indices into the arrays returned by the gridpointarray module functions. We can use this to confirm that the centroid of the zones is the average of the individual gridpoint locations.

print("average gridpoint locations: ")
print(gpa.pos()[za.gridpoints()].sum(axis=1)/8.0)

output:
average gridpoint locations:
[[ 0.5  0.5  0.5]
[ 1.5  0.5  0.5]
[ 2.5  0.5  0.5]
...,
[ 7.5  9.5  9.5]
[ 8.5  9.5  9.5]
[ 9.5  9.5  9.5]]

print("zone centroids: ")
print(za.pos())

output:
zone centroids:
[[ 0.5  0.5  0.5]
[ 1.5  0.5  0.5]
[ 2.5  0.5  0.5]
...,
[ 7.5  9.5  9.5]
[ 8.5  9.5  9.5]
[ 9.5  9.5  9.5]]


Similarly, we can get the indices of the zones which are attached to each gridpont.

za.gridpoints()

output:
[[   0    1    2 ...,    5    6    7]
[   1    8    4 ...,    7   10   11]
[   8   12    9 ...,   11   14   15]
...,
[1195 1196 1206 ..., 1327 1317 1328]
[1196 1197 1207 ..., 1328 1318 1329]
[1197 1198 1208 ..., 1329 1319 1330]]


The itasca.zonearray.faces function (za.faces()) returns the indices of the gridpoints on each face of each zone.

za.faces()

output:
[[[   1    4    2    0]
[   3    5    7    6]
[   0    2    5    3]
[   6    7    4    1]
[   2    4    7    5]
[   0    3    6    1]]

[[   8    9    4    1]
[   6    7   11   10]
[   1    4    7    6]
[  10   11    9    8]
[   4    9   11    7]
[   1    6   10    8]]

[[  12   13    9    8]
[  10   11   15   14]
[   8    9   11   10]
[  14   15   13   12]
[   9   13   15   11]
[   8   10   14   12]]

...,
[[1196 1207 1206 1195]
[1316 1327 1328 1317]
[1195 1206 1327 1316]
[1317 1328 1207 1196]
[1206 1207 1328 1327]
[1195 1316 1317 1196]]

[[1197 1208 1207 1196]
[1317 1328 1329 1318]
[1196 1207 1328 1317]
[1318 1329 1208 1197]
[1207 1208 1329 1328]
[1196 1317 1318 1197]]

[[1198 1209 1208 1197]
[1318 1329 1330 1319]
[1197 1208 1329 1318]
[1319 1330 1209 1198]
[1208 1209 1330 1329]
[1197 1318 1319 1198]]]


Finally, the itasca.zonearray.neighbors function (za.neighbors()) returns the indices of the zones which are adjacent to the faces of each zone. These functions give an index of -1 for absent faces, gridpoints or neighbors.

print(za.neighbors())
print()
exterior_zone_mask = [(v==-1).any() for v in za.neighbors()]
print(n_exterior, "zones on the model exterior")

N = 10
assert n_exterior == 2*N*N + 2*(N-2)*N + 2*(N-2)*(N-2)

output:
[[ -1 100  -1   1  10  -1]
[ -1 101   0   2  11  -1]
[ -1 102   1   3  12  -1]
...,
[897  -1 996 998  -1 987]
[898  -1 997 999  -1 988]
[899  -1 998  -1  -1 989]]

488 zones on the model exterior
`

## Example File

The source code for this example is flac3d_array_interface.py