Problems and the system

In specific problems that we wish to solve with immersed layers, there may be other data and operators that we would like to cache. We do this with an extra cache, which the user can define, along with a problem type associated with this cache. The basic cache and the extra cache are generated and associated together in a system.

There are a few basic ingredients to do this:

  • Create a problem type, using the macro @ilmproblem. This mostly just serves as a means of dispatching correctly, but also holds the grid and bodies so they can be passed along when the caches are constructed.
  • Create an extra cache type, making it a subtype of AbstractExtraILMCache. This can hold pretty much anything you want it to.
  • Extend the function prob_cache(prob,base_cache) to serve as a constructor for your extra cache, when your problem type is passed in.

Optionally, you can also extend the function solve in order to perform the steps of your algorithm. However, generically, you can just use pass in the system structure, which holds the basic ILM cache and your extra cache, into any function.

Example of problem and system use

We will demonstrate the use of problems and systems with the example given in A Dirichlet Poisson problem. Here, we will assemble the various additional data structures and operators used to solve this problem into an extra cache. We will also create a problem type called DirichletPoissonProblem, which we make a subtype of AbstractScalarILMProblem.

using ImmersedLayers
using Plots
using UnPack

Create your problem type

For this job, we have a macro

@ilmproblem DirichletPoisson scalar

This generates a type DirichletPoissonProblem for dispatch, and an associated constructor that we'll use later.

Create your extra cache

Here, we'd like this extra cache to hold the Schur complement and the filtering matrices, as well as some cache variables.

struct DirichletPoissonCache{SMT,CMT,FRT,ST,FT} <: ImmersedLayers.AbstractExtraILMCache
   S :: SMT
   C :: CMT
   forcing_cache :: FRT
   fb :: ST
   fstar :: FT
end

Extend the prob_cache function

We need this to construct our extra cache. This will get called when the system is constructed. We can make use of the data available in the base_cache (which is already constructed before this is called) in order to create the extra cache. (In the example here, we aren't quite ready to use the forcing_cache, so we just keep it blank for now. We will use it later.)

function ImmersedLayers.prob_cache(prob::DirichletPoissonProblem,base_cache::BasicILMCache)
    S = create_RTLinvR(base_cache)
    C = create_surface_filter(base_cache)
    forcing_cache = nothing
    fb = zeros_surface(base_cache)
    fstar = zeros_grid(base_cache)
    DirichletPoissonCache(S,C,forcing_cache,fb,fstar)
end

Extend the solve function

Here, we actually do the work of the algorithm, making use of all of the operators and data structures that we have cached for efficiency. The example below takes in some surface Dirichlet data fbplus, and returns the solutions f and s (filtered).

function ImmersedLayers.solve(fbplus,prob::DirichletPoissonProblem,sys::ILMSystem)
    @unpack extra_cache, base_cache = sys
    @unpack S, C, fb, fstar = extra_cache

    f = zeros_grid(base_cache)
    s = zeros_surface(base_cache)

    surface_divergence!(fstar,fbplus,base_cache)
    fb .= 0.5*fbplus

    inverse_laplacian!(fstar,base_cache)

    interpolate!(s,fstar,base_cache)
    s .= fb - s
    s .= -(S\s);

    regularize!(f,s,base_cache)
    inverse_laplacian!(f,base_cache)
    f .+= fstar;

    s .= C^6*s

    return f, s
end

Set up the grid, shape, and cache

We do this just as we did in Immersed layer caches, but now we don't create a cache, since it will be done internally.

Δx = 0.01
Lx = 4.0
xlim = (-Lx/2,Lx/2)
ylim = (-Lx/2,Lx/2)
g = PhysicalGrid(xlim,ylim,Δx)
RadC = 1.0
Δs = 1.4*cellsize(g)
body = Circle(RadC,Δs)
Circular body with 448 points and radius 1.0
   Current position: (0.0,0.0)
   Current angle (rad): 0.0

Do the work

We do this in three steps:

  • Create the problem instance. This is where we use the constructor generated by @ilmproblem
  • Call construct_system to create the caches, assembled into a system
  • Call solve to solve the problem.

Also, note that pretty much any function that accepts base_cache as an argument also accepts sys.

prob = DirichletPoissonProblem(g,body,scaling=GridScaling)
sys = construct_system(prob)

pts = points(sys)
f, s = solve(pts.u,prob,sys)

plot(f,sys)
Example block output
plot(s,sys)
Example block output

More advanced use, with keyword arguments

In the previous example, we supplied the surface data as an argument directly to the solve function. We also didn't have any right-hand side forcing. But in a more general case we might have more complicated boundary conditions or forcing information. This will be particularly true when we get to time varying problems.

So in this next example, we will demonstrate how the problem constructor can be used to supply more sophisticated information to the solve function. In particular, we will use three keywords:

  • The bc keyword to supply boundary condition information.
  • The forcing keyword to supply forcing.
  • The phys_params keyword to supply physical parameters.

In general, the philosophy is that we wish to enable the user to provide a function that supplies the required boundary or forcing data. There is a lot of freedom in this approach, as we will demonstrate.

Boundary conditions

Here, we wish to construct functions that return the value of f on the outside and inside of the surface. These functions will get called in the solve function, and they will be passed the base cache, the physical parameters. So we can make use of both of these to return the boundary data, e.g., here we return the x coordinate of the surface points on the outside of the surface, and zeros on the inside.

get_fbplus(base_cache,phys_params) = points(base_cache).u
get_fbminus(base_cache,phys_params) = zeros_surface(base_cache)
get_fbminus (generic function with 1 method)

Using the bc keyword, we can pass these along to the problem specification by various means. To make use of some routines that automate the application of these functions, we must pass them in a Dict, with the keys exterior and interior, respectively.

bcdict = Dict("exterior"=>get_fbplus,"interior"=>get_fbminus)
Dict{String, Function} with 2 entries:
  "interior" => get_fbminus
  "exterior" => get_fbplus

Forcing

Forcing can come in several forms. It can be

  • area forcing, in which the forcing is distributed over a whole region, possibly confined
  • line forcing, in which the forcing is distributed along a curve (but singular in the orthogonal direction)
  • point forcing, in which the forcing is singular at one or more points

In all cases, the user needs to supply some information about the shape of the forcing region and some information about the instantaneous strength of the forcing, in the form of a model function. These get bundled together into a forcing model.

For confined area forcing and for line forcing, the shape is supplied as a Body or BodyList, similar to bodies (serving as the mask for confined-area forcing and as the curve for line forcing). For point forcing, the shape is the set of discrete points at which we want to apply forcing. We can either supply these points' coordinates directly or provide them via a position function that returns the point coordinates. The advantage of the latter is that it allows the points to be changed later without regenerating the system.

In the example shown here, we apply point forcing at two points: one at (-1.0,1.5) and the other at (1.0,1.5), with respective strengths -1 and 1. We use a position function to provide the positions. This function must have the signature as shown here, accepting a general state argument, a time argument t, the cache for this forcing, and physical parameters. Any of these can be used to help determine the point positions. In this case, we just simply provide the coordinates directly. However, it is straightforward to pass these in via the state argument (or perhaps to update the positions by some velocity).

function my_point_positions(state,t,fr::PointRegionCache,phys_params)
    x = [-1.0,1.0]
    y = [1.5,1.5]
    return x, y
end
my_point_positions (generic function with 1 method)

The model function returns the strength of the forcing. In this point forcing example, it must return the strength of each point. It must be set up as an in-place function, accepting the vector of strengths and mutating (i.e. changing) this vector. Beyond that, it has the same signature as the position function.

function my_point_strengths!(σ,state,t,fr::PointRegionCache,phys_params)
    σ .= [-1.0,1.0]
end
my_point_strengths! (generic function with 1 method)

Now we bundle these together into the forcing model. We also tell it what kind of regularized DDF we want to use. We use the $M_4'$ DDF here. pfm will get passed into the problem setup via the forcing keyword.

pfm = PointForcingModel(my_point_positions,my_point_strengths!;ddftype=CartesianGrids.M4prime);

Now, we need to make sure that the forcing data is used. When we generate the extra cache, we use the function ForcingModelAndRegion to assemble the forcing's cache.

function ImmersedLayers.prob_cache(prob::DirichletPoissonProblem,base_cache::BasicILMCache)
    @unpack phys_params, forcing = prob
    S = create_RTLinvR(base_cache)
    C = create_surface_filter(base_cache)
    forcing_cache = ForcingModelAndRegion(forcing,base_cache)
    fb = zeros_surface(base_cache)
    fstar = zeros_grid(base_cache)
    DirichletPoissonCache(S,C,forcing_cache,fb,fstar)
end

We now redefine the solve function and use the boundary condition and forcing information. For the forcing, we make use of the function apply_forcing! which takes the forcing and automatically calculates the right-hand side field

function ImmersedLayers.solve(prob::DirichletPoissonProblem,sys::ILMSystem)
    @unpack bc, forcing, phys_params, extra_cache, base_cache = sys
    @unpack gdata_cache = base_cache
    @unpack S, C, forcing_cache, fb, fstar = extra_cache

    f = zeros_grid(base_cache)
    s = zeros_surface(base_cache)

    # apply_forcing! evaluates the forcing field on the grid and put
    # the result in the `gdata_cache`.
    fill!(gdata_cache,0.0)
    x, t = nothing, 0.0
    apply_forcing!(gdata_cache,f,x,t,forcing_cache,sys)

    # Get the prescribed jump in boundary data across the interface using
    # the functions we supplied via the `Dict`.
    prescribed_surface_jump!(fb,sys)

    # Evaluate the double-layer term and add it to the right-hand side
    surface_divergence!(fstar,fb,base_cache)
    fstar .+= gdata_cache

    # Intermediate solution
    inverse_laplacian!(fstar,base_cache)

    # Get the prescribed average of boundary data on the interface using
    # the functions we supplied via the `Dict`.
    prescribed_surface_average!(fb,sys)

    # Correction
    interpolate!(s,fstar,base_cache)
    s .= fb - s
    s .= -(S\s);

    regularize!(f,s,base_cache)
    inverse_laplacian!(f,base_cache)
    f .+= fstar;

    return f, C^6*s
end

Set up the problem

Now we specify the problem, create the system, and solve it, as before, but now supplying the boundary condition and forcing information with the bc and forcing keywords:

prob = DirichletPoissonProblem(g,body,scaling=GridScaling,bc=bcdict,forcing=pfm)
sys = construct_system(prob)
f, s = solve(prob,sys)
plot(f,sys)
Example block output

So we get the additional features from the sources. Now, suppose we wish to change the the boundary conditions or source points? We can do it easily without regenerating the cache and system, simply by redefining our bc and forcing model functions. For example, to create an internal solution, with surface data equal to the $y$ coordinate, and four sources inside.

get_fbplus(base_cache,phys_params) = zeros_surface(base_cache)
get_fbminus(base_cache,phys_params) = points(base_cache).v

function my_point_positions(state,t,fr::PointRegionCache,phys_params)
    x = [-0.2,0.2,-0.2,0.2]
    y = [0.2,0.2,-0.2,-0.2]
    return x, y
end
function my_point_strengths!(σ,state,t,fr::PointRegionCache,phys_params)
    σ .= [-1.0,1.0,-1.0,1.0]
end
my_point_strengths! (generic function with 1 method)

We can solve immediately without having to reconstruct the system, so it's very fast.

@time f, s = solve(prob,sys)
plot(f,sys)
Example block output

Problem types and functions

ImmersedLayers.AbstractScalarILMProblemType
abstract type AbstractScalarILMProblem{DT, ST, DTP} <: ImmersedLayers.AbstractILMProblem{DT, ST, DTP}

When defining a problem type with scalar data, make it a subtype of this.

source
ImmersedLayers.AbstractVectorILMProblemType
abstract type AbstractVectorILMProblem{DT, ST, DTP} <: ImmersedLayers.AbstractILMProblem{DT, ST, DTP}

When defining a problem type with vector data, make it a subtype of this.

source
ImmersedLayers.@ilmproblemMacro

The @ilmproblem macro is used to automatically generate a type particular to an immersed-layer problem, which can then be used for dispatch on those types of problems. It takes two arguments: the name of the problem (to which Problem will be automatically appended), and whether the problem is of scalar or vector type. For example,

@ilmproblem(StokesFlow,vector)

would generate a type StokesFlowProblem dealing with vector-valued data. The resulting type then automatically has a constructor that allows one to pass in the grid information and bodies, as well as optional choices for the DDF function and the scaling type. For the example, this constructor would be

StokesFlowProblem(grid,bodies[,ddftype=CartesianGrids.Yang3][,scaling=GridScaling])

Note that there is another constructor for problems with no surfaces that only requires that the grid information be passed, e.g.,

StokesFlowProblem(grid)

There are several keyword arguments for the problem constructor

  • ddftype = to set the DDF type. The default is Yang3.
  • scaling = to set the scaling type, GridScaling (default) or IndexScaling.
  • dtype = to set the element type to Float64 (default) or ComplexF64.
  • phys_params = to pass in physical parameters
  • bc = to pass in boundary condition data or functions
  • forcing = to pass in forcing functions or data
  • motions = to provide function(s) that specify the velocity of the immersed surface(s). Note: if this keyword is used, it is assumed that surfaces will move.
  • reference_body = to provide the ID of the body whose axes the problem is solved in
  • timestep_func = to pass in a function for time-dependent problems that provides the time-step size. It is expected that this function takes in two arguments, the grid::PhysicalGrid and phys_params, and returns the time step. It is up to the user to decide how to determine this. It could also simply return a constant value, regardless of the arguments.
source
ImmersedLayers.prob_cacheFunction
prob_cache(prob,base_cache::BasicILMCache)

This function is called automatically by construct_system to generate a problem-specific extra cache. Extend this function in order to generate an extra cache for a user-defined problem type. The user must also define the cache type itself.

source

System types and functions

ImmersedLayers.update_systemFunction
update_system(sysold::ILMSystem,body::Body/BodyList)

From an existing system sysold, return a new system based on the Body or BodyList body.

source
ImmersedLayers.update_system!Function
update_system!(sys::ILMSystem,u,sysold,t)

From an existing system sysold at time t, return a new system sys in place, based on the solution vector u, whose body state information will be used to replace the body information and subsequent operators in sysold.

source

Forcing functions

ImmersedLayers.AreaForcingModelType
  AreaForcingModel(shape::Union{Body,BodyList},transform::MotionTransform,model_function!)

Bundles a shape (i.e., a Body, BodyList,), a transform (to specify where to place the shape), and a model_function! (a function that returns the strength of the forcing) for area-type forcing. model_function! must be in-place with a signature of the form

  model_function!(str,state,t,fcache,phys_params)

where str is the strength to be returned, state the state vector, t is time, fcache is a corresponding AreaRegionCache and phys_params are user-supplied physical parameters. Any of these can be utilized to compute the strength.

There are a number of keyword arguments that can be passed in: ddftype =,specifying the type of DDF; spatialfield = to provide an AbstractSpatialField to help in evaluating the strength. (The resulting field is available to model_function! in the generated_field field of fcache.)

source
AreaForcingModel(model_function!)

Creates area-type forcing over the entire domain, using a model_function! (a function that returns the strength of the forcing).

model_function! must be in-place with a signature of the form

  model_function!(str,state,t,fcache,phys_params)

where str is the strength to be returned, state the state vector, t is time, fcache is a corresponding AreaRegionCache and phys_params are user-supplied physical parameters. Any of these can be utilized to compute the strength.

The keyword spatialfield = can provide an AbstractSpatialField to help in evaluating the strength. (The resulting field is available to model_function! in the generated_field field of fcache.)

source
ImmersedLayers.LineForcingModelType
  LineForcingModel(shape::Union{Body,BodyList},transform::MotionTransform,model_function!)

Bundles a shape (i.e., a Body, BodyList,), a transform (to specify where to place the shape), and a model_function! (a function that returns the strength of the forcing) for line-type forcing. model_function! must be in-place with a signature of the form

  model_function!(str,state,t,fcache,phys_params)

where str is the strength to be returned, state the state vector, t is time, fcache is a corresponding LineRegionCache and phys_params are user-supplied physical parameters. Any of these can be utilized to compute the strength.

The keyword ddftype = can be used to specify the type of DDF.

source
ImmersedLayers.PointForcingModelType
  PointForcingModel(pts::VectorData,transform::MotionTransform,model_function!)

Bundles point coordinates pts, a transform (to specify where the origin of the points' coordinate system is relative to the inertial system's origin), and a model_function! (a function that returns the strength of the forcing) for point-type forcing. model_function! must be in-place with a signature of the form

  model_function!(str,state,t,fcache,phys_params)

where str is the strength to be returned, state the state vector, t is time, fcache is a corresponding PointRegionCache and phys_params are user-supplied physical parameters. Any of these can be utilized to compute the strength.

The keyword ddftype = can be used to specify the type of DDF.

source
PointForcingModel(point_function::Function,transform::MotionTransform,model_function!::Function)

Bundles a point_function (a function that returns the positions of forcing points), a transform (to specify where the origin of the points' coordinate system is relative to the inertial system's origin), and a model_function (a function that returns the strength of the forcing) for point-type forcing.

point_function must have an out-of-place signature of the form

  point_function(state,t,fcache,phys_params)

where state the state vector, t is time, fcache is a corresponding PointRegionCache and phys_params are user-supplied physical parameters. Any of these can be utilized to compute the positions. It must return either a vector for each coordinate or VectorData.

model_function! must have an in-place signature of the form

  model_function!(str,state,t,fcache,phys_params)

where str is the strength to be returned.

source
ImmersedLayers.AreaRegionCacheType
AreaRegionCache(shape::Body/BodyList,cache::BasicILMCache)

Create an area region (basically, a mask) of the shape(s) shape, using the data in cache to provide the details of the grid and regularization.

source
AreaRegionCache(cache::BasicILMCache)

Create an area region that spans the entire domain, using the data in cache to provide the grid details.

source
ImmersedLayers.LineRegionCacheType
LineRegionCache(shape::Body/BodyList,cache::BasicILMCache)

Create a line region of the shape(s) shape, using the data in cache to provide the details of the regularization.

source
ImmersedLayers.PointRegionCacheType
PointRegionCache(pts::VectorData,cache::BasicILMCache[,kwargs])

Create a regularized point collection based on points pts, using the data in cache to provide the details of the regularization. The kwargs can be used to override the regularization choices, such as ddf.

source
ImmersedLayers.ForcingModelAndRegionType
ForcingModelAndRegion

A type that holds the forcing model function, region, and cache

Constructors

ForcingModelAndRegion(model::AbstractForcingModel,cache::BasicILMCache)

ForcingModelAndRegion(modellist::Vector{AbstractForcingModel},cache::BasicILMCache)

These forms generally get called when building the extra cache. They also gracefully generate an empty list of models if one passes along nothing in the first argument.

source
ImmersedLayers.arcsMethod
arcs(lr::LineRegionCache)

Return the vector of arc length coordinates for the given line region lr.

source
ImmersedLayers.apply_forcing!Function
apply_forcing!(out,y,x,t,fv::Vector{ForcingModelAndRegion},sys::ILMSystem)

Return the total contribution of forcing in the vector fv to out, based on the current state y, auxiliary state x, time t, and ILM system sys. Note that out is zeroed before the contributions are added.

source
apply_forcing!(dy,y,x,t,f::ForcingModelAndRegion,sys::ILMSystem)

Return the contribution of forcing in f to the right-hand side dy based on the current state y, auxiliary state x, time t, and ILM system sys.

source

Output functions and macros

ImmersedLayers.@snapshotoutputMacro
@snapshotoutput(name)

A function that returns some instantaneous output about a time-varying solution should have the signature name(state,constraint,aux_state,sys::ILMSystem,t[,kwargs...]), where state is a solution vector (of the same type as returned by state(zeros_sol(sys))), constraint is of the same type as returned by constraint(zeros_sol(sys))), sys is the system struct, t is time, and kwargs are any additional keyword arguments. This macro extends that function so that it will also operate on the integrator e.g, name(integrator), and the solution history array sol, e.g., name(sol::ODESolution,sys,t), using interpolation for values of t that are not explicitly in the solution array. It can also take a range of values of t, e.g, name(sol::ODESolution,sys,0.1:0.01:0.2).

source
ImmersedLayers.@scalarsurfacemetricMacro
@scalarsurfacemetric(name)

A function that returns a time-varying scalar surface metric should have the signature name(state,constraint,sys::ILMSystem,t,bodyid,[,kwargs...]), where state is a solution vector (of the same type as returned by state(zeros_sol(sys))), constraint is of the same type as returned by constraint(zeros_sol(sys))), sys is the system struct, t is time, bodyid is the body ID, and kwargs are any additional keyword arguments. This macro extends that function so that it will also operate on the integrator e.g, name(integrator,bodyid), and the solution history array sol, e.g., name(sol::ODESolution,sys,t,bodyid), using interpolation for values of t that are not explicitly in the solution array. It can also take a range of values of t, e.g, name(sol::ODESolution,sys,0.1:0.01:0.2,bodyid). And finally, it can simply take name(sol::ODESolution,sys,bodyid) to return the entire history.

source
ImmersedLayers.@vectorsurfacemetricMacro
@vectorsurfacemetric(name)

A function that returns a time-varying scalar surface metric should have the signature name(state,constraint,sys::ILMSystem,t,bodyid,[,kwargs...]), where state is a solution vector (of the same type as returned by state(zeros_sol(sys))), constraint is of the same type as returned by constraint(zeros_sol(sys))), sys is the system struct, t is time, bodyid is the body ID, and kwargs are any additional keyword arguments. This macro extends that function so that it will also operate on the integrator e.g, name(integrator,bodyid), and the solution history array sol, e.g., name(sol::ODESolution,sys,t,bodyid), using interpolation for values of t that are not explicitly in the solution array. It can also take a range of values of t, e.g, name(sol::ODESolution,sys,0.1:0.01:0.2,bodyid). And finally, it can simply take name(sol::ODESolution,sys,bodyid) to return the entire history.

source
ImmersedLayers.surfacesFunction
surfaces(u::ConstrainedSystems.ArrayPartition,sys::ILMSystem,t) -> BodyList

Return the list of surfaces (as a BodyList) in the solution vector u. If the surfaces are stationary, then this simply returns them from sys and ignores the time argument.

source
surfaces(sys::ILMSystem) -> BodyList

Return the list of surfaces (as a BodyList) in the system sys.

source
surfaces(sol::ODESolution,sys::ILMSystem,t) -> BodyList

Return the list of surfaces (as a BodyList) at the time t, using the ODE solution array sol. If the surfaces are stationary, then this simply returns them and ignores the time argument.

source
surfaces(int::ConstrainedSystems.OrdinaryDiffEq.ODEIntegrator) -> BodyList

Return the list of surfaces (as a BodyList) in the integrator int.

source

This page was generated using Literate.jl.