Jacobian-free Newton–Krylov method

# Particles# IterationsExecution time
26k3002 min
using Tesserae

using IterativeSolvers: gmres!
using LinearMaps: LinearMap

function main()

    # Simulation parameters
    h  = 0.05 # Grid spacing
    T  = 3.0  # Time span
    Δt = 0.01 # Time step

    # Material constants
    E  = 100e3                  # Young's modulus
    ν  = 0.3                    # Poisson's ratio
    λ  = (E*ν) / ((1+ν)*(1-2ν)) # Lame's first parameter
    μ  = E / 2(1 + ν)           # Shear modulus
    ρ⁰ = 1000.0                 # Initial density

    # Newmark-beta integration
    β = 1/4
    γ = 1/2

    GridProp = @NamedTuple begin
        X   :: Vec{3, Float64}
        m   :: Float64
        m⁻¹ :: Float64
        v   :: Vec{3, Float64}
        vⁿ  :: Vec{3, Float64}
        mv  :: Vec{3, Float64}
        a   :: Vec{3, Float64}
        aⁿ  :: Vec{3, Float64}
        ma  :: Vec{3, Float64}
        u   :: Vec{3, Float64}
        f   :: Vec{3, Float64}
        δu  :: Vec{3, Float64}
    end
    ParticleProp = @NamedTuple begin
        x    :: Vec{3, Float64}
        m    :: Float64
        V⁰   :: Float64
        v    :: Vec{3, Float64}
        a    :: Vec{3, Float64}
        ∇a   :: SecondOrderTensor{3, Float64, 9}
        ∇u   :: SecondOrderTensor{3, Float64, 9}
        F    :: SecondOrderTensor{3, Float64, 9}
        ΔF⁻¹ :: SecondOrderTensor{3, Float64, 9}
        τ    :: SecondOrderTensor{3, Float64, 9}
        ℂ    :: FourthOrderTensor{3, Float64, 81}
    end

    # Background grid
    grid = generate_grid(GridProp, CartesianMesh(h, (0,1.5), (-0.6,0.6), (-0.6,0.6)))

    # Particles
    beam = extract(grid.X, (0,1.5), (-0.3,0.3), (-0.3,0.3))
    particles = generate_particles(ParticleProp, beam; alg=GridSampling(spacing=1/6))
    particles.V⁰ .= volume(beam) / length(particles)
    filter!(particles) do pt
        x, y, z = pt.x
        (-0.3<y<-0.25 || 0.25<y<0.3) && (-0.3<z<-0.25 || 0.25<z<0.3)
    end
    @. particles.m = ρ⁰ * particles.V⁰
    @. particles.F = one(particles.F)
    @show length(particles)

    # Interpolation
    # Use the kernel correction to properly handle the boundary conditions
    mpvalues = generate_mpvalues(KernelCorrection(BSpline(Quadratic())), grid.X, length(particles))

    # Neo-Hookean model
    function kirchhoff_stress(F)
        J = det(F)
        b = symmetric(F * F')
        μ*(b-I) + λ*log(J)*I
    end

    # Outputs
    outdir = mkpath(joinpath("output", "implicit_jacobian_free"))
    pvdfile = joinpath(outdir, "paraview")
    closepvd(openpvd(pvdfile)) # Create file

    t = 0.0
    step = 0
    fps = 60
    savepoints = collect(LinRange(t, T, round(Int, T*fps)+1))

    Tesserae.@showprogress while t < T

        for p in eachindex(particles, mpvalues)
            update!(mpvalues[p], particles.x[p], grid.X)
        end

        @P2G grid=>i particles=>p mpvalues=>ip begin
            m[i]  = @∑ w[ip] * m[p]
            mv[i] = @∑ w[ip] * m[p] * v[p]
            ma[i] = @∑ w[ip] * m[p] * a[p]
        end

        # Compute the grid velocity and acceleration at t = tⁿ
        @. grid.m⁻¹ = inv(grid.m) * !iszero(grid.m)
        @. grid.vⁿ = grid.mv * grid.m⁻¹
        @. grid.aⁿ = grid.ma * grid.m⁻¹

        # Create dofmask
        dofmask = trues(3, size(grid)...)
        for i in eachindex(grid)
            iszero(grid.m[i]) && (dofmask[:,i] .= false)
        end

        # Update boundary conditions
        @. grid.u = zero(grid.u)
        for i in eachindex(grid)[1,:,:]
            dofmask[:,i] .= false
        end
        for i in eachindex(grid)[end,:,:]
            dofmask[:,i] .= false
            grid.u[i] = (rotmat(2π*Δt, Vec(1,0,0)) - I) * grid.X[i]
        end
        dofmap = DofMap(dofmask)

        # Solve the nonlinear equation
        state = (; grid, particles, mpvalues, kirchhoff_stress, β, γ, dofmap, Δt)
        U = copy(dofmap(grid.u)) # Convert grid data to plain vector data
        compute_residual(U) = residual(U, state)
        compute_jacobian(U) = jacobian(U, state)
        Tesserae.newton!(U, compute_residual, compute_jacobian; linsolve = (x,A,b)->gmres!(x,A,b))

        # Grid dispacement, velocity and acceleration have been updated during Newton's iterations
        @G2P grid=>i particles=>p mpvalues=>ip begin
            v[p] += @∑ w[ip] * ((1-γ)*a[p] + γ*a[i]) * Δt
            a[p]  = @∑ w[ip] * a[i]
            x[p]  = @∑ w[ip] * (X[i] + u[i])
            ∇u[p] = @∑ u[i] ⊗ ∇w[ip]
            F[p]  = (I + ∇u[p]) * F[p]
        end

        t += Δt
        step += 1

        if t > first(savepoints)
            popfirst!(savepoints)
            openpvd(pvdfile; append=true) do pvd
                openvtk(string(pvdfile, step), particles.x) do vtk
                    vtk["Velocity (m/s)"] = particles.v
                    vtk["von Mises stress (kPa)"] = @. 1e-3 * vonmises(particles.τ / det(particles.F))
                    pvd[t] = vtk
                end
            end
        end
    end
    sum(particles.x) / length(particles)
end

function residual(U::AbstractVector, state)
    (; grid, particles, mpvalues, kirchhoff_stress, β, γ, dofmap, Δt) = state

    dofmap(grid.u) .= U
    @. grid.a = (1/(β*Δt^2))*grid.u - (1/(β*Δt))*grid.vⁿ - (1/2β-1)*grid.aⁿ
    @. grid.v = grid.vⁿ + ((1-γ)*grid.aⁿ + γ*grid.a) * Δt

    geometric(τ) = @einsum G[i,j,k,l] := τ[i,l] * one(τ)[j,k]
    @G2P grid=>i particles=>p mpvalues=>ip begin
        # In addition to updating the stress tensor, the stiffness tensor,
        # which is utilized in the Jacobian-vector product, is also updated.
        ∇u[p] = @∑ u[i] ⊗ ∇w[ip]
        ΔF⁻¹[p] = inv(I + ∇u[p])
        F = (I + ∇u[p]) * F[p]
        ∂τ∂F, τ = gradient(kirchhoff_stress, F, :all)
        τ[p] = τ
        ℂ[p] = ∂τ∂F ⊡ F' - geometric(τ)
    end
    @P2G grid=>i particles=>p mpvalues=>ip begin
        f[i] = @∑ V⁰[p] * τ[p] * (∇w[ip] ⊡ ΔF⁻¹[p])
    end

    @. β*Δt^2 * ($dofmap(grid.a) + $dofmap(grid.f) * $dofmap(grid.m⁻¹))
end

function jacobian(U::AbstractVector, state)
    (; grid, particles, mpvalues, β, dofmap, Δt) = state

    # Create a linear map to represent Jacobian-vector product J*δU.
    # `U` is acutally not used because the stiffness tensor is already calculated
    # when computing the residual vector.
    fillzero!(grid.δu)
    LinearMap(ndofs(dofmap)) do JδU, δU
        dofmap(grid.δu) .= δU

        @G2P grid=>i particles=>p mpvalues=>ip begin
            ∇u[p] = @∑ δu[i] ⊗ (∇w[ip] ⊡ ΔF⁻¹[p])
            τ[p] = ℂ[p] ⊡₂ ∇u[p]
        end
        @P2G grid=>i particles=>p mpvalues=>ip begin
            f[i] = @∑ V⁰[p] * τ[p] * (∇w[ip] ⊡ ΔF⁻¹[p])
        end

        @. JδU = δU + β*Δt^2 * $dofmap(grid.f) * $dofmap(grid.m⁻¹)
    end
end

This page was generated using Literate.jl.