Files
JuliaFEM.jl/src/solvers.jl
T

600 lines
18 KiB
Julia
Raw Normal View History

2015-10-09 23:45:28 +03:00
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md
abstract AbstractSolver
type Solver{S<:AbstractSolver}
2016-02-05 12:27:36 +02:00
name :: ASCIIString # some descriptive name for problem
time :: Real # current time
problems :: Vector{Problem}
2016-06-27 16:11:33 +03:00
norms :: Vector{Tuple} # solution norms for convergence studies
ndofs :: Int # number of degrees of freedom in problem
properties :: S
end
2016-02-01 09:13:07 +02:00
2016-06-27 16:11:33 +03:00
function Solver{S<:AbstractSolver}(::Type{S}, name="solver", properties...)
variant = S(properties...)
2016-06-27 16:11:33 +03:00
solver = Solver{S}(name, 0.0, [], [], 0, variant)
return solver
2016-02-01 09:13:07 +02:00
end
2016-06-25 04:12:53 +03:00
function get_problems(solver::Solver)
return solver.problems
end
2016-02-03 06:49:42 +02:00
function push!(solver::Solver, problem)
2016-02-01 09:13:07 +02:00
push!(solver.problems, problem)
end
2016-06-25 04:12:53 +03:00
function getindex(solver::Solver, problem_name::ASCIIString)
for problem in get_problems(solver)
if problem.name == problem_name
return problem
end
end
throw(KeyError(problem_name))
end
2016-02-01 09:13:07 +02:00
# one-liner helpers to identify problem types
2016-06-27 16:11:33 +03:00
is_field_problem(problem) = false
is_field_problem{P<:FieldProblem}(problem::Problem{P}) = true
is_boundary_problem(problem) = false
is_boundary_problem{P<:BoundaryProblem}(problem::Problem{P}) = true
get_field_problems(solver::Solver) = filter(is_field_problem, get_problems(solver))
get_boundary_problems(solver::Solver) = filter(is_boundary_problem, get_problems(solver))
2016-02-01 09:13:07 +02:00
2016-06-27 16:11:33 +03:00
"""
Posthook for field assembly. By default, do nothing.
This can be used to make some modifications for assembly
after all elements are assembled.
Examples
--------
function field_assembly_posthook!(solver::Solver,
K::SparseMatrixCSC,
Kg::SparseMatrixCSC,
f::SparseMatrixCSC,
fg::SpareMatrixCSC)
info("doing stuff, size(K) = ", size(K))
2016-02-01 09:13:07 +02:00
end
2016-06-27 16:11:33 +03:00
"""
2016-02-05 14:03:27 +02:00
function field_assembly_posthook!
end
2016-02-05 12:27:36 +02:00
2016-02-01 09:13:07 +02:00
"""Return one combined field assembly for a set of field problems.
Parameters
----------
solver :: Solver
Returns
-------
2016-06-27 16:11:33 +03:00
M, K, Kg, f, fg :: SparseMatrixCSC
2016-02-01 09:13:07 +02:00
Notes
-----
If several field problems exists, they are simply summed together, so
problems must have unique node ids.
"""
2016-06-27 16:11:33 +03:00
function get_field_assembly(solver::Solver; show_info=true)
2016-02-05 12:27:36 +02:00
problems = get_field_problems(solver)
2016-06-27 16:11:33 +03:00
M = SparseMatrixCOO()
2016-02-01 09:13:07 +02:00
K = SparseMatrixCOO()
Kg = SparseMatrixCOO()
2016-02-01 09:13:07 +02:00
f = SparseMatrixCOO()
2016-06-27 16:11:33 +03:00
fg = SparseMatrixCOO()
2016-02-01 09:13:07 +02:00
for problem in problems
2016-06-27 16:11:33 +03:00
append!(M, problem.assembly.M)
2016-06-09 01:27:56 +03:00
append!(K, problem.assembly.K)
append!(Kg, problem.assembly.Kg)
2016-06-09 01:27:56 +03:00
append!(f, problem.assembly.f)
2016-06-27 16:11:33 +03:00
append!(fg, problem.assembly.fg)
end
2016-06-27 16:11:33 +03:00
if solver.ndofs == 0
solver.ndofs = size(K, 1)
2016-06-27 16:11:33 +03:00
show_info && info("automatically determined problem dimension, ndofs = $(solver.ndofs)")
end
2016-06-27 16:11:33 +03:00
M = sparse(M, solver.ndofs, solver.ndofs)
K = sparse(K, solver.ndofs, solver.ndofs)
2016-07-03 21:16:03 +03:00
if nnz(K) == 0
warn("Field assembly seems to be empty. Check that elements are pushed to problem and formulation is correct.")
end
Kg = sparse(Kg, solver.ndofs, solver.ndofs)
2016-02-05 12:27:36 +02:00
f = sparse(f, solver.ndofs, 1)
2016-06-27 16:11:33 +03:00
fg = sparse(fg, solver.ndofs, 1)
2016-02-05 14:03:27 +02:00
# run any posthook for assembly if defined
2016-06-27 16:11:33 +03:00
args = Tuple{Solver, SparseMatrixCSC, SparseMatrixCSC, SparseMatrixCSC, SparseMatrixCSC}
2016-02-05 14:03:27 +02:00
if method_exists(field_assembly_posthook!, args)
2016-06-27 16:11:33 +03:00
field_assembly_posthook!(solver, K, Kg, fg, fg)
2016-02-05 14:03:27 +02:00
end
2016-06-27 16:11:33 +03:00
return M, K, Kg, f, fg
2016-02-01 09:13:07 +02:00
end
2016-02-05 14:03:27 +02:00
""" Posthook for boundary assembly. By default, do nothing. """
function boundary_assembly_posthook!
end
2016-02-05 12:27:36 +02:00
2016-02-01 09:13:07 +02:00
""" Return one combined boundary assembly for a set of boundary problems.
Returns
-------
2016-02-05 12:27:36 +02:00
C1, C2, D, g :: SparseMatrixCSC
Notes
-----
When some dof is constrained by multiple boundary problems an algorithm is
launched what tries to do it's best to solve issue. It's far from perfect
but is able to handle some basic situations occurring in corner nodes and
crosspoints.
2016-02-01 09:13:07 +02:00
"""
function get_boundary_assembly(solver::Solver)
2016-02-05 12:27:36 +02:00
ndofs = solver.ndofs
@assert ndofs != 0
2016-02-24 01:20:39 +02:00
K = spzeros(ndofs, ndofs)
2016-02-05 12:27:36 +02:00
C1 = spzeros(ndofs, ndofs)
C2 = spzeros(ndofs, ndofs)
D = spzeros(ndofs, ndofs)
2016-02-24 01:20:39 +02:00
f = spzeros(ndofs, 1)
2016-02-05 12:27:36 +02:00
g = spzeros(ndofs, 1)
for problem in get_boundary_problems(solver)
assembly = problem.assembly
2016-02-24 01:20:39 +02:00
K_ = sparse(assembly.K, ndofs, ndofs)
2016-02-05 12:27:36 +02:00
C1_ = sparse(assembly.C1, ndofs, ndofs)
C2_ = sparse(assembly.C2, ndofs, ndofs)
D_ = sparse(assembly.D, ndofs, ndofs)
2016-02-24 01:20:39 +02:00
f_ = sparse(assembly.f, ndofs, 1)
2016-02-05 12:27:36 +02:00
g_ = sparse(assembly.g, ndofs, 1)
2016-02-05 14:03:27 +02:00
# check for overconstraint situation and handle it if possible
2016-02-05 12:27:36 +02:00
already_constrained = get_nonzero_rows(C2)
new_constraints = get_nonzero_rows(C2_)
overconstrained_dofs = intersect(already_constrained, new_constraints)
if length(overconstrained_dofs) != 0
overconstrained_dofs = sort(overconstrained_dofs)
overconstrained_nodes = find_nodes_by_dofs(problem, overconstrained_dofs)
handle_overconstraint_error!(problem, overconstrained_nodes,
overconstrained_dofs, C1, C1_, C2, C2_, D, D_, g, g_)
end
2016-02-24 01:20:39 +02:00
K += K_
2016-02-05 12:27:36 +02:00
C1 += C1_
C2 += C2_
D += D_
2016-02-24 01:20:39 +02:00
f += f_
2016-02-05 12:27:36 +02:00
g += g_
2016-02-01 09:13:07 +02:00
end
2016-02-24 01:20:39 +02:00
return K, C1, C2, D, f, g
2016-02-01 09:13:07 +02:00
end
2016-07-03 21:16:03 +03:00
function resize!(A::SparseMatrixCSC, m::Int64, n::Int64)
(n == A.n) && (m == A.m) && return
@assert n >= A.n
@assert m >= A.m
append!(A.colptr, A.colptr[end]*ones(Int, m-A.m))
A.n = n
A.m = m
end
2016-02-05 12:27:36 +02:00
2016-02-01 09:13:07 +02:00
"""
2016-06-27 16:11:33 +03:00
Given C and g, construct new basis such that v = P*u + g
2016-02-01 09:13:07 +02:00
Parameters
----------
S set of linearly independent dofs.
"""
function create_projection(C::SparseMatrixCSC, g; S=nothing, tol=1.0e-12)
n, m = size(C)
@assert n == m
if S == nothing
S = get_nonzero_rows(C)
end
# FIXME: this creates dense matrices
# efficiency / memory usage is a question
2016-06-25 04:12:53 +03:00
M = get_nonzero_columns(C)
F = qrfact(C[S,:])
P = spzeros(n,m)
P[:,M] = sparse(F \ full(C[S,M]))
h = sparse(F \ full(g[S]))
resize!(P, n, m)
resize!(h, n, 1)
P = speye(n) - P
SparseMatrix.droptol!(P, tol)
return P, h
end
2016-02-01 09:13:07 +02:00
"""
Solve linear system using LDLt factorization (SuiteSparse). This version
requires that final system is symmetric and positive definite, so boundary
conditions are first eliminated before solution.
"""
2016-06-27 16:11:33 +03:00
function solve!(K, C1, C2, D, f, g, u, la, ::Type{Val{1}}; F=nothing, debug=false)
2016-07-03 05:01:18 +03:00
nnz(D) == 0 || return F, false
nz = get_nonzero_rows(C2)
B = get_nonzero_rows(C2')
# C2^-1 exists or this doesn't work
2016-07-03 05:01:18 +03:00
length(nz) == length(B) || return F, false
A = get_nonzero_rows(K)
I = setdiff(A, B)
if debug
info("# nz = $(length(nz))")
info("# A = $(length(A))")
info("# B = $(length(B))")
info("# I = $(length(I))")
end
2016-02-05 12:27:36 +02:00
# solver boundary dofs
try
u[B] = lufact(C2[nz,B]) \ full(g[nz])
catch
2016-07-03 05:01:18 +03:00
error("solver #1 failed to solve boundary dofs (you should not see this message).")
end
# solve interior domain using LDLt factorization
2016-06-27 16:11:33 +03:00
if F == nothing
F = ldltfact(K[I,I])
end
u[I] = F \ (f[I] - K[I,B]*u[B])
# solve lambda
la[B] = lufact(C1[B,nz]) \ full(f[B] - K[B,I]*u[I] - K[B,B]*u[B])
2016-06-27 16:11:33 +03:00
return F, true
end
"""
Solve linear system using LU factorization (UMFPACK). This version solves
directly the saddle point problem without elimination of boundary conditions.
"""
2016-06-27 16:11:33 +03:00
function solve!(K, C1, C2, D, f, g, u, la, ::Type{Val{2}}; F=nothing)
# construct global system Ax = b and solve using lufact (UMFPACK)
A = [K C1'; C2 D]
b = [f; g]
2016-02-05 12:27:36 +02:00
nz = get_nonzero_rows(A)
2016-02-01 09:13:07 +02:00
x = zeros(length(b))
2016-06-27 16:11:33 +03:00
if F == nothing
F = lufact(A[nz,nz])
end
x[nz] = F \ full(b[nz])
ndofs = size(K, 1)
u[:] = x[1:ndofs]
la[:] = x[ndofs+1:end]
2016-06-27 16:11:33 +03:00
return F, true
2016-02-01 09:13:07 +02:00
end
2016-06-27 16:11:33 +03:00
""" Default linear system solver for solver. """
function solve_linear_system(solver::Solver; F=nothing, empty_assemblies_before_solution=true, show_info=true)
show_info && info("Solving problems ...")
t0 = Base.time()
# assemble field & boundary problems
# TODO: return same kind of set for both assembly types
# M1, K1, Kg1, f1, fg1, C11, C21, D1, g1 = get_field_assembly(solver)
# M2, K2, Kg2, f2, fg2, C12, C22, D2, g2 = get_boundary_assembly(solver)
2016-06-09 01:27:56 +03:00
2016-06-27 16:11:33 +03:00
M, K, Kg, f, fg = get_field_assembly(solver)
2016-06-09 01:27:56 +03:00
Kb, C1, C2, D, fb, g = get_boundary_assembly(solver)
K = K + Kg + Kb
2016-06-27 16:11:33 +03:00
f = f + fg + fb
2016-06-09 01:27:56 +03:00
K = 1/2*(K + K')
2016-06-27 16:11:33 +03:00
M = 1/2*(M + M')
# free up some memory before solution
for problem in get_problems(solver)
if empty_assemblies_before_solution
empty!(problem.assembly)
else
optimize!(problem.assembly)
end
gc()
end
2016-06-09 01:27:56 +03:00
u = zeros(solver.ndofs)
la = zeros(solver.ndofs)
status = false
2016-06-27 16:11:33 +03:00
i = 0
for i in [1, 2]
2016-06-27 16:11:33 +03:00
F, status = solve!(K, C1, C2, D, f, g, u, la, Val{i}; F=F)
2016-07-03 05:01:18 +03:00
status && break
2016-06-09 01:27:56 +03:00
end
status || error("Failed to solve linear system!")
2016-06-09 01:27:56 +03:00
2016-06-27 16:11:33 +03:00
t1 = round(Base.time()-t0, 2)
norms = (norm(u), norm(la))
show_info && info("Solved problems in $t1 seconds using solver $i. Solution norms = $norms.")
push!(solver.norms, norms)
return F, u, la
end
""" Default assembler for solver. """
function assemble!(solver::Solver; show_info=true)
show_info && info("Assembling problems ...")
t0 = Base.time()
nproblems = 0
ndofs = 0
for problem in solver.problems
empty!(problem.assembly)
assemble!(problem, solver.time)
nproblems += 1
ndofs = max(ndofs, size(problem.assembly.K, 2))
end
solver.ndofs = ndofs
t1 = round(Base.time()-t0, 2)
show_info && info("Assembled $nproblems problems in $t1 seconds. ndofs = $ndofs.")
end
""" Default initializer for solver. """
function initialize!(solver::Solver; show_info=true)
show_info && info("Initializing problems ...")
t0 = Base.time()
for problem in solver.problems
initialize!(problem, solver.time)
end
t1 = round(Base.time()-t0, 2)
show_info && info("Initialized problems in $t1 seconds.")
end
""" Default update for solver. """
function update!(solver::Solver, u::Vector, la::Vector; show_info=true)
show_info && info("Updating problems ...")
t0 = Base.time()
for problem in solver.problems
u_new, la_new = update_assembly!(problem, u, la)
update_elements!(problem, u_new, la_new)
end
t1 = round(Base.time()-t0, 2)
show_info && info("Updated problems in $t1 seconds.")
end
### Nonlinear quasistatic solver
type Nonlinear <: AbstractSolver
iteration :: Int # iteration counter
min_iterations :: Int64 # minimum number of iterations
max_iterations :: Int64 # maximum number of iterations
convergence_tolerance :: Float64
error_if_no_convergence :: Bool # throw error if no convergence
end
function Nonlinear()
solver = Nonlinear(0, 1, 20, 5.0e-5, true)
return solver
2016-06-09 01:27:56 +03:00
end
2016-02-05 12:27:36 +02:00
2016-02-01 09:13:07 +02:00
""" Check convergence of problems.
Notes
-----
Default convergence criteria is obtained by checking each sub-problem convergence.
"""
2016-06-27 16:11:33 +03:00
function has_converged(solver::Solver{Nonlinear}; show_info=false,
check_convergence_for_boundary_problems=false)
properties = solver.properties
2016-02-01 09:13:07 +02:00
converged = true
eps = properties.convergence_tolerance
2016-02-01 09:13:07 +02:00
for problem in solver.problems
2016-02-03 20:39:03 +02:00
has_converged = true
if is_field_problem(problem)
has_converged = problem.assembly.u_norm_change < eps
if isapprox(norm(problem.assembly.u), 0.0)
2016-06-27 16:11:33 +03:00
# trivial solution
has_converged = true
end
2016-06-27 16:11:33 +03:00
show_info && info("Details for problem $(problem.name)")
show_info && info("Norm: $(norm(problem.assembly.u))")
show_info && info("Norm change: $(problem.assembly.u_norm_change)")
show_info && info("Has converged? $(has_converged)")
2016-02-03 20:39:03 +02:00
end
2016-02-11 02:51:27 +02:00
if is_boundary_problem(problem) && check_convergence_for_boundary_problems
2016-02-03 20:39:03 +02:00
has_converged = problem.assembly.la_norm_change/norm(problem.assembly.la) < eps
2016-06-27 16:11:33 +03:00
show_info && info("Details for problem $(problem.name)")
show_info && info("Norm: $(norm(problem.assembly.la))")
show_info && info("Norm change: $(problem.assembly.la_norm_change)")
show_info && info("Has converged? $(has_converged)")
2016-02-01 09:13:07 +02:00
end
converged &= has_converged
end
2016-06-27 16:11:33 +03:00
return converged
2016-02-01 09:13:07 +02:00
end
type NonlinearConvergenceError <: Exception
solver :: Solver
end
function Base.showerror(io::IO, exception::NonlinearConvergenceError)
max_iters = exception.solver.properties.max_iterations
2016-02-01 09:13:07 +02:00
print(io, "nonlinear iteration did not converge in $max_iters iterations!")
end
""" Default solver for quasistatic nonlinear problems. """
2016-06-27 16:11:33 +03:00
function call(solver::Solver{Nonlinear}; show_info=true)
properties = solver.properties
2016-02-01 09:13:07 +02:00
# 1. initialize each problem so that we can start nonlinear iterations
initialize!(solver)
2016-02-01 09:13:07 +02:00
# 2. start non-linear iterations
for properties.iteration=1:properties.max_iterations
2016-06-27 16:11:33 +03:00
show_info && info(repeat("-", 80))
show_info && info("Starting nonlinear iteration #$(properties.iteration)")
show_info && info("Increment time t=$(round(solver.time, 3))")
show_info && info(repeat("-", 80))
2016-02-24 01:20:39 +02:00
2016-06-27 16:11:33 +03:00
# 2.1 update linearized assemblies
2016-06-19 20:01:37 +03:00
assemble!(solver)
2016-02-01 09:13:07 +02:00
2016-06-27 16:11:33 +03:00
# 2.2 call solver for linearized system
F, u, la = solve_linear_system(solver)
2016-02-01 09:13:07 +02:00
# 2.3 update solution back to elements
2016-06-27 16:11:33 +03:00
update!(solver, u, la)
2016-02-01 09:13:07 +02:00
# 2.4 check convergence
if has_converged(solver)
info("Converged in $(properties.iteration) iterations.")
2016-06-27 16:11:33 +03:00
properties.iteration >= properties.min_iterations && return true
info("Convergence criteria met, but iteration < min_iterations, continuing...")
2016-02-01 09:13:07 +02:00
end
end
# 3. did not converge
2016-06-27 16:11:33 +03:00
properties.error_if_no_convergence && throw(NonlinearConvergenceError(solver))
end
""" Convenience function to call nonlinear solver. """
function NonlinearSolver(problems...)
solver = Solver(Nonlinear, "default nonlinear solver")
if length(problems) != 0
push!(solver, problems...)
end
return solver
end
2016-07-03 21:16:03 +03:00
function NonlinearSolver(name::ASCIIString, problems::Problem...)
solver = NonlinearSolver(problems...)
solver.name = name
return solver
end
2016-06-27 16:11:33 +03:00
### Linear quasistatic solver
""" Quasistatic solver for linear problems.
Notes
-----
Main differences in this solver, compared to nonlinear solver are:
1. system of problems is assumed to converge in one step
2. reassembly of problem is done only if it's manually requested using empty!(problem.assembly)
"""
type Linear <: AbstractSolver
end
function assemble!(solver::Solver{Linear}; show_info=true)
show_info && info("Assembling problems ...")
tic()
nproblems = 0
ndofs = 0
for problem in get_problems(solver)
if isempty(problem.assembly)
assemble!(problem, solver.time)
nproblems += 1
else
show_info && info("$(problem.name) already assembled, skipping.")
end
ndofs = max(ndofs, size(problem.assembly.K, 2))
end
solver.ndofs = ndofs
t1 = round(toq(), 2)
show_info && info("Assembled $nproblems problems in $t1 seconds. ndofs = $ndofs.")
end
function call(solver::Solver{Linear}; F=nothing, show_info=true, return_factorization=true)
t0 = Base.time()
show_info && info(repeat("-", 80))
show_info && info("Starting linear solver")
show_info && info("Increment time t=$(round(solver.time, 3))")
show_info && info(repeat("-", 80))
initialize!(solver)
assemble!(solver)
F, u, la = solve_linear_system(solver; F=F, empty_assemblies_before_solution=false)
update!(solver, u, la)
t1 = round(Base.time()-t0, 2)
show_info && info("Linear solver ready in $t1 seconds.")
if return_factorization
return F
end
2016-02-01 09:13:07 +02:00
end
2016-02-24 01:20:39 +02:00
2016-06-27 16:11:33 +03:00
""" Convenience function to call linear solver. """
2016-07-01 02:55:56 +03:00
function LinearSolver(problems::Problem...)
2016-06-27 16:11:33 +03:00
solver = Solver(Linear, "default linear solver")
if length(problems) != 0
push!(solver, problems...)
end
return solver
end
2016-07-01 02:55:56 +03:00
function LinearSolver(name::ASCIIString, problems::Problem...)
solver = LinearSolver(problems...)
solver.name = name
return solver
end
2016-06-27 16:11:33 +03:00
### End of linear quasistatic solver
2016-07-03 05:01:18 +03:00
### Postprocessor
type Postprocessor <: AbstractSolver
assembly :: Assembly
F :: Union{Factorization, Void}
end
function Postprocessor()
Postprocessor(Assembly(), nothing)
end
function assemble!(solver::Solver{Postprocessor}; show_info=true)
show_info && info("Assembling problems ...")
tic()
nproblems = 0
ndofs = 0
assembly = solver.properties.assembly
empty!(assembly)
for problem in get_problems(solver)
for element in get_elements(problem)
postprocess!(assembly, problem, element, solver.time)
end
nproblems += 1
ndofs = max(ndofs, size(problem.assembly.K, 2))
end
solver.ndofs = ndofs
t1 = round(toq(), 2)
show_info && info("Assembled $nproblems problems in $t1 seconds. ndofs = $ndofs.")
end
function call(solver::Solver{Postprocessor}; show_info=true)
t0 = Base.time()
show_info && info(repeat("-", 80))
show_info && info("Starting postprocessor")
show_info && info("Increment time t=$(round(solver.time, 3))")
show_info && info(repeat("-", 80))
initialize!(solver)
assemble!(solver)
assembly = solver.properties.assembly
M = sparse(assembly.M)
f = sparse(assembly.f)
F = cholfact(M)
q = F \ f
t1 = round(Base.time()-t0, 2)
show_info && info("Postprocess of results ready in $t1 seconds.")
return q
end
""" Convenience function to call postprocessor. """
function Postprocessor(problems::Problem...)
solver = Solver(Postprocessor, "default postprocessor")
if length(problems) != 0
push!(solver, problems...)
end
return solver
end
function Postprocessor(name::ASCIIString, problems::Problem...)
solver = Postprocessor(problems...)
solver.name = name
return solver
end