# 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} name :: ASCIIString # some descriptive name for problem time :: Real # current time problems :: Vector{Problem} norms :: Vector{Tuple} # solution norms for convergence studies ndofs :: Int # number of degrees of freedom in problem properties :: S end function Solver{S<:AbstractSolver}(::Type{S}, name="solver", properties...) variant = S(properties...) solver = Solver{S}(name, 0.0, [], [], 0, variant) return solver end function get_problems(solver::Solver) return solver.problems end function push!(solver::Solver, problem) push!(solver.problems, problem) end 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 # one-liner helpers to identify problem types 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)) """ 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)) end """ function field_assembly_posthook! end """Return one combined field assembly for a set of field problems. Parameters ---------- solver :: Solver Returns ------- M, K, Kg, f, fg :: SparseMatrixCSC Notes ----- If several field problems exists, they are simply summed together, so problems must have unique node ids. """ function get_field_assembly(solver::Solver; show_info=true) problems = get_field_problems(solver) M = SparseMatrixCOO() K = SparseMatrixCOO() Kg = SparseMatrixCOO() f = SparseMatrixCOO() fg = SparseMatrixCOO() for problem in problems append!(M, problem.assembly.M) append!(K, problem.assembly.K) append!(Kg, problem.assembly.Kg) append!(f, problem.assembly.f) append!(fg, problem.assembly.fg) end if solver.ndofs == 0 solver.ndofs = size(K, 1) show_info && info("automatically determined problem dimension, ndofs = $(solver.ndofs)") end M = sparse(M, solver.ndofs, solver.ndofs) K = sparse(K, solver.ndofs, solver.ndofs) 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) f = sparse(f, solver.ndofs, 1) fg = sparse(fg, solver.ndofs, 1) # run any posthook for assembly if defined args = Tuple{Solver, SparseMatrixCSC, SparseMatrixCSC, SparseMatrixCSC, SparseMatrixCSC} if method_exists(field_assembly_posthook!, args) field_assembly_posthook!(solver, K, Kg, fg, fg) end return M, K, Kg, f, fg end """ Posthook for boundary assembly. By default, do nothing. """ function boundary_assembly_posthook! end """ Return one combined boundary assembly for a set of boundary problems. Returns ------- 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. """ function get_boundary_assembly(solver::Solver) ndofs = solver.ndofs @assert ndofs != 0 K = spzeros(ndofs, ndofs) C1 = spzeros(ndofs, ndofs) C2 = spzeros(ndofs, ndofs) D = spzeros(ndofs, ndofs) f = spzeros(ndofs, 1) g = spzeros(ndofs, 1) for problem in get_boundary_problems(solver) assembly = problem.assembly K_ = sparse(assembly.K, ndofs, ndofs) C1_ = sparse(assembly.C1, ndofs, ndofs) C2_ = sparse(assembly.C2, ndofs, ndofs) D_ = sparse(assembly.D, ndofs, ndofs) f_ = sparse(assembly.f, ndofs, 1) g_ = sparse(assembly.g, ndofs, 1) # check for overconstraint situation and handle it if possible 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 K += K_ C1 += C1_ C2 += C2_ D += D_ f += f_ g += g_ end return K, C1, C2, D, f, g end 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 """ Given C and g, construct new basis such that v = P*u + g 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 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 """ 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. """ function solve!(K, C1, C2, D, f, g, u, la, ::Type{Val{1}}; F=nothing, debug=false) nnz(D) == 0 || return F, false nz = get_nonzero_rows(C2) B = get_nonzero_rows(C2') # C2^-1 exists or this doesn't work 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 # solver boundary dofs try u[B] = lufact(C2[nz,B]) \ full(g[nz]) catch error("solver #1 failed to solve boundary dofs (you should not see this message).") end # solve interior domain using LDLt factorization 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]) return F, true end """ Solve linear system using LU factorization (UMFPACK). This version solves directly the saddle point problem without elimination of boundary conditions. """ 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] nz = get_nonzero_rows(A) x = zeros(length(b)) 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] return F, true end """ 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) M, K, Kg, f, fg = get_field_assembly(solver) Kb, C1, C2, D, fb, g = get_boundary_assembly(solver) K = K + Kg + Kb f = f + fg + fb K = 1/2*(K + K') 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 u = zeros(solver.ndofs) la = zeros(solver.ndofs) status = false i = 0 for i in [1, 2] F, status = solve!(K, C1, C2, D, f, g, u, la, Val{i}; F=F) status && break end status || error("Failed to solve linear system!") 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 end """ Check convergence of problems. Notes ----- Default convergence criteria is obtained by checking each sub-problem convergence. """ function has_converged(solver::Solver{Nonlinear}; show_info=false, check_convergence_for_boundary_problems=false) properties = solver.properties converged = true eps = properties.convergence_tolerance for problem in solver.problems has_converged = true if is_field_problem(problem) has_converged = problem.assembly.u_norm_change < eps if isapprox(norm(problem.assembly.u), 0.0) # trivial solution has_converged = true end 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)") end if is_boundary_problem(problem) && check_convergence_for_boundary_problems has_converged = problem.assembly.la_norm_change/norm(problem.assembly.la) < eps 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)") end converged &= has_converged end return converged end type NonlinearConvergenceError <: Exception solver :: Solver end function Base.showerror(io::IO, exception::NonlinearConvergenceError) max_iters = exception.solver.properties.max_iterations print(io, "nonlinear iteration did not converge in $max_iters iterations!") end """ Default solver for quasistatic nonlinear problems. """ function call(solver::Solver{Nonlinear}; show_info=true) properties = solver.properties # 1. initialize each problem so that we can start nonlinear iterations initialize!(solver) # 2. start non-linear iterations for properties.iteration=1:properties.max_iterations 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)) # 2.1 update linearized assemblies assemble!(solver) # 2.2 call solver for linearized system F, u, la = solve_linear_system(solver) # 2.3 update solution back to elements update!(solver, u, la) # 2.4 check convergence if has_converged(solver) info("Converged in $(properties.iteration) iterations.") properties.iteration >= properties.min_iterations && return true info("Convergence criteria met, but iteration < min_iterations, continuing...") end end # 3. did not converge 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 function NonlinearSolver(name::ASCIIString, problems::Problem...) solver = NonlinearSolver(problems...) solver.name = name return solver end ### 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 end """ Convenience function to call linear solver. """ function LinearSolver(problems::Problem...) solver = Solver(Linear, "default linear solver") if length(problems) != 0 push!(solver, problems...) end return solver end function LinearSolver(name::ASCIIString, problems::Problem...) solver = LinearSolver(problems...) solver.name = name return solver end ### End of linear quasistatic solver ### 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