Utilities for implicit methods
Implicit MPM formulations often need degree-of-freedom maps, sparse matrices, matrix assembly, and nonlinear iteration. Tesserae provides small utilities for those parts of an implicit formulation.
Degrees of freedom (DoF) mapping
Tesserae.DofMap — Type
DofMap(mask::AbstractArray{Bool})Create a degree of freedom (DoF) map from a mask of size (ndofs, size(grid)...). ndofs represents the number of DoFs for a field.
julia> mesh = CartesianMesh(1, (0,2), (0,1));
julia> grid = generate_grid(@NamedTuple{x::Vec{2,Float64}, v::Vec{2,Float64}}, mesh);
julia> grid.v .= reshape(reinterpret(Vec{2,Float64}, 1.0:12.0), 3, 2)
3×2 Matrix{Vec{2, Float64}}:
[1.0, 2.0] [7.0, 8.0]
[3.0, 4.0] [9.0, 10.0]
[5.0, 6.0] [11.0, 12.0]
julia> dofmask = falses(2, size(grid)...);
julia> dofmask[1,1:2,:] .= true; # activate nodes
julia> dofmask[:,3,2] .= true; # activate nodes
julia> reinterpret(reshape, Vec{2,Bool}, dofmask)
3×2 reinterpret(reshape, Vec{2, Bool}, ::BitArray{3}) with eltype Vec{2, Bool}:
[1, 0] [1, 0]
[1, 0] [1, 0]
[0, 0] [1, 1]
julia> dofmap = DofMap(dofmask);
julia> dofmap(grid.v)
6-element view(reinterpret(reshape, Float64, ::Matrix{Vec{2, Float64}}), CartesianIndex{3}[CartesianIndex(1, 1, 1), CartesianIndex(1, 2, 1), CartesianIndex(1, 1, 2), CartesianIndex(1, 2, 2), CartesianIndex(1, 3, 2), CartesianIndex(2, 3, 2)]) with eltype Float64:
1.0
3.0
7.0
9.0
11.0
12.0Sparse matrix
Tesserae.create_sparse_matrix — Function
create_sparse_matrix(mesh; ndofs)
create_sparse_matrix((rowmesh, colmesh); ndofs=(row_ndofs, col_ndofs))
create_sparse_matrix(basis, mesh; ndofs)Create a sparse matrix. Since the created matrix accounts for all nodes in the mesh, it needs to be extracted for active nodes using the DofMap. ndofs specifies the number of DoFs for a field and must be provided explicitly. For a mesh pair, the first mesh defines the rows and the second defines the columns.
julia> mesh = CartesianMesh(1, (0,10), (0,10));
julia> A = create_sparse_matrix(BSpline(Linear()), mesh; ndofs = 1)
121×121 SparseArrays.SparseMatrixCSC{Float64, Int64} with 961 stored entries:
⎡⠻⣦⡀⠘⢳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎤
⎢⣀⠈⠻⣦⡀⠘⢳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠙⢶⣀⠈⠻⣦⡀⠙⢷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠙⢶⣄⠈⠻⣦⡀⠙⠷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠙⢷⣄⠈⠻⣦⡀⠉⠷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠙⢧⡄⠈⠛⣤⡀⠉⠣⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠙⢧⡄⠈⠻⣦⡀⠉⠷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⢦⡄⠈⠱⣦⡀⠉⠷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢧⡄⠈⠻⣦⡀⠙⢷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢧⣄⠈⠻⣦⡀⠙⢷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢷⣄⠈⠻⣦⡀⠘⢳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢷⣀⠈⠻⣦⡀⠘⢳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢶⣀⠈⠻⢆⡀⠘⠳⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢶⣀⠈⠻⣦⡀⠘⢳⣄⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢢⣀⠈⠛⣤⡀⠘⢳⣄⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢶⣀⠈⠻⣦⡀⠙⢷⣄⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢶⣄⠈⠻⣦⡀⠙⠷⣄⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢷⣄⠈⠻⣦⡀⠉⠷⣄⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢧⡄⠈⠻⣦⡀⠉⎥
⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢧⡄⠈⠻⣦⎦
julia> dofmask = falses(1, size(mesh)...);
julia> dofmask[:,1:3,1:3] .= true;
julia> dofmap = DofMap(dofmask);
julia> extract(A, dofmap)
9×9 SparseArrays.SparseMatrixCSC{Float64, Int64} with 49 stored entries:
0.0 0.0 ⋅ 0.0 0.0 ⋅ ⋅ ⋅ ⋅
0.0 0.0 0.0 0.0 0.0 0.0 ⋅ ⋅ ⋅
⋅ 0.0 0.0 ⋅ 0.0 0.0 ⋅ ⋅ ⋅
0.0 0.0 ⋅ 0.0 0.0 ⋅ 0.0 0.0 ⋅
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
⋅ 0.0 0.0 ⋅ 0.0 0.0 ⋅ 0.0 0.0
⋅ ⋅ ⋅ 0.0 0.0 ⋅ 0.0 0.0 ⋅
⋅ ⋅ ⋅ 0.0 0.0 0.0 0.0 0.0 0.0
⋅ ⋅ ⋅ ⋅ 0.0 0.0 ⋅ 0.0 0.0Tesserae.extract — Method
extract(matrix::AbstractMatrix, dofmap_row::DofMap, dofmap_col::DofMap = dofmap_row)Extract the active degrees of freedom of a matrix.
Assembly of global matrix
Tesserae.@P2G_Matrix — Macro
@P2G_Matrix grid=>(i,j) particles=>p weights=>(ip,jp) [partition] begin
equations...
endParticle-to-grid transfer macro for assembling a global matrix. A typical global stiffness matrix can be assembled as follows:
@P2G_Matrix grid=>(i,j) particles=>p weights=>(ip,jp) begin
K[i,j] = @∑ ∇w[ip] ⊡ c[p] ⊡ ∇w[jp] * V[p]
endwhere c and V denote the stiffness (symmetric fourth-order) tensor and the volume, respectively. It is recommended to create global stiffness K using create_sparse_matrix.
Solvers
Tesserae.newton! — Function
Tesserae.newton!(x::AbstractVector, f, J,
maxiter = 100, atol = zero(eltype(x)), rtol = sqrt(eps(eltype(x))),
linsolve = (x,A,b) -> copyto!(x, A\b),
backtracking = false, verbose = false)A simple implementation of Newton's method. The functions f(x) and J(x) should return the residual vector and its Jacobian, respectively.
Evaluation order:
r = f(x) # update state/caches derived from x and return residual
while not converged
x_old = x
Jx = J(x) # compute from x or reuse caches from f(x)
δx = solve(Jx, r)
if backtracking
ϕ′0 = -dot(r, Jx, δx)
ϕ′0 < 0 || fail
for α in trial_steps
x = x_old - α * δx
r = f(x) # update trial state
accept && break
end
else
x = x_old - δx
r = f(x)
end
endIf backtracking fails, x is restored to the last accepted iterate and f(x) is called once more to restore the corresponding state.
At each iteration, newton! evaluates J(x) only after f(x) has already been evaluated at the same x. In simulation codes, residual and tangent/Jacobian assembly often share intermediate quantities. These quantities may be stored in caller-owned state while evaluating f(x), so that the following J(x) call can reuse them without recomputing them. This is optional: J(x) may also assemble the Jacobian directly from x.