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)
plot(s,sys)
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)
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)
Problem types and functions
ImmersedLayers.AbstractScalarILMProblem
— Typeabstract type AbstractScalarILMProblem{DT, ST, DTP} <: ImmersedLayers.AbstractILMProblem{DT, ST, DTP}
When defining a problem type with scalar data, make it a subtype of this.
ImmersedLayers.AbstractVectorILMProblem
— Typeabstract type AbstractVectorILMProblem{DT, ST, DTP} <: ImmersedLayers.AbstractILMProblem{DT, ST, DTP}
When defining a problem type with vector data, make it a subtype of this.
ImmersedLayers.@ilmproblem
— MacroThe @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 isYang3
.scaling =
to set the scaling type,GridScaling
(default) orIndexScaling
.dtype =
to set the element type toFloat64
(default) orComplexF64
.phys_params =
to pass in physical parametersbc =
to pass in boundary condition data or functionsforcing =
to pass in forcing functions or datamotions =
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 intimestep_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, thegrid::PhysicalGrid
andphys_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.
ImmersedLayers.BasicScalarILMProblem
— TypeBasicScalarILMProblem
ILM problem type dealing with scalar-type data.
ImmersedLayers.BasicVectorILMProblem
— TypeBasicVectorILMProblem
ILM problem type dealing with vector-type data.
ImmersedLayers.prob_cache
— Functionprob_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.
System types and functions
ImmersedLayers.ILMSystem
— Typemutable struct ILMSystem{static, PT, N, PHT, BCF, FF, DTF, MTF, BCT, ECT}
A system of operators and caches for immersed layer problems. This is constructed by construct_system
ImmersedLayers.construct_system
— Functionconstruct_system(prob::AbstractILMProblem) -> ILMSystem
Return a system of type of type ILMSystem
from the given problem instance prob
.
ImmersedLayers.update_system
— Functionupdate_system(sysold::ILMSystem,body::Body/BodyList)
From an existing system sysold
, return a new system based on the Body or BodyList body
.
ImmersedLayers.update_system!
— Functionupdate_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
.
Forcing functions
ImmersedLayers.AreaForcingModel
— Type 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
.)
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
.)
ImmersedLayers.LineForcingModel
— Type 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.
ImmersedLayers.PointForcingModel
— Type 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.
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.
ImmersedLayers.AreaRegionCache
— TypeAreaRegionCache(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.
AreaRegionCache(cache::BasicILMCache)
Create an area region that spans the entire domain, using the data in cache
to provide the grid details.
ImmersedLayers.LineRegionCache
— TypeLineRegionCache(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.
ImmersedLayers.PointRegionCache
— TypePointRegionCache(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.
ImmersedLayers.ForcingModelAndRegion
— TypeForcingModelAndRegion
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.
ImmersedLayers.arcs
— Methodarcs(lr::LineRegionCache)
Return the vector of arc length coordinates for the given line region lr
.
ImmersedLayers.mask
— Methodmask(ar::AreaRegionCache)
Return the mask for the given area region ar
.
ImmersedLayers.points
— Methodpoints(pr::PointRegionCache)
Return the vector of coordinates of points associated with pr
.
ImmersedLayers.apply_forcing!
— Functionapply_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.
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
.
Output functions and macros
ImmersedLayers.@snapshotoutput
— Macro@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,[args...];[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))
), and aux_state
is the same as aux_state(zeros_sol(sys))
, sys
is the system struct, t
is time, and args
are additional arguments 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)
.
This will also create a function name_xy
that operates on the same sets of arguments, but returns a spatially interpolatable version of the quantity name
, which can be accessed with x
, y
coordinates. In the case of the array with a time range, it returns a vector of such interpolatable objects.
ImmersedLayers.@scalarsurfacemetric
— Macro@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.
ImmersedLayers.@vectorsurfacemetric
— Macro@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.
ImmersedLayers.surfaces
— Functionsurfaces(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.
surfaces(sys::ILMSystem) -> BodyList
Return the list of surfaces (as a BodyList
) in the system sys
.
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.
surfaces(int::ConstrainedSystems.OrdinaryDiffEq.ODEIntegrator) -> BodyList
Return the list of surfaces (as a BodyList
) in the integrator int
.
This page was generated using Literate.jl.