Parametric FEM simulations using JuliaFEM and Gmsh

The JuliaFEM project develops open-source software for reliable, scalable, distributed Finite Element Method. Gmsh is an open-source 3D finite element mesh generator with a built-in CAD engine and post-processor. In this blog post, I will demonstrate how to create a simple geometry, mesh it using Gmsh Julia API and transfer the mesh after that to a format JuliaFEM understands. Because the mesh is created programmatically, it is possible to parametrize to make an automatic workflow. Further possibilities are then to attach this automated workflow to, for example, optimization loop. {% include_relative notebook.html %}

Use Gmsh to create model.

In [1]:
import Gmsh: gmsh
gmsh.initialize()
gmsh.option.setNumber("General.Terminal", 1)
gmsh.model.add("plate_with_hole")
In [2]:
lc = 0.5
gmsh.model.geo.addPoint(0.0, 0.0, 0.0, lc, 1)
gmsh.model.geo.addPoint(1.0, 0.0, 0.0, lc, 2)
gmsh.model.geo.addPoint(5.0, 0.0, 0.0, lc, 3)
gmsh.model.geo.addPoint(5.0, 5.0, 0.0, lc, 4)
gmsh.model.geo.addPoint(0.0, 5.0, 0.0, lc, 5)
gmsh.model.geo.addPoint(0.0, 1.0, 0.0, lc, 6)
gmsh.model.geo.addLine(2, 3, 1)
gmsh.model.geo.addLine(3, 4, 2)
gmsh.model.geo.addLine(4, 5, 3)
gmsh.model.geo.addLine(5, 6, 4)
gmsh.model.geo.addCircleArc(6, 1, 2, 5)
gmsh.model.geo.addCurveLoop([3, 4, 5, 1, 2], 1)
gmsh.model.geo.addPlaneSurface([1], 1)
gmsh.model.geo.extrude([(2,1)], 0, 0, 0.3)
Out[2]:
7-element Array{Tuple{Int32,Int32},1}:
 (2, 32)
 (3, 1) 
 (2, 15)
 (2, 19)
 (2, 23)
 (2, 27)
 (2, 31)
In [3]:
gmsh.model.addPhysicalGroup(2, [19], 1)
gmsh.model.addPhysicalGroup(2, [31], 2)
gmsh.model.addPhysicalGroup(2, [15], 3)
gmsh.model.addPhysicalGroup(2, [27], 4)
gmsh.model.addPhysicalGroup(2, [32], 5)
gmsh.model.addPhysicalGroup(2, [1], 6)
gmsh.model.addPhysicalGroup(3, [1], 1)
Out[3]:
1
In [4]:
gmsh.model.setPhysicalName(2, 1, "LEFT")
gmsh.model.setPhysicalName(2, 2, "RIGHT")
gmsh.model.setPhysicalName(2, 3, "TOP")
gmsh.model.setPhysicalName(2, 4, "DOWN")
gmsh.model.setPhysicalName(2, 5, "FRONT")
gmsh.model.setPhysicalName(2, 6, "BACK")
gmsh.model.setPhysicalName(3, 1, "PLATE")
In [5]:
gmsh.model.geo.synchronize()
gmsh.model.mesh.generate(3)
Info    : Meshing 1D...
Info    : Meshing curve 1 (Line)
Info    : Meshing curve 2 (Line)
Info    : Meshing curve 3 (Line)
Info    : Meshing curve 4 (Line)
Info    : Meshing curve 5 (Circle)
Info    : Meshing curve 7 (Line)
Info    : Meshing curve 8 (Line)
Info    : Meshing curve 9 (Circle)
Info    : Meshing curve 10 (Line)
Info    : Meshing curve 11 (Line)
Info    : Meshing curve 13 (Line)
Info    : Meshing curve 14 (Line)
Info    : Meshing curve 18 (Line)
Info    : Meshing curve 22 (Line)
Info    : Meshing curve 26 (Line)
Info    : Done meshing 1D (0.00172 s)
Info    : Meshing 2D...
Info    : Meshing surface 1 (Plane, Delaunay)
Info    : Meshing surface 15 (Surface, MeshAdapt)
Info    : Meshing surface 19 (Surface, MeshAdapt)
Info    : Meshing surface 23 (Surface, MeshAdapt)
Info    : Meshing surface 27 (Surface, MeshAdapt)
Info    : Meshing surface 31 (Surface, MeshAdapt)
Info    : Meshing surface 32 (Plane, Delaunay)
Info    : Done meshing 2D (0.016156 s)
Info    : Meshing 3D...
Info    : 3D Meshing 1 volumes with 1 connected components
Info    : Tetrahedrization of 320 points in 0.005657 seconds
Info    : Reconstructing mesh...
Info    :  - Creating surface mesh
Info    :  - Identifying boundary edges
Info    :  - Recovering boundary
Info    :  - Added 4 Steiner points
Info    : Done reconstructing mesh (0.019033 s)
Info    : Found region 1
Info    : 0 points created - worst tet radius 0.984173 (points removed 0 0)
Info    : 3D point insertion terminated (316 points created):
Info    :  - 0 Delaunay cavities modified for star shapeness
Info    :  - 0 points could not be inserted
Info    :  - 830 tetrahedra created in 3.7e-05 sec. (22432432 tets/s)
Info    : Done meshing 3D (0.034424 s)
Info    : Optimizing 3D mesh...
Info    : Optimizing volume 1
Info    : Optimization starts (volume = 7.27039) with worst = 0.300757 / average = 0.715182:
Info    : 0.00 < quality < 0.10 :         0 elements
Info    : 0.10 < quality < 0.20 :         0 elements
Info    : 0.20 < quality < 0.30 :         0 elements
Info    : 0.30 < quality < 0.40 :        10 elements
Info    : 0.40 < quality < 0.50 :        18 elements
Info    : 0.50 < quality < 0.60 :        11 elements
Info    : 0.60 < quality < 0.70 :       165 elements
Info    : 0.70 < quality < 0.80 :       608 elements
Info    : 0.80 < quality < 0.90 :        18 elements
Info    : 0.90 < quality < 1.00 :         0 elements
Info    : No ill-shaped tets in the mesh :-)
Info    : 0.00 < quality < 0.10 :         0 elements
Info    : 0.10 < quality < 0.20 :         0 elements
Info    : 0.20 < quality < 0.30 :         0 elements
Info    : 0.30 < quality < 0.40 :        10 elements
Info    : 0.40 < quality < 0.50 :        18 elements
Info    : 0.50 < quality < 0.60 :        11 elements
Info    : 0.60 < quality < 0.70 :       165 elements
Info    : 0.70 < quality < 0.80 :       608 elements
Info    : 0.80 < quality < 0.90 :        18 elements
Info    : 0.90 < quality < 1.00 :         0 elements
Info    : Done optimizing 3D mesh (0.00118 s)
Info    : 318 vertices 1547 elements

Nodes can be extracted using getNodes().

In [6]:
node_ids, node_coords, _ = gmsh.model.mesh.getNodes()
length(node_ids), length(node_coords)
Out[6]:
(318, 954)
In [7]:
node_ids[1:3], node_coords[1:9]
Out[7]:
(UInt64[0x000000000000013d, 0x0000000000000001, 0x0000000000000002], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 5.0, 0.0, 0.0])

While it's not strictly necessary, let's create dictionary of nodes.

In [8]:
nodes = Dict(node_ids[i] => node_coords[3*(i-1)+1:3*(i-1)+3] for i=1:length(node_ids))
nodes
Out[8]:
Dict{UInt64,Array{Float64,1}} with 318 entries:
  0x0000000000000120 => [4.14173, 2.16239, 0.3]
  0x0000000000000132 => [1.63987, 3.86227, 0.3]
  0x000000000000000b => [1.5, 0.0, 0.0]
  0x0000000000000086 => [2.14902, 4.58079, 0.0]
  0x000000000000009e => [4.68156, 1.73755, 0.0]
  0x00000000000000a0 => [3.46932, 2.44776, 0.0]
  0x00000000000000d7 => [1.60496, 2.7289, 0.3]
  0x000000000000001d => [3.5, 5.0, 0.0]
  0x0000000000000083 => [0.491192, 4.50977, 0.0]
  0x00000000000000f9 => [4.58979, 2.13984, 0.3]
  0x00000000000000cf => [4.13702, 4.13374, 0.3]
  0x000000000000012c => [3.912, 1.72834, 0.3]
  0x00000000000000ad => [3.09815, 1.27928, 0.0]
  0x0000000000000121 => [3.09815, 1.27928, 0.3]
  0x000000000000004a => [5.0, 1.5, 0.3]
  0x00000000000000c9 => [1.2428, 1.83259, 0.3]
  0x00000000000000b0 => [0.68916, 1.03144, 0.0]
  0x0000000000000039 => [0.0, 3.5, 0.3]
  0x000000000000001f => [2.5, 5.0, 0.0]
  0x000000000000011d => [2.88243, 4.03622, 0.3]
  0x000000000000013e => [0.0, 0.0, 0.3]
  0x0000000000000046 => [4.0, 0.0, 0.3]
  0x0000000000000021 => [1.5, 5.0, 0.0]
  0x00000000000000fc => [1.9895, 3.70152, 0.3]
  0x0000000000000072 => [1.43943, 0.480031, 0.0]
  ⋮                  => ⋮

Next elements. For that, we extract physical groups and entities attached to them.

In [9]:
physical_groups = gmsh.model.getPhysicalGroups()
for (dim, tag) in physical_groups
    entities = gmsh.model.getEntitiesForPhysicalGroup(dim, tag)
    physical_name = gmsh.model.getPhysicalName(dim, tag)
    println("($dim, $tag) => $physical_name, entities: ", join(entities, ", "))
end
(2, 1) => LEFT, entities: 19
(2, 2) => RIGHT, entities: 31
(2, 3) => TOP, entities: 15
(2, 4) => DOWN, entities: 27
(2, 5) => FRONT, entities: 32
(2, 6) => BACK, entities: 1
(3, 1) => PLATE, entities: 1

For example, elements in set LEFT can be accessed using getElements, where the first argument is dimension (2 = 2d), and the second argument is the tag of entity.

In [10]:
element_types, element_ids, element_connectivity = gmsh.model.mesh.getElements(2, 19)
Out[10]:
(Int32[2], Array{UInt64,1}[[0x0000000000000123, 0x0000000000000124, 0x0000000000000125, 0x0000000000000126, 0x0000000000000127, 0x0000000000000128, 0x0000000000000129, 0x000000000000012a, 0x000000000000012b, 0x000000000000012c, 0x000000000000012d, 0x000000000000012e, 0x000000000000012f, 0x0000000000000130, 0x0000000000000131, 0x0000000000000132]], Array{UInt64,1}[[0x0000000000000007, 0x0000000000000004, 0x0000000000000037, 0x0000000000000004, 0x0000000000000024, 0x0000000000000037, 0x000000000000002a, 0x0000000000000005, 0x0000000000000008, 0x000000000000002a  …  0x000000000000003b, 0x0000000000000028, 0x000000000000003c, 0x000000000000003b, 0x0000000000000029, 0x000000000000002a, 0x000000000000003c, 0x000000000000003c, 0x000000000000002a, 0x000000000000003d]])
In [11]:
element_types
Out[11]:
1-element Array{Int32,1}:
 2
In [12]:
for i in element_types
    println("$i = ", gmsh.model.mesh.getElementProperties(i))
end
2 = ("Triangle 3", 2, 1, 3, [0.0, 0.0, 1.0, 0.0, 0.0, 1.0])

Like with nodes, let's create auxiliary dictionaries to make further processing of mesh easier.

In [13]:
left_elements = Dict(element_ids[1][i] => element_connectivity[1][3*(i-1)+1:3*(i-1)+3] for i=1:length(element_ids[1]))
Out[13]:
Dict{UInt64,Array{UInt64,1}} with 16 entries:
  0x0000000000000123 => UInt64[0x0000000000000007, 0x0000000000000004, 0x000000…
  0x0000000000000129 => UInt64[0x0000000000000037, 0x0000000000000025, 0x000000…
  0x0000000000000130 => UInt64[0x0000000000000028, 0x000000000000003c, 0x000000…
  0x0000000000000132 => UInt64[0x000000000000003c, 0x000000000000002a, 0x000000…
  0x000000000000012f => UInt64[0x000000000000003a, 0x0000000000000028, 0x000000…
  0x0000000000000126 => UInt64[0x000000000000002a, 0x0000000000000008, 0x000000…
  0x000000000000012a => UInt64[0x0000000000000025, 0x0000000000000039, 0x000000…
  0x000000000000012b => UInt64[0x0000000000000026, 0x0000000000000027, 0x000000…
  0x000000000000012c => UInt64[0x0000000000000026, 0x000000000000003a, 0x000000…
  0x0000000000000128 => UInt64[0x0000000000000025, 0x0000000000000026, 0x000000…
  0x0000000000000124 => UInt64[0x0000000000000004, 0x0000000000000024, 0x000000…
  0x000000000000012e => UInt64[0x0000000000000028, 0x0000000000000029, 0x000000…
  0x0000000000000131 => UInt64[0x0000000000000029, 0x000000000000002a, 0x000000…
  0x0000000000000127 => UInt64[0x0000000000000024, 0x0000000000000025, 0x000000…
  0x0000000000000125 => UInt64[0x000000000000002a, 0x0000000000000005, 0x000000…
  0x000000000000012d => UInt64[0x000000000000003a, 0x0000000000000027, 0x000000…

We do the same thing for all physical groups. A small function is written to make task easier.

In [14]:
function elements_to_dict(dim, tag)
    element_types, element_ids, element_connectivity = gmsh.model.mesh.getElements(dim, tag)
    length(element_types) == 1 || error("Only one element type / entity supported.")
    nelements = length(element_ids[1])
    nnodes = length(element_connectivity[1]) ÷ nelements
    elements = Dict(element_ids[1][i] => element_connectivity[1][nnodes*(i-1)+1:nnodes*i] for i=1:nelements)
end

right_elements = elements_to_dict(2, 31)
top_elements = elements_to_dict(2, 15)
down_elements = elements_to_dict(2, 27)
front_elements = elements_to_dict(2, 32)
back_elements = elements_to_dict(2, 1)
plate_elements = elements_to_dict(3, 1);
In [15]:
println("Number of nodes in model (total) = ", length(nodes))
println("Number of elements in physical group LEFT  = ", length(left_elements))
println("Number of elements in physical group RIGHT = ", length(right_elements))
println("Number of elements in physical group TOP   = ", length(top_elements))
println("Number of elements in physical group DOWN  = ", length(down_elements))
println("Number of elements in physical group FRONT = ", length(front_elements))
println("Number of elements in physical group BACK  = ", length(back_elements))
println("Number of elements in physical group PLATE = ", length(plate_elements))
Number of nodes in model (total) = 318
Number of elements in physical group LEFT  = 16
Number of elements in physical group RIGHT = 20
Number of elements in physical group TOP   = 20
Number of elements in physical group DOWN  = 16
Number of elements in physical group FRONT = 270
Number of elements in physical group BACK  = 270
Number of elements in physical group PLATE = 830

Now the mesh data is in dictionaries. What is left, is to create Mesh object and then build the model and run analysis.

In [16]:
using JuliaFEM
In [17]:
mesh = Mesh()
Out[17]:
Mesh(Dict{Int64,Array{Float64,1}}(), Dict{Symbol,Set{Int64}}(), Dict{Int64,Array{Int64,1}}(), Dict{Int64,Symbol}(), Dict{Int64,Symbol}(), Dict{Symbol,Set{Int64}}(), Dict{Symbol,Array{Tuple{Int64,Symbol},1}}(), Dict{Symbol,Symbol}(), nothing)
In [18]:
for (nid, ncoords) in nodes
    add_node!(mesh, Int(nid), ncoords)
end
In [19]:
for (elid, elcon) in left_elements
    add_element!(mesh, Int(elid), :Tri3, elcon)
    add_element_to_element_set!(mesh, :LEFT, Int(elid))
end
In [20]:
function elements_to_mesh!(mesh, elements, eltype, set_name)
    for (elid, elcon) in elements
        add_element!(mesh, Int(elid), eltype, elcon)
        add_element_to_element_set!(mesh, set_name, Int(elid))
    end
end

elements_to_mesh!(mesh, right_elements, :Tri3, :RIGHT)
elements_to_mesh!(mesh, top_elements, :Tri3, :TOP)
elements_to_mesh!(mesh, down_elements, :Tri3, :DOWN)
elements_to_mesh!(mesh, front_elements, :Tri3, :FRONT)
elements_to_mesh!(mesh, back_elements, :Tri3, :BACK)
elements_to_mesh!(mesh, plate_elements, :Tet4, :PLATE)
mesh.element_sets
Out[20]:
Dict{Symbol,Set{Int64}} with 7 entries:
  :PLATE => Set([1316, 1090, 1333, 1131, 1265, 905, 892, 1124, 1337, 873  …  10…
  :LEFT  => Set([291, 297, 304, 306, 303, 294, 298, 299, 300, 296, 292, 302, 30…
  :RIGHT => Set([329, 326, 337, 332, 336, 334, 341, 328, 333, 324, 342, 327, 32…
  :TOP   => Set([288, 287, 275, 282, 278, 279, 281, 280, 274, 273, 286, 277, 27…
  :BACK  => Set([11, 158, 215, 160, 134, 29, 131, 249, 207, 173  …  24, 73, 119…
  :DOWN  => Set([308, 310, 316, 311, 307, 319, 314, 321, 312, 317, 320, 313, 30…
  :FRONT => Set([520, 491, 464, 582, 391, 478, 384, 542, 499, 477  …  389, 420,…
In [21]:
length(mesh.nodes)
Out[21]:
318

Mesh is ready. Next create problems:

In [22]:
plate = Problem(Elasticity, "plate", 3)
plate_elements = create_elements(mesh, :PLATE)
update!(plate_elements, "youngs modulus", 210e9)
update!(plate_elements, "poissons ratio", 0.3)
add_elements!(plate, plate_elements)
┌ Info: Creating a new problem of type Elasticity, having name `plate` and dimension 3 dofs/node.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:136
┌ Info: Created 830 elements (830 x Tet4) from element set: PLATE.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/preprocess.jl:244
┌ Info: Updating field `youngs modulus` => 2.1e11 for 830 elements.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/elements.jl:328
┌ Info: Updating field `poissons ratio` => 0.3 for 830 elements.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/elements.jl:328
┌ Info: Adding 830 elements to problem `plate`
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:382
In [23]:
left_elements = create_elements(mesh, :LEFT)
update!(left_elements, "displacement 1", 0.0)
left_bc = Problem(Dirichlet, "left bc", 3, "displacement")
add_elements!(left_bc, left_elements)
┌ Info: Created 16 elements (16 x Tri3) from element set: LEFT.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/preprocess.jl:244
┌ Info: Updating field `displacement 1` => 0.0 for 16 elements.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/elements.jl:328
┌ Info: Creating a new boundary problem of type Dirichlet, having name `left bc` and dimension 3 dofs/node. This boundary problems fixes field `displacement`.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:173
┌ Info: Adding 16 elements to problem `left bc`
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:382
In [24]:
right_elements = create_elements(mesh, :RIGHT)
update!(right_elements, "displacement traction force 1", 10e3)
right_bc = Problem(Elasticity, "right bc", 3)
add_elements!(right_bc, right_elements)
┌ Info: Created 20 elements (20 x Tri3) from element set: RIGHT.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/preprocess.jl:244
┌ Info: Updating field `displacement traction force 1` => 10000.0 for 20 elements.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/elements.jl:328
┌ Info: Creating a new problem of type Elasticity, having name `right bc` and dimension 3 dofs/node.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:136
┌ Info: Adding 20 elements to problem `right bc`
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:382
In [25]:
down_elements = create_elements(mesh, :DOWN)
update!(down_elements, "displacement 2", 0.0)
down_bc = Problem(Dirichlet, "down bc", 3, "displacement")
add_elements!(down_bc, down_elements)
┌ Info: Created 16 elements (16 x Tri3) from element set: DOWN.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/preprocess.jl:244
┌ Info: Updating field `displacement 2` => 0.0 for 16 elements.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/elements.jl:328
┌ Info: Creating a new boundary problem of type Dirichlet, having name `down bc` and dimension 3 dofs/node. This boundary problems fixes field `displacement`.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:173
┌ Info: Adding 16 elements to problem `down bc`
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:382
In [26]:
front_elements = create_elements(mesh, :FRONT)
update!(front_elements, "displacement 3", 0.0)
front_bc = Problem(Dirichlet, "front bc", 3, "displacement")
add_elements!(front_bc, front_elements)
┌ Info: Created 270 elements (270 x Tri3) from element set: FRONT.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/preprocess.jl:244
┌ Info: Updating field `displacement 3` => 0.0 for 270 elements.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/elements.jl:328
┌ Info: Creating a new boundary problem of type Dirichlet, having name `front bc` and dimension 3 dofs/node. This boundary problems fixes field `displacement`.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:173
┌ Info: Adding 270 elements to problem `front bc`
└ @ FEMBase /home/jukka/repositories/FEMBase/src/problems.jl:382

Analysis:

In [27]:
analysis = Analysis(Linear)
add_problems!(analysis, plate, left_bc, right_bc, down_bc, front_bc)
┌ Info: Creating a new analysis of type Linear with name `Linear Analysis`.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/analysis.jl:30
┌ Info: Adding problem `plate` to analysis `Linear Analysis`.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/analysis.jl:36
┌ Info: Adding problem `left bc` to analysis `Linear Analysis`.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/analysis.jl:36
┌ Info: Adding problem `right bc` to analysis `Linear Analysis`.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/analysis.jl:36
┌ Info: Adding problem `down bc` to analysis `Linear Analysis`.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/analysis.jl:36
┌ Info: Adding problem `front bc` to analysis `Linear Analysis`.
└ @ FEMBase /home/jukka/repositories/FEMBase/src/analysis.jl:36

Write results to Xdmf file during analysis

In [28]:
xdmf_output = Xdmf("plate_results"; overwrite=true)
add_results_writer!(analysis, xdmf_output)
run!(analysis)
close(xdmf_output)
┌ Info: Running linear quasistatic analysis `Linear Analysis` at time 0.0.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/solvers.jl:637
┌ Info: Assembling 5 problems.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/solvers.jl:640
┌ Info: Solving linear system.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/solvers.jl:269
┌ Info: Solved linear system in 1.15 seconds using solver 1. Solution norms (||u||, ||la||): (3.008804266899659e-6, 3917.0157991440396).
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/solvers.jl:323
┌ Info: 
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/solvers.jl:335
┌ Info: Postprocessing 5 problems.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/solvers.jl:492
┌ Info: Quasistatic linear analysis ready.
└ @ JuliaFEM /home/jukka/repositories/JuliaFEM/src/solvers.jl:650

Results