API updates

Source

Here we describe some updates for the API of VoronoiFVM.jl. These have been implemented mostly on top of the existing API, whose functionality is not affected.

TableOfContents(; aside = false, depth = 5)
begin
    import Pkg as _Pkg
    haskey(ENV, "PLUTO_PROJECT") && _Pkg.activate(ENV["PLUTO_PROJECT"])
    using Revise
    using VoronoiFVM
    using ExtendableGrids
    using ExtendableSparse
    using Test
    using PlutoUI
    using GridVisualize
    using LinearSolve
    using ILUZero
    using LinearAlgebra
    using CairoMakie
    CairoMakie.activate!(; type = "svg", visible = false)
    GridVisualize.default_plotter!(CairoMakie)
end;

v0.19

This is a breaking release. Implementations using default solver settings should continue to work (albeit possibly with deprecation and allocation warnings). Really breaking is control of iterative linear solvers and allocation checks.

Solve now a method of CommonSolve.solve

As a consequence, all VoronoiFVM.solve methods with signatures others than solve(system; kwargs...) are now deprecated

n = 100
100
begin
    h = 1.0 / convert(Float64, n)
    const eps = 1.0e-2
    function reaction(f, u, node)
        f[1] = u[1]^2
    end

    function flux(f, u, edge)
        f[1] = eps * (u[1, 1]^2 - u[1, 2]^2)
    end

    function source(f, node)
        x1 = node[1] - 0.5
        x2 = node[2] - 0.5
        f[1] = exp(-20.0 * (x1^2 + x2^2))
    end

    function storage(f, u, node)
        f[1] = u[1]
    end

    function bcondition(f, u, node)
        boundary_dirichlet!(f,
                            u,
                            node;
                            species = 1,
                            region = 2,
                            value = ramp(node.time; dt = (0, 0.1), du = (0, 1)))
        boundary_dirichlet!(f,
                            u,
                            node;
                            species = 1,
                            region = 4,
                            value = ramp(node.time; dt = (0, 0.1), du = (0, 1)))
    end

    sys0 = VoronoiFVM.System(0.0:h:1.0,
                             0.0:h:1.0;
                             reaction,
                             flux,
                             source,
                             storage,
                             bcondition,
                             species = [1],)
end
VoronoiFVM.System{Float64, Float64, Int32, Int64, Matrix{Int32}, Matrix{Float64}}(num_species=1)

Deprecated call:

begin
    inival = unknowns(sys0; inival = 0.1)
    sol00 = unknowns(sys0)
    solve!(sol00, inival, sys0)
end
1×10201 Matrix{Float64}:
 1.0  0.951791  0.906016  0.862563  0.821331  …  0.862563  0.906016  0.951791  1.0

Replace this by:

sol0 = solve(sys0; inival = 0.1)
1×10201 Matrix{Float64}:
 1.02241e-33  0.0319717  0.045243  0.0554731  …  0.045243  0.0319717  1.02241e-33

Docstring of solve

solve(system; kwargs...)

Built-in solution method for VoronoiFVM.System.

Keyword arguments:

  • General for all solvers

    • inival (default: 0) : Array created via unknowns or number giving the initial value.

    • control (default: nothing): Pass instance of SolverControl

    • All elements of SolverControl can be used as kwargs. Eventually overwrites values given via control

    • params: Parameters (Parameter handling is experimental and may change)

  • Stationary solver: Invoked if neither times nor embed, nor tstep are given as keyword argument.

    • time (default: 0.0): Set time value.

    Returns a DenseSolutionArray or SparseSolutionArray

  • Embedding (homotopy) solver: Invoked if embed kwarg is given. Use homotopy embedding + damped Newton's method to solve stationary problem or to solve series of parameter dependent problems. Parameter step control is performed according to solver control data. kwargs and default values are:

    • embed (default: nothing ): vector of parameter values to be reached exactly

    In addition, all kwargs of the implicit Euler solver (besides times) are handled. Returns a transient solution object sol containing the stored solution(s), see TransientSolution.

  • Implicit Euler transient solver: Invoked if times kwarg is given. Use implicit Euler method + damped Newton's method to solve time dependent problem. Time step control is performed according to solver control data. kwargs and default values are:

    • times (default: nothing ): vector of time values to be reached exactly

    • pre (default: (sol,t)->nothing ): callback invoked before each time step

    • post (default: (sol,oldsol, t, Δt)->nothing ): callback invoked after each time step

    • sample (default: (sol,t)->nothing ): callback invoked after timestep for all times in times[2:end].

    • delta (default: (system, u,v,t, Δt)->norm(sys,u-v,Inf) ): Value used to control the time step size Δu

    If control.handle_error is true, if time step solution throws an error, stepsize is lowered, and step solution is called again with a smaller time value. If control.Δt<control.Δt_min, solution is aborted with error. Returns a transient solution object sol containing the stored solution, see TransientSolution.

  • Implicit Euler timestep solver. Invoked if tstep kwarg is given. Solve one time step of the implicit Euler method.

    • time (default: 0): Set time value.

    • tstep: time step

    Returns a DenseSolutionArray or SparseSolutionArray

Docstring of SolverControl

SolverControl
SolverControl(;kwargs...)
SolverControl(linear_solver_strategy, sys; kwargs...)

Solver control parameter for time stepping, embedding, Newton method and linear solver control. All field names can be used as keyword arguments for solve(system::VoronoiFVM.AbstractSystem; kwargs...)

Newton's method solves $F(u)=0$ by the iterative procedure $u_{i+1}=u_{i} - d_i F'(u_i)^{-1}F(u_i)$ starting with some initial value $u_0$, where $d_i$ is a damping parameter.

For linear solver strategies, see VoronoiFVM.LinearSolverStrategy.

  • verbose::Union{Bool, String}: Verbosity control. A collection of output categories is given in a string composed of the following letters:

    • a: allocation warnings

    • d: deprecation warnings

    • e: time/parameter evolution log

    • n: newton solver log

    • l: linear solver log

    Alternatively, a Bool value can be given, resulting in

    • true: "neda"

    • false: "da"

    Switch off all output including deprecation warnings via verbose="". In the output, corresponding messages are marked e.g. via '[n]', [a] etc. (besides of '[l]')

  • abstol::Float64: Tolerance (in terms of norm of Newton update): terminate if $\Delta u_i=||u_{i+1}-u_i||_\infty <$abstol.

  • reltol::Float64: Tolerance (relative to the size of the first update): terminate if $\Delta u_i/\Delta u_1<$reltol.

  • maxiters::Int64: Maximum number of newton iterations.

  • tol_round::Float64: Tolerance for roundoff error detection: terminate if $|\;||u_{i+1}||_1 - ||u_{i}||_1\;|/ ||u_{i}||_1<$tol_round occurred max_round times in a row.

  • tol_mono::Float64: Tolerance for monotonicity test: terminate with error if $\Delta u_i/\Delta u_{i-1}>$1/tol_mono.

  • damp_initial::Float64: Initial damping parameter $d_0$. To handle convergence problems, set this to a value less than 1.

  • damp_growth::Float64: Damping parameter growth factor: $d_{i+1}=\min(d_i\cdot$max_growth$,1)$. It should be larger than 1.

  • max_round::Int64: Maximum number of consecutive iterations within roundoff error tolerance The default effectively disables this criterion.

  • unorm::Function: Calculation of Newton update norm

  • rnorm::Function: Functional for roundoff error calculation

  • method_linear::Union{Nothing, LinearSolve.SciMLLinearSolveAlgorithm}: Solver method for linear systems (see LinearSolve.jl). If given nothing, as default are chosen (for Float64 calculations):

    • 1D: KLUFactorization()

    • 2D: SparspakFactorization()

    • 3D: UMFPACKFactorization()

    SparspakFactorization() is the default choice for general number types. Users should experiment with what works best for their problem.

    For easy access to this functionality, see see also VoronoiFVM.LinearSolverStrategy.

  • reltol_linear::Float64: Relative tolerance of iterative linear solver.

  • abstol_linear::Float64: Absolute tolerance of iterative linear solver.

  • maxiters_linear::Int64: Maximum number of iterations of linear solver

  • precon_linear::Union{Nothing, Function, ExtendableSparse.AbstractFactorization, LinearSolve.SciMLLinearSolveAlgorithm, Type}: Constructor for preconditioner for linear systems. This should work as a function precon_linear(A) which takes an AbstractSparseMatrixCSC (e.g. an ExtendableSparseMatrix) and returns a preconditioner object in the sense of LinearSolve.jl, i.e. which has an ldiv!(u,A,v) method. Useful examples:

    • ExtendableSparse.ILUZero

    • ExtendableSparse.Jacobi

    For easy access to this functionality, see see also VoronoiFVM.LinearSolverStrategy.

  • keepcurrent_linear::Bool: Update preconditioner in each Newton step ?

  • Δp::Float64: Initial parameter step for embedding.

  • Δp_max::Float64: Maximal parameter step size.

  • Δp_min::Float64: Minimal parameter step size.

  • Δp_grow::Float64: Maximal parameter step size growth.

  • Δp_decrease::Float64: Parameter step decrease factor upon rejection

  • Δt::Float64: Initial time step size.

  • Δt_max::Float64: Maximal time step size.

  • Δt_min::Float64: Minimal time step size.

  • Δt_grow::Float64: Maximal time step size growth.

  • Δt_decrease::Float64: Time step decrease factor upon rejection

  • Δu_opt::Float64: Optimal size of update for time stepping and embedding. The algorithm tries to keep the difference in norm between "old" and "new" solutions approximately at this value.

  • Δu_max_factor::Float64: Control maximum sice of update Δu for time stepping and embeding relative to Δu_opt. Time steps with Δu > Δu_max_factor*Δu_opt will be rejected.

  • force_first_step::Bool: Force first timestep.

  • num_final_steps::Int64: Number of final steps to adjust at end of time interval in order to prevent breakdown of step size.

  • handle_exceptions::Bool: Handle exceptions during transient solver and parameter embedding. If true, exceptions in Newton solves are caught, the embedding resp. time step is lowered, and solution is retried. Moreover, if embedding or time stepping fails (e.g. due to reaching minimal step size), a warning is issued, and a solution is returned with all steps calculated so far.

    Otherwise (by default) errors are thrown.

  • store_all::Bool: Store all steps of transient/embedding problem:

  • in_memory::Bool: Store transient/embedding solution in memory

  • log::Any: Record history

  • edge_cutoff::Float64: Edge parameter cutoff for rectangular triangles.

  • pre::Function: Function pre(sol,t) called before time/embedding step

  • post::Function: Function post(sol,oldsol,t,Δt) called after successful time/embedding step

  • sample::Function: Function sample(sol,t) to be called for each t in times[2:end]

  • delta::Function: Time step error estimator. A function Δu=delta(system,u,uold,t,Δt) to calculate Δu.

  • tol_absolute::Union{Nothing, Float64}

  • tol_relative::Union{Nothing, Float64}

  • damp::Union{Nothing, Float64}

  • damp_grow::Union{Nothing, Float64}

  • max_iterations::Union{Nothing, Int64}

  • tol_linear::Union{Nothing, Float64}

  • max_lureuse::Union{Nothing, Int64}

  • mynorm::Union{Nothing, Function}

  • myrnorm::Union{Nothing, Function}

Rely on LinearSolve.jl for linear system solution

This provides easy access to a large variety of linear solvers:

LU factorization from UMFPACK

umf_sol = solve(sys0; inival = 0.1, method_linear = UMFPACKFactorization(), verbose = true)
1×10201 Matrix{Float64}:
 1.02241e-33  0.0319717  0.045243  0.0554731  …  0.045243  0.0319717  1.02241e-33
@test isapprox(umf_sol, sol0, atol = 1.0e-7)
Test Passed

LU factorization from Sparspak.jl

sppk_sol = solve(sys0; inival = 0.1, method_linear = SparspakFactorization(), verbose = true)
1×10201 Matrix{Float64}:
 1.02241e-33  0.0319717  0.045243  0.0554731  …  0.045243  0.0319717  1.02241e-33
@test isapprox(sppk_sol, sol0, atol = 1.0e-7)
Test Passed

Iterative solvers

BICGstab from Krylov.jl with diagonal (Jacobi) preconditioner

The Jacobi preconditioner is defined in ExtendableSparse.jl.

krydiag_sol = solve(sys0;
                    inival = 0.1,
                    method_linear = KrylovJL_BICGSTAB(),
                    precon_linear = JacobiPreconditioner,
                    verbose = true,)
1×10201 Matrix{Float64}:
 1.02241e-33  0.0319717  0.045243  0.0554731  …  0.045243  0.0319717  1.02241e-33
@test isapprox(krydiag_sol, sol0, atol = 1.0e-5)
Test Passed
BICGstab from Krylov.jl with delayed factorization preconditioner
krydel_sol = solve(sys0;
                   inival = 0.1,
                   method_linear = KrylovJL_BICGSTAB(),
                   precon_linear = SparspakFactorization(),
                   verbose = "nlad",)
1×10201 Matrix{Float64}:
 1.02241e-33  0.0319717  0.045243  0.0554731  …  0.045243  0.0319717  1.02241e-33
@test isapprox(krydel_sol, sol0, atol = 1.0e-5)
Test Passed
BICGstab from Krylov.jl with ilu0 preconditioner

ILUZeroPreconditioner is exported from ExtendableSparse and wraps the predonditioner defined in ILUZero.jl .

kryilu0_sol = solve(sys0;
                    inival = 0.5,
                    method_linear = KrylovJL_BICGSTAB(),
                    precon_linear = ILUZeroPreconditioner,
                    verbose = true,)
1×10201 Matrix{Float64}:
 1.05391e-33  0.0319717  0.045243  0.0554731  …  0.045243  0.0319717  1.05573e-33
@test isapprox(kryilu0_sol, sol0, atol = 1.0e-5)
Test Passed

New verbosity handling

  • verbose can now be a Bool or a String of flag characters, allowing for control of different output categories. I would love to do this via logging, but there is still a long way to go IMHO

  • Allocation check is active by default with warnings which can be muted by passing a verbose string without 'a'. This is now the only control in this respect. All check_allocs methods/kwargs, control via environment variables have been removed.

  • Deprecation warnings can be switched off by passing a verbose string without 'd'.

  • Improve iteration logging etc., allow for logging of linear iterations ('l' flag character)

The following example gives some information in this respect:

D = 0.1
0.1
function xflux(f, u, edge)
    f[1] = D * (u[1, 1]^2 - u[1, 2]^2)
end
xflux (generic function with 1 method)
xsys = VoronoiFVM.System(0:0.001:1; flux = xflux, species = [1])
VoronoiFVM.System{Float64, Float64, Int32, Int64, Matrix{Int32}, Matrix{Float64}}(num_species=1)
solve(xsys; inival = 0.1, times = [0, 1]);

If we find these warnings annoying, we can switch them off:

solve(xsys; inival = 0.1, times = [0, 1], verbose = "");

Or we get some more logging:

solve(xsys; inival = 0.1, times = [0, 1], verbose = "en");

But we can also look for the reasons of the allocations. Here, global values should be declared as constants.

const D1 = 0.1
0.1
function xflux1(f, u, edge)
    f[1] = D1 * (u[1, 1]^2 - u[1, 2]^2)
end
xflux1 (generic function with 1 method)
xsys1 = VoronoiFVM.System(0:0.001:1; flux = xflux1, species = [1])
VoronoiFVM.System{Float64, Float64, Int32, Int64, Matrix{Int32}, Matrix{Float64}}(num_species=1)
solve(xsys1; inival = 0.1, times = [0, 1]);

v0.14

VoronoiFVM.System constructor

Implicit creation of physics

The VoronoiFVM.Physics struct almost never was used outside of the constructor of VoronoiFVM.System. Now it is possible to specify the flux functions directly in the system constructor. By default, it is also possible to set a list of species which are attached to all interior and boundary regions of the grid.

grid1 = simplexgrid(0:0.1:1);
function multispecies_flux(y, u, edge)
    for i = 1:(edge.nspec)
        y[i] = u[i, 1] - u[i, 2]
    end
end
multispecies_flux (generic function with 1 method)
function test_reaction(y, u, node)
    y[1] = u[1]
    y[2] = -u[1]
end
test_reaction (generic function with 1 method)
begin
    system1 = VoronoiFVM.System(grid1;
                                flux = multispecies_flux,
                                reaction = test_reaction,
                                species = [1, 2])
    boundary_dirichlet!(system1; species = 1, region = 1, value = 1)
    boundary_dirichlet!(system1; species = 2, region = 2, value = 0)
end;
sol1 = solve(system1);
@test isapprox(sum(sol1), 11.323894375033476, rtol = 1.0e-14)
Test Passed

Boundary conditions as part of physics

This makes the API more consistent and opens an easy possibility to have space and time dependent boundary conditions. One can specify them either in breaction or the synonymous bcondition.

function bcond2(y, u, bnode)
    boundary_neumann!(y, u, bnode; species = 1, region = 1, value = sin(bnode.time))
    boundary_dirichlet!(y, u, bnode; species = 2, region = 2, value = 0)
end;
system2 = VoronoiFVM.System(grid1;
                            flux = multispecies_flux,
                            reaction = test_reaction,
                            species = [1, 2],
                            bcondition = bcond2,
                            check_allocs = false);
sol2 = solve(system2; times = (0, 10), Δt_max = 0.01);
GridVisualizer(Plotter=CairoMakie)

time: 4.99

@test isapprox(sum(sol2) / length(sol2), 2.4921650158811794, rtol = 1.0e-14)
Test Passed

Implicit creation of grid

By passing data for grid creation (one to three abstract vectors) instead a grid, a tensor product grid is implicitly created. This example also demonstrates position dependent boundary values.

function bcond3(y, u, bnode)
    boundary_dirichlet!(y, u, bnode; region = 4, value = bnode[2])
    boundary_dirichlet!(y, u, bnode; region = 2, value = -bnode[2])
end;
system3 = VoronoiFVM.System(-1:0.1:1,
                            -1:0.1:1;
                            flux = multispecies_flux,
                            bcondition = bcond3,
                            species = 1);
sol3 = solve(system3);
@test isapprox(sum(sol3), 0.0, atol = 1.0e-14)
Test Passed

GridVisualize API extended to System

Instead of a grid, a system can be passed to gridplot and scalarplot.

scalarplot(system3, sol3; resolution = (300, 300), levels = 10, colormap = :hot)

Parameters of solve

The solve API has been simplified and made more Julian. All entries of VoronoiFVM.NewtonControl can be now passed as keyword arguments to solve.

Another new keyword argument is inival which allows to pass an initial value which by default is initialized to zero. Therefore we now can write solve(system) as we already have seen above.

reaction4(y, u, bnode) = y[1] = -bnode[1]^2 + u[1]^4;
bc4(args...) = boundary_dirichlet!(args...; value = 0);
system4 = VoronoiFVM.System(-10:0.1:10;
                            species = [1],
                            reaction = reaction4,
                            flux = multispecies_flux,
                            bcondition = bc4);
sol4 = solve(system4; log = true, damp_initial = 0.001, damp_growth = 3);
@test isapprox(sum(sol4), 418.58515700568535, rtol = 1.0e-14)
Test Passed


Built with Julia 1.10.2 and

CairoMakie 0.11.5
ExtendableGrids 1.2.3
ExtendableSparse 1.3.1
GridVisualize 1.5.0
ILUZero 0.2.0
LinearSolve 2.22.1
Pkg 1.10.0
PlutoUI 0.7.55
Revise 3.5.13
VoronoiFVM 1.16.0