Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DiffEq's PDE Story 2nd Try: Domains, BCs, Operators, and Problems #260

Closed
ChrisRackauckas opened this issue Feb 23, 2018 · 107 comments
Closed

Comments

@ChrisRackauckas
Copy link
Member

ChrisRackauckas commented Feb 23, 2018

DiffEq's first PDE attempt (which started the package) was a failure. The issue was that it was tied directly to an FEM subpackage I made internally, but then it couldn't scale to all of the problems it needed to face. The rest of DiffEq was then built up around decentralization. The core idea is that if you separate the data that specifies a problem and leave the solver support to dispatch, many different approaches and packages can step in to offer unique (and fast) ways to solve the problems. That has worked splendidly, so the FEM support was dropped in DiffEq 4.0 since it didn't conform to this.

What I want to return with is a PDE story worthy of the rest of DiffEq. So the goals it needs to meet is:

  1. It should specify problems in a manner that is distinct from implementation. But,
  2. It should hold the information required for algorithms to be fully optimized with their unique features.
  3. All of the components to "do it yourself" as a toolbox should be exposed. And
  4. It should interface with many different approaches and packages. But,
  5. There should be high-level automation to make the standard cases easy and efficient for "most users".

Here is a prototype for a design that could hopefully handle this.

General Flow: Heat Equation FDM

Let's talk about the Heat Equation. First we start with a domain type. Domain types are specified by types <:AbstractDomain. For example,

domain = RegularGrid((0,5),dx)

is a regular grid of [0,5] with spacing dx. But now we have to start talking about boundaries. It's the domain that knows what boundaries it has, and thus what has to be specified. So to the domain we need to add boundary functions.

Boundary conditions are specified by types <:AbstractBoundaryCondition. So we could have:

lbc = Dirichlet(0)
rbc = Neumann(g(t,x))

Now we'd package that together:

space = Space(domain,lbc,rbc)

Those domains are compatible with those boundary conditions, so it works without error. Once we have a space, our Heat Equation is specified via:

u' = D(t,x,u)*A*u + f(t,x,u)

so we specify:

tspan = (0,15)
prob = HeatProblem(D,f,space,tspan)

This is just a bundle of information that describes what we want to solve. But how do we want to solve it? We need to do a call that converts it into something with usable operators. Let's do it method of lines via a conversion call:

ode_prob = ODEProblem(prob,FiniteDifference())

where FiniteDifference dispatches this conversion to DiffEqOperators.jl with default order. FiniteDifference(order=4) can set the spatial discretization order, or we can choose other packages with adaptive space operators etc. This we can take to the ODE solver and solve.

Details

Now let's talk about details.

Domains

Domains are a specification of the domain to solve on. The basic ones will be provided by DiffEqPDEBase.jl. Wrapper libraries such as DiffEqApproxFun.jl or FEniCS.jl could provide extras, like SpectralGrid or FEMMesh. It can be as lean as possible, though in some cases like FEM it will need to hold node/mesh pairings.

Boundary Conditions

These are defined in DiffEqPDEBase.jl and have the minimal information describing a specific kind of boundary condition. Either holding constants, arrays, or functions, they just encapsulate the idea of the BC choice.

Space

Space is how domains and BCs come together. For a given domain, it will make sure the BCs are compatible with the domain. Can this be merged with Domains?

Operators

This part wasn't shown in the simple example. One we have a space, that is enough information to define operators. The high level interface is something like

DerivativeOperator(space,alg,derivative_order,approximation_order)

etc, and then via alg it dispatches to the appropriate package to spit out DiffEqOperators (this stuff is already defined) appropriate for that space. So like DiffEqOperators would just need to be changed to implement that dispatch.

Problems

Problems are just an abstraction over operators which basically knows what operators it should be building given the space, alg, and problem. They don't do anything until the conversion call, in which case they build these operators to then build something to solve.

Conversions

The obvious conversions are to ODE/SDE/DAE/DDEProblems and keep on going. We need two more problem types though. LinearProblem and NonlinearProblem. Then we cover it all. Everything becomes instantiates operators through some conversion.

High-Level Conversions

Then there can also be some standard conversions like DiffusionAdvection(prob,space,tspan) where prob=SDEProblem. This is just high level sugar to make everything boil down to conversions on PDE problems to then give linear, nonlinear, or diffeqs out in the end.

Big Questions

Documentation

I've learned by now to plan documentation levels at the same time as the interface. Where are things documented? I think the general workflow is documented, and then boundary conditions are documented generally, some general domains, the operator calls, and problems are all documented package-wide generic. Then unique domains, space, operator algorithms, and conversions are documented per-package. DiffEqOperators describes no unique domains, it describes what BCs it can support, choices for operator construction, and problem conversions it can perform. DiffEqApproxFun links over to ApproxFun for choices of domains, has a single Spectral() for generating the spectral operators (with choices in there), and then can generate ODEs for the Heat Equation, or a fully implicit discretization to a LinearProblem via a 2D discretization (can it?).

Representation Information?

When you get this ODEProblem... what does it actually represent? We need a way to describe back to the user what u0 means. Coefficients of ...? u0[i,j,k] means what (x,y,z)? Not sure how to handle this.

Time?

Time is kept separate because it usually acts differently when it exists. For algorithms like a fully implicit discretization, LinearProblem(prob,FullyFiniteDifference(dt)) or something.

Passing space

This is just minor bikeshedding, but instead of like making the Heat Equation require the user give f(u,p,t,x,y,z) for 3D, should it be f(u,p,t,x) for x a tuple/number? That keeps it the same regardless of dimension. Minor bikeshedding.

Conclusion

This allows us to specify our problem at a high level, interface with different packages in an extendable way to generate appropriate FDM/FVM/FEM/spectral discretizations, and spit out linear/nonlinear/diffeq problems to take to solvers. The hope is that this give enough places to inject information and package-specific components that it can be efficient, yet gives a good level of automation and transferrability to other setups.

Thanks. Pulling in some people to bikeshed this high level story. @dextorious @dlfivefifty @alanedelman @jlperla

@ChrisRackauckas
Copy link
Member Author

To make this complete, let me describe the highly related DiffEqOperators interface. Basically, a DiffEqOperator is the discretization of an operator to L. There are many instantiations it can have, but the interface is that it can A_mul_B! and *, it can do conversions:

full(L) # Get a dense matrix
sparse(L) # Get a sparse matrix
banded(L) # Get a banded matrix
...

it can possibly expm or expmv, it acts as a standard DiffEq ODE function L(du,u,p,t) (for conversion purposes), and it has a dispatch for the following function:

update_coefficients!(L,u,p,t,x)

which updates internal coefficients of L to that setup. For example, the DiffEqArrayOperator can hold

L = a(u,p,t,x)*A

some quasi-semi-linear PDE discretization operator with a matrix A, and when you call update_coefficients! it will set it to the current time point and the current u values.

The reason why this is nice is that quasi-semi-time_dependent operators can all be appropriately evaluated via the sequence:

update_coefficients!(L,u,p,t,x)
L*u

Have a linear PDE and want the stationary solution (i.e. it's not time-dependent)?

update_coefficients!(L,nothing,p,nothing,x)
# Solve L*u = b

Want to write an Euler loop for a quasilinear PDE?

for i in 1:10
  t = t + dt
  update_coefficients!(L,u,p,t,x)
  u = u + dt*L*u
end

Etc. You see that these codes are operator independent, and independent of the implementation of the underlying operator. All it needs to know is that L* will be correct, and then it can use it as it needs to. You can update_coefficients and then call GMRES, or throw it into a nonlinear solver making sure you update_coefficients! each time. And this would take care of time-dependent BCs. While I showed this for a DiffEqArrayOperator which has an underlying matrix, the RegularFDMOperator from DiffEqOperators (it's getting a rename) is a lazy function for the FDM stencil, and update_coefficients!(L,u,p,t,x) sets the boundary conditions to the correct time to make L*u perform correctly (and then of course you can get sparse matrices and everything out as well). Since it holds the coefficients, RegularUpwindOperator can use update_coefficients! to calculate the directions and perform that correctly.

We plan to support special matrix types like BlockBandedMatrices.jl as well if that can be done generically, or that can be dispatching on the DiffEqOperator concrete type.

@jlperla
Copy link

jlperla commented Feb 23, 2018

Looks like a great start. I will put up more thoughts soon but a few to start... some are on general framework while some are on the gen:

  • I am going to leave off any comments on the higher level interface of the solvers such as HeatProblem and ODEProblem since those wouldn't be directly useful for the sorts of applications I have in mind at this time.
  • As examples of the boundaries, I think that Dirichlet0 and Neumann0 are worthwhile as special cases because they are not affine (which can help do specialized algorithms when dispatching solutions).
  • Can you give more details on what you are thinking about for the DerivativeOperator? In particular,
    • What are examples of the alg you would have in mind
    • How do I get the discretized operator from it?

@dlfivefifty
Copy link
Contributor

Some comments:

  1. using lbc and rbc are bad ideas: they don't scale to higher dimensions, and many functional constraints are not "left" or "right".
  2. Space is not the right word if you are specifying non-zero boundary conditions.
  3. Why does Neumann(g(t,x)) depend on x?
  4. I don't understand why the domain is called RegularGrid but doesn't have a discretisation size.

@jlperla
Copy link

jlperla commented Feb 23, 2018

Sorry, just say that you answered the discretization question, but would love to see examples of alg

@jlperla
Copy link

jlperla commented Feb 23, 2018

It would be nice to have Space separated from the discretization... spectral methods could operate on the space and the boundaries in different ways. These are only combined with a Grid for finite difference methods? i.e.

domain = CartesianGrid((0,5)) #presumably could take the cartesian product of multiple tuples
space = CartesianSpace(domain, (lbc,rbc)) #Calling it cartesian because this would be specific to a particular higher dimension

Then the key thing I am missing is whether the DerivativeOperator is intended to be the abstract derivative, or to have the particular discretization involved (which I think is what your alg has in it) Would be nice to separate...

grid = RegularGrid(space, dx) #Potentially `ChebyshevDiscretization`, etc. 
op = DerivativeOperator(space, derivative_order)
L = FiniteDifferenceOperator(op, grid, alg, approximation_order).
update_coefficients!(L,u,p,t,x)

I don't have strong feelings about the separation of the DerivativeOperator and the particular discretization, but the space vs. domain would be helpful.

@ChrisRackauckas
Copy link
Member Author

using lbc and rbc are bad ideas: they don't scale to higher dimensions, and many functional constraints are not "left" or "right".

Yes, that was just for RegularGrid which had only one tuple, i.e. a 1D regular grid. Space(domain,bcs...) would have to check to make sure there's enough BCs. For example, for RegularGrid((0,1),(0,1),dx,dy) it's a 2D regular grid, so

space(lbc,rbc,tbc,bbc,corner1,corner2,corner3,corner4)

would be what's required. Documenting this needs to be done by pattern for nD domain/space pairs, so there must be a better idea. But that crucial step is why I wanted to leave open the possibility to separate domain and space, maybe it's easier like this? What's gained? I don't really know that fully yet.

Space is not the right word if you are specifying non-zero boundary conditions.

I agree, but I didn't want the lack of a good name to pause the release of the idea any more.

Why does Neumann(g(t,x)) depend on x?

In case it's for a whole side of the domain. Left of a 2D grid needs a function.

I don't understand why the domain is called RegularGrid but doesn't have a discretisation size.

It has dx.

I am going to leave off any comments on the higher level interface of the solvers such as HeatProblem and ODEProblem since those wouldn't be directly useful for the sorts of applications I have in mind at this time.

I'm sure most people who read this won't be using that part, just the operators (or they will be building this level of the abstraction 👍 ). But this is how we support users who say "how do I solve the Diffusion-Advection equation?" Obviously they can do it themselves by building the operators, but it's not hard with this tooling to give them a function that spits out an ODE to solve. Also, this is a great place for undergrad and GSoC projects IMO.

As examples of the boundaries, I think that Dirichlet0 and Neumann0 are worthwhile as special cases because they are not affine (which can help do specialized algorithms when dispatching solutions).

Yes. They will be there.

What are examples of the alg you would have in mind

The "standard" one will be the current DiffEqOperators one. It might even good to have defaults for this. But for example, someone can write a package which computes fast Laplacians for regular girds, and so they can let FastLaplacian() dispatch to their package (or some wrapper over their package) and spit out a DiffEqOperator that does the discretization their way. Since we support lazy operators in this, there are so many different ways to define the same mathematical operator that we want to leave open the possibility of better data structures. DiffEqOperators.jl would do the 2D Laplacian for the Heat Equation via two of its operators and then Ay*u + u*Ax. Someone can write a FastLaplacians.jl could be created to make FastLaplacian() write the fused loop version for some very specific 2D operator choices, and thus it can be a good dispatch to use in that specific case. That's the point there.

How do I get the discretized operator from it?

See the stuff on the <:AbstractDiffEqOperator interface.

Then the key thing I am missing is whether the DerivativeOperator is intended to be the abstract derivative, or to have the particular discretization involved (which I think is what your alg has in it

If I make it lower case, would it be easier to understand that it's supposed to be a high level "give me a discretization of this derivative to this order that's appropriate for this space using this method". derivative_operator(space,2,2,FiniteDifference()).

@dlfivefifty
Copy link
Contributor

It's not possible to know what boundary conditions are needed without knowing the differential operator, and even if you know the differential operator its non-trivial.

@ChrisRackauckas
Copy link
Member Author

I see what you mean. So then it'll just have to error at the problem/operator construction call instead of the space construction time, meaning there's no reason for Space and we might as well add the BCs to the Domain?

@dlfivefifty
Copy link
Contributor

Bcs should either be in the operator, or in the basis/space (for zero conditions). Putting them in the domain doesn't really make sense.

Also, there's some work on a general purpose Domains.jl package: https://github.com/daanhb/Domains.jl

@jlperla
Copy link

jlperla commented Feb 23, 2018

For stochastic processes (I can't say much about physics applications) the boundaries are connected to the domain: reflecting or absorbing barriers.

The problem with associating them with the operator itself is that Chris has the idea of composing the operators lazily. It is only at the level of the completely composed operator that you can apply the boundary values (for stochastic processes at least).

Are we sure the operator composition approach would work (for boundaries which end up affine)?

@ChrisRackauckas
Copy link
Member Author

The problem with associating them with the operator itself is that Chris has the idea of composing the operators lazily. It is only at the level of the completely composed operator that you can apply the boundary values (for stochastic processes at least).

Yeah, this is where the idea comes from. The first approach I had in mind was:

domain = RegularGrid((0,5),dx)
lbc = Dirichlet(0)
rbc = Neumann(g(t,x))
bcs = BoundarySet(lbc,rbc)
coeff(u,p,t,x) = x^2 + t^2
A = derivative_operator(domain,bcs,FiniteDifference(),coeff,2,2)
L = derivative_operator(domain,bcs,LazyUpwind(),coeff,2,2)

but @jlperla asked whether there's a way to make sure the same BCs are satisfied, in which case I thought we might as well package the BCs into the "domain". Maybe that's still fine but we need a new name?

Are we sure the operator composition approach would work (for boundaries which end up affine)?

Yes. There's the odd issue that LinearProblem will have to modify your b to make the "real" linear operator actually linear, but that's just a detail. Whether composing lazy operators is a good idea is what I'm not so sure about.

@dlfivefifty
Copy link
Contributor

dlfivefifty commented Feb 23, 2018

As soon as you associate the boundary conditions with the domain, it (1) breaks generality, as you can't impose constraints like \int_Ω u(x) dx = 0 and (2) doesn't make sense in the ∞ -dimension setting, where bcs are either side constraints, or incorporated in the function space.

@ChrisRackauckas
Copy link
Member Author

ChrisRackauckas commented Feb 23, 2018

breaks generality, as you can't impose constraints like \int_Ω u(x) dx = 0

Wouldn't that just be IntegralConstraint(0) and stick that in the domain? Of course domain construction won't be able to error since it won't know whether it has enough BCs for something like that, but then it can error with that information at operator construction? It would simply have the BCs tagging along in the domain type without checking whether that's a sensible grouping, just to make sure every operator gets the same BCs. Is there a case where you'd want different operators to have different constraints? If there's a case of that, then yes let's split it out.

doesn't make sense in the ∞ -dimension setting, where bcs are either side constraints, or incorporated in the function space.

That's covered by the answer to the first, as either BCs are just a collection sent to every operator build (so it doesn't matter if there's some weird ones or none), or it needs to be at the operator instantiation call.

@dlfivefifty
Copy link
Contributor

I don't think I understand: an FD second-derivative maps n points to n-2 points, then you add whatever boundary conditions you want (by convention at the top and bottom). So the boundary conditions should always be an after thought?

@ChrisRackauckas
Copy link
Member Author

Thinking less mathematically and more practically,

A = derivative_operator(domain,bcs,...)
L = derivative_operator(domain,bcs,...)

would I ever want those bcs (or more generally, constraints) to be different? If every operator has to always satisfy the same constraints, do they need to be kept separate?

@jlperla
Copy link

jlperla commented Feb 23, 2018

The main question I have is whether the generality to allow lazily composing operators is worth the complexity in interface complexity and short-term performance. With loop fusion, etc. it may be possible to generate an overhead free implementation, but it is going to be tricky.

For economics and finance applications at least, there are very few finite difference discretizations we would need (e.g. for a diffusion process: central differences on a diffusive second derivative and upwind on the first-derivative drift).

@dlfivefifty
Copy link
Contributor

I don’t understand why the derivative takes in bcs: it seems to be conflating the application of the operator with the inversion of the operator.

@jlperla
Copy link

jlperla commented Feb 23, 2018

For example, we could have a specialized operator for a diffusion process which generates the coefficients given the passed in boundary functions:

domain = RegularGrid((0,5),dx)
mu(x) = .... mydrift function
sigma(x) = ... my variance function
mu_x = mu.(domain.grid)
sigma_x = sigma.(domain.grid)
A = upwind_diffusion_operator(domain, mu_x, sigma_x, (Neuman0, Neuman0))

@ChrisRackauckas
Copy link
Member Author

The main question I have is whether the generality to allow lazily composing operators is worth the complexity in interface complexity and short-term performance.

What can be dropped/simplified if we aren't composing operators lazily? None of the PDE story has that in it, that's the DiffEqOperator stuff which users can pretty much ignore.

@ChrisRackauckas
Copy link
Member Author

I don’t understand why the derivative takes in bcs: it seems to be conflating the application of the operator with the inversion of the operator.

Even forward application of the operator requires the BCs. A*u for the Laplacian has a different result for periodic BCs vs Neumann vs Dirichlet etc. I'm not sure it can be well-defined without it.

@jlperla
Copy link

jlperla commented Feb 23, 2018

DiffEqOperator stuff which users can pretty much ignore.

For me at least, I have no use for the higher level "heat equation" and "odeproblem" solution methods, so it is the DiffEqOperator which I would directly use at this point.

What could be simplified:

  • this whole discussion of the connection between boundary values from the operators goes away as users only use the fully composed operator along with the boundary values they specify.
  • We don't have to worry about the details of loop fusion when all we want is the sparse(A) etc.
  • I am still not convinced that boundary values that create affine setups are that easy to generalize with decomposed operators.
  • People who are not generic programming geniuses can add to the library of discretization of differential operators if they just work through the finite-difference algebra.

Now, if in the underlying code for the operator you want to use lazily decomposed operators, that is an implementation detail, but we are discussing the higher level interface here.

@dlfivefifty
Copy link
Contributor

The derivative of a function can’t “see” a single point (or rather it’s just not defined at a point where it’s not differentiable), so I don’t agree that the notion of derivative is different for Dirichlet and Neumann.

Maybe I’m missing the point of the conversation: is the point of this discussion to make a system for doing finite differences (and only finite differences), or is the point to make something general purpose where you can swap out the Discretization method?

@ChrisRackauckas
Copy link
Member Author

ChrisRackauckas commented Feb 23, 2018

this whole discussion of the connection between boundary values from the operators goes away as users only use the fully composed operator along with the boundary values they specify.

The stencil matrix still has to be modified at the edges to satisfy the BCs. That's what the naive loop is essentially doing.

We don't have to worry about the details of loop fusion when all we want is the sparse(A) etc.

Where do you have to worry about that? Show me an example of where you have to worry about the details of loop fusion. Just get and sparse the operator if all you want is the sprase operator?

I am still not convinced that boundary values that create affine setups are that easy to generalize with decomposed operators.

full(A)->(A,B) which is A*u+B when it has to.

People who are not generic programming geniuses can add to the library of discretization of differential operators if they just work through the finite-difference algebra.

Is this about the lazy implementation of DiffEqOperators via Fornberg's algorithm? That has nothing to do with the interface... but anyways, even that has been mostly built by undergrads so it's at least at that level. Most of the code is just a loop over a stencil array. But this has nothing to do with the interface.

@ChrisRackauckas
Copy link
Member Author

ChrisRackauckas commented Feb 23, 2018

Maybe I’m missing the point of the conversation: is the point of this discussion to make a system for doing finite differences (and only finite differences), or is the point to make something general purpose where you can swap out the Discretization method?

The latter. For example, this should be generic enough that I can have a wrapper over an ApproxFun domain, specify some BCs in the DiffEq style, derivative_operator kicks out some matrices for the discretized operator, and then use that to build an ODEProblem etc. If it can't do that, then it failed.

The derivative of a function can’t “see” a single point (or rather it’s just not defined at a point where it’s not differentiable), so I don’t agree that the notion of derivative is different for Dirichlet and Neumann.

But the differential operator is the derivative at each point in the discretization. At some point that operator has to be instantiated, and in order to know its action it has to have the BCs in it.

@dlfivefifty
Copy link
Contributor

But in FD it shouldn’t be each point: each derivative drops one point. So D maps values at n points to values at n-1 points, D^2 maps values at n points to n-2 points and so on.

@dlfivefifty
Copy link
Contributor

Only in the periodic case (which really is a different domain since the topology is a 1-torus) do you get a map from n points to n points.

@jlperla
Copy link

jlperla commented Feb 23, 2018

When I said

this whole discussion of the connection between boundary values from the operators goes away as users only use the fully composed operator along with the boundary values they specify.

What I meant was that if the user only directly works with fully composed operators in their higher level interface, then we don't have the current issue we are discussing with the boundary values being connected to the derivative. If the user works directly with the fully composed differential operator, then it makes sense to group that with the appropriate boundaries.

@ChrisRackauckas
Copy link
Member Author

But in FD it shouldn’t be each point: each derivative drops one point. So D maps values at n points to values at n-1 points, D^2 maps values at n points to n-2 points and so on.

Only in the periodic case (which really is a different domain since the topology is a 1-torus) do you get a map from n points to n points.

Not necessarily. If you know the BCs then you can incorporate them into the operator to make the map not drop points. The periodic case is well known where you just put stencil coefficients in the upper right and lower left corner of the matrix (thinking about it as a matrix). But for Dirichlet/Neumann, in order for the BCs to be satisfied it defines what the values have to be at the boundaries, giving you an n -> n map by filling in the ends via the BCs. I'm trying to think of whether there's a case that can't be true for.

@dlfivefifty
Copy link
Contributor

That incorporation is exactly equivalent to a step of Gaussian elimination ... I don’t see why you would want to try doing Gaussian elimination before you’ve setup the problem ..

@jlperla
Copy link

jlperla commented Feb 23, 2018

For a user in my field, what they really want is to (1) specify the stochastic process and (2) specify what happens at the boundaries of that stochastic process . There are only two types of boundaries that matter: (a) a reflecting barrier and (b) an absorbing barrier.

I would love to specify these things in an abstract way independent of the discretization, but it isn't necessary. In fact, there are practical considerations (i.e. in solving HJBE the drift changes all the time as part of the algorithm, and you solve for the new drifts at grid points) that make sticking with finite differences reasonable for this exact problem.

So, in my way of thinking you are having me specify the https://en.wikipedia.org/wiki/Infinitesimal_generator_(stochastic_processes) for the stochastic process directly, which is fine. If you want me to specify it in parts and have lazy composition then I think that is fine as well, but somehow you are going to have to attach the boundary types to it when you generate the full matrix that puts things together. The general algebra behind that is not at all obvious to me, let alone the numeric implementation or what happens when I want to take the adjoint (as you do when moving between solving the KBE and KFE).

@ChrisRackauckas
Copy link
Member Author

ChrisRackauckas commented Mar 3, 2018

So here's the gist of it. I think I have most of it clear now. Copying my notes for future reference.

The domain is discretized and u is a vector of points on that space. The indices for the boundary are bidxs defined by the domain and for the interior iidxs s.t. their union is the full u.

R is the restriction operator which is defined by the domain. It is the operation which is defined as R*u = u[iidxs]. This is unique since it's given by the definition of the domain's discretization, and is thus I where you 0 remove columns which aren't in the interior. u is in the space of (discretized) functions which satisfy the boundary conditions, and v is the space of R*u, the interior functions which could satisfy the BCs. For a 1D discretization, it's n-2 x n.

B is the boundary condition operator. In 1D, it is defined as satisfying B*u = [bc1;bc2] for any u in the space of functions that satisfy the BCs, for the conditions bc1 and bc2. B is not necessarily unique and is instead a choice. The choice of B is exactly the choice of boundary value discretization, i.e. choosing to do 1st or second order Neumann BCs is simply the choice of the operator B (first order for example is the choice that bc1 = u[1] - u[2] for Neumann0). For Dirichlet, or after a discretization of the derivative is chosen in Neumann or Robin, this B is unique. From looking at the relation it has to satisfy, you see it's 2 x n.

Q is the operator that is defined as Q*R*u = u for any u that satisfies the BCs (or Q=R^(-1), but be careful there since it's not square and this is only true as maps on functions which satisfy the boundary). It has a second relation B*Q*v = [bc1;v;bc2], basically saying that it's a map from discretizations of functions in the interior to functions which satisfy the BCs. Q is actually in general affine, meaning that Q*u = Qa*u + Qb. You can show some nice properties of affine operators, like that the definition given of * will make Q*x=b in an iterative solver converge to Qa*x = b-Qb (so if we actually define a type that does this in Julia, it will work with iterative solvers without a fuss. Just the same thing idea as writing in residual form). From the relations you get that Qa is n x n-2 and Qb is of length n. In order for Q*R*u = u to hold for trivial u, you need that Qb is zero except on Qb[bidxs] (so 2 points for 1D). For the relation B*Q*v = [bc1;v;bc2] to hold on trivial v, you need that Q is I on the interior, so it's defined by its first and last rows. I believe that makes it 4 linear conditions to map to 4 points so Q is unique.

Now let's say we want to solve u_t = A*u, abuse notation so that it means both the discretized and non-discretized forms. Then we have two relations:

A[:,bidxs]*(B[:,bidxs]\[bc1;bc2]) = A*Qb

and

R*(A - A[:,bidxs*(B[:,bidxs]\B)) = A*Qa

I don't have a good way showing why that should be true though, so maybe @dlfivefifty can chime in on how that definition actually works. So that can be used to derive Q systematically, or in special cases it can be done more directly (later).

But once we're at this point, to solve the actual differential equation we only want to solve the interior, since the boundaries are given directly by the interior u = Q*v and it would be numerically unstable to solve an equation with more points than there are degrees of freedom in the problem. Therefore we actually want to solve v_t = A*Q*v. Notice that the discretized A maps from the full domain to the interior (since the PDE is only defined on the interior). Notably, that means it's not square. By the operator algebra we have:

v_t = A*Qa*v + A*Qb

is how the operator is applied. For a steady state problem, it's the same idea. Discretize some f in the domain for f = A*u, and you get f = A*Qa*v + A*Qb. Those are the linear equations which define the ODE or whose solution is the solution to the PDE.

This shows that what's important and non-trivial is Q. It contains all of the information about both our boundary and our domain from the two relations. So what is Q? Here's all of the relevant cases for finite differencing.

Dirichlet0. This case is easy. If you have any v in the interior, its unique extension to satisfying the BCs is sticking 0s on the end, so Q = [0;I;0] is linear, meaning Qa=Q and Qb=0.

Dirichlet. The boundary is not dependent on any points in the interior, and therefore Qa = [0;I;0], but to satisfy the BCs you need Qb=[bc1;0s;bc2].

Neumann. You have to pick a discretization of B. This defines linear relation between the interior points and the BC. For example, 1st order discretization of the boundary derivative means that db/dx = (u[2] - u[1])/dx = bc1, and therefore u[1] = u[2] - dx*bc1. You can see that this means Qb = [dx*bc1;0s;dx*bc2], while Qa is I for the interior but [1,0,0,...] for the first row (so that Qa*v = v[1] = u[2]) and [...,0,0,1] for the last row.

Robin. Left as an exercise for the reader :P. It's just the same ideas from before to write out the affine relation. It needs a choice of B just like Neumann.

And that's all of the FDM discretizations. Let's take a quick look at the spectral discretizations then. In this case, let u be the discretization using n coefficients. We then define the interior as chopping off the last two coefficients, so R = I[1:n-2,:]. Now for Q the boundary determining nodes are at the end. Q needs to take into account the space. Let c[i] = T[i](-1) for T[i] the ith Chebyschev function (or it could be any expansion function). Let's do Dirichlet dot(v,c) = bc1 (the evaluation relation) and for d on the other end dot(v,d) = bc2. Thus the relation of extension is linear, meaning Qb = 0 and Qa = [I;c;d] (Qa*v = [v;bc1;bc2]). What's nice is that in this space, arbitrary Robin is actually linear. Using dc[i] = dT[i](-1) for the derivative, it's clear how that works out, and then Robin is just a linear combination of those two sums, so then it's just a linear combination of those two Qa.

We can actually make clear where the affine-ness comes from from this. If you let T[i] be the hat function basis, then the spectral discretization is the same as the FDM discretization. Why does it come out affine then? It's because it's degenerate: T[i](bc)=0 for every hat function for the interior (since they are zero at every node other than themselves), so this relation comes out to dot(v,0) = 0 ?= bc1 and so you have to go affine to add the bc on. So, in other words, Q is linear unless its interior basis expansion is degenerate at the boundaries and the condition is non-zero Dirichlet. We can also imagine this as being true for Neumann if the basis has zero derivative at the boundary points.

Of course this extends to finite element methods since it's usually discussed in an operator form much like this. So this covers the different discretizations as long as you can find Q (and maybe that way above is a nice way to generally do it?). Either way, what's left is just getting the interior stencil for the discretization for A. For even and uneven grid FDM that's just the Fornberg method which is the interior part in DiffEqOperators.jl, and for spectral discretizations that's just querying ApproxFun.jl.

@ChrisRackauckas
Copy link
Member Author

ChrisRackauckas commented Mar 5, 2018

I wanted to finish up with the discussion of how exponentiation of the operator works. Using the notation of the previous post, we have that our diffeq is given via:

v_t = A*Q*v

where Q is possibly affine, but assume it's linear right now. Then A*Q is a square n-2 x n-2 operator, so if we define that as L then

v_t = L*v

and it's clear that we just use exp(dt*L) in the exponential integrators.

But now let's take Q being affine. We can expand it out. If the original PDE was semilinear, then we would get

v_t = A*Qa*v + A*Qb + f(t,v)

but then if we define L=A*Qa and f̃ = A*Qb + f(t,v), then we get

v_t = L*v + (t,v)

which is the semilinear ODE with a square L that exponential integrators want. Thus in this framework it's not difficult to understand why it would work, how to change the implementations, and how to handle the affine part (we'll need an expmv to only do the linear part, and have a way to query the Qb part and have the integrator add it to the user's f. Annoying but not difficult).

I think that covers the questions for how to get it right. Now we need to implement it all....

Reference: the Chebfun paper https://academic.oup.com/imajna/article/36/1/108/2363563

@ChrisRackauckas
Copy link
Member Author

The abstract definition of the operator, along with domains, has been punted to SciCompDSL.jl. Operator instantiation will be the dispatches discussed above for DiffEqOperators.jl and DiffEqApproxFun.jl.

@dlfivefifty
Copy link
Contributor

At some point we should talk about unifying ApproxFun.Operator with DiffEqOperators, etc., with an eye to both lazy and non-lazy variants. Even if it's only at the level of an OperatorsBase.jl that standardises the interface. (I always mean linear operator between spaces when I say "operator", which is different than LinearOperators.jl which seems to mean lazy matrices.)

@ChrisRackauckas
Copy link
Member Author

I think we're going to have to make such an interface in SciCompDSL.jl, so that's where it will likely happen. It's getting hard to predict though, I think it just needs to get done and then refactored after we have something working.

@dlfivefifty
Copy link
Contributor

Maybe the bests steps are:

  1. You design things the way you think they should be
  2. I see how your design works and try to find common ground
  3. I make an OperatorsBase.jl and submit a PR to SciCompDSL.jl
  4. We iterate on the PR

@ChrisRackauckas
Copy link
Member Author

Yup, sounds good.

@dlfivefifty
Copy link
Contributor

FYI I think this approach to boundary conditions works for eigenvalue problems too.

@ranocha
Copy link
Member

ranocha commented Mar 28, 2018

I am not sure whether this approach works for general PDEs and discretisations. As far as I've understood the discussion above, the boundary conditions are imposed in a strong sense, i.e. the numerical solution has to fulfil the boundary conditions exactly. For hyperbolic conservation laws (linear advection, wave equation, Burgers' equation, Euler, ...), the imposition of boundary conditions might be a little bit different. Such a strong enforcement of boundary conditions can result in unstable methods. Therefore, summation-by-parts (SBP) operators using simultaneos approximation terms (SATs) have been designed to construct provably stable semidiscretisations (https://doi.org/10.1016/j.jcp.2014.02.031). The basic idea is to mimic integration by parts discretely by SBP operators, i.e. (finite difference) derivative operators that are defined on the complete grid, including the boundary nodes. In the interior, these derivative operators can be just the classical centered derivative operators. Near the boundary, special boundary stencils are chosen to mimic integration by parts discretely. Thus, the derivative is computed without using boundary conditions.

To enforce boundary conditions, the SATs are added at the boundary nodes. In the simplest case, these terms contain some difference of the value of the numerical solution at the boundary and the desired boundary value. Thus, the numerical solution does not necessarily satisfy the boundary conditions exactly and the numerical solution has to be computed at all nodes (including the boundary nodes). However, this procedure allows the creation of provably stable semidiscretisations for hyperbolic equations.

If I understood the discussion above correctly, I don't know how such an approach can be represented in the framework discussed above. For hyperbolic equations, well-posed boundary conditions might be specified only at certain boundaries and not everywhere (inflow or outflow). Moreover, whether it is possible to enforce (Dirichlet) boundary conditions can also depend on the numerical solution itself. But this might be handled by the corresponding semidiscretisation.

I can see how the approach discussed above works for parabolic PDEs. Please, correct me if I'm wrong, but I don't see immediately how this approach can be extended to weakly imposed boundary conditions using SATs.

@ChrisRackauckas
Copy link
Member Author

@ranocha brings up a good point. In fact, what it basically points out is that this version of boundary condition handling we have been discussing is only compatible with linear boundary conditions, basically boundary conditions of the form

c_0*u(x) + c_1*u'(x) + c_2*u''(x) + ...

since then you can discretize the derivative terms and then the expansion operator has to be a linear operator. But if you have a global boundary condition or something which is nonlinear, like Integral ||u^2(x)||=1, then it cannot be handled like this.

But I think it highlights that what really needs to be discussed is what we want out of the PDE story. (Note: I'm mostly using this to prepare my thoughts for today's workshop). Essentially, we want two things:

  1. A high level user interface so that way people who don't know PDEs well can solve the ones that they need to.
  2. Developer level tools that make it easy to discretize a PDE.

What we tried to layout was a formulation for (2), which ran into multiple issues which we could get different answers for, but I think that it's ultimately doomed to failure because the developers need the flexibility in order to handle whatever wonky PDEs and boundary conditions that they are interested in. So I think it makes sense to abandon (2) as seemingly impossible without giving up both performance and feature-completeness. FEM, FDM, etc. libraries are going to have to act differently in order to exploit their unique features, and anyone working at that level will need to make tough but isolated decisions. DiffEq can hold a variety of tools like this, with DiffEqOperators being the FEM operators with linear boundary conditions which is core to many discretizations, while others like ApproxFun play a central role in the surrounding ecosystem as well.

But (1) is the goal we should be trying to attain. The question is stated as: how can someone specify a high level what PDE they want to solve without simultaneously specifying a discretization and solution method? Mathematically the PDE doesn't have a discretization, that's just the numerical method, so it should be possible to setup an interface where a user writes down a PDE, chooses a method, and then a solver method runs. There's two possible levels this can be written at. First of all, it can be written at a level like AbstractHeatProblem (Semilinear Heat Equation with Robin BCs), where there's set problems to discretize and solve, and people can offer up methods for those specific PDEs. The domain can be a required type with standard domains set in the standard library, and other packages (like FEM packages) can add domains as necessary. This method is simple to implement, everyone can easily join in and use their favorite methods/tools to solve the problem, etc. The issue here is granularity: what PDEs do you supply?

The second option is to use a DSL like ModelingToolkit.jl to abstractly specify the PDEs. It can define D = Differential(x), and then users can build, in the parlance of ModelingToolkit.jl, a PDE system with equation D^2 * u + D * u = f and boundary conditions a*D*u + b*u = c and give a domain type. Then someone could write dispatches to solve for AbstractPDEProblem which check if they can handle the domain and boundary conditions and automatically discretizes and solves. This is infinitely harder to do arbitrarily, but is the PDE solver that Julia deserves.

So I think the intermediate solution is to do both. Setup the AbstractPDEProblem interface and and add some concrete problems. JuliaDiffEq can start by offering some FEM and (pseudo)spectral methods to solve it. Then we can build GenericPDEProblem which uses the ModelingToolkit.jl stuff to abstractly specify that, and we can have DiffEqOperators.jl be the first package to handle that mostly automatically if the domain is square and if the BCs are linear. That will be quite a project but can be a next year GSoC goal.

From there a nice ecosystem could evolve. More AbstractPDEProblems for specific well-known PDEs can be proposed and people can offer solution methods, and then as we get more advanced we can build out the more fully automated generic pipelines. This then doesn't restrict anything about how the PDE is actually solved, or what PDEs can be solved, just how the PDE is specified and what the output looks like (i.e. make sure the solutions all subscribe to some solution interface so it's easy to swap around).

I think we will want to separate out the PDE docs though since I imagine this will grow quickly. I see it sectioning out like the DiffEq docs: each AbstractPDEProblem has a page describing the PDE to be solved and the problem options, and then there's a page describing all of the solvers available, and anyone can PR to add their solver package to the list with a description. There needs to be some tutorials specifically on specifying, solving, and handling PDE solutions. There needs to be pages describing the abstract domain and the concrete types. And there needs to be pages describing whatever extra options. That seems to need its own documentation since otherwise half of the DiffEqDocs would be PDEs all of the sudden (of course it would like over!)

@dlfivefifty
Copy link
Contributor

Integral constraints like $\int_a^b u(x) dx = c$ are linear so should be fine. Non-linear constraints Are harder without a full DAE solver.

Are there actually cases where people have non-linear constraints that are reducible to explicit solvers in an adhoc way?

@ChrisRackauckas
Copy link
Member Author

I don't think so, but why would be looking at an explicit solver for a problem with nonlinear constraints? Seems like it would be natural to use a discretization to a DAE.

@ranocha
Copy link
Member

ranocha commented Aug 7, 2018

For nonlinear conservation laws, useful boundary conditions are often nonlinear. Nevertheless, they can be solved using spatial semidiscretisations such as finite volume or finite difference methods and explicit Runge-Kutta schemes.

@jlperla
Copy link

jlperla commented Aug 7, 2018

In economics there are lots of nonlinear problems (some of which you saw in SciML/PDERoadmap#22 post) but... I am not sure that this is possible to solve in general, and it is usually the interior themselves that are nonlinear rather than the boundaries.

Many of the approaches to those use an iterative apporach where you define a linear differential equation, solve it, change the parameters, repeat. Of course, you could also define these as generating nonlinear systems of equations instead... but I think we should wait on that sort of generality. When Capstan.jl is mature and reverse-mode works well, the "bigass nonlinear system" approach might work better. So, my feeling is that this library should stay focused on affine and linear operators and boundaries, and that the fully nonlinear stuff should wait until this works.

As an aside note, one of the first nonlinear problems I need to solve are differential quasi-variational inequalities, that is, writing down the stationary problem to solve for u(x) for given functions b(x) and S(x) and a differential operator A:

max( r u(x) - b(x) - A u(x), u(x) - S(x)) = 0

This isn't a linear problem, but it is a linear-complementarity problem when discretized. That is
max(C u - b, u - S) = 0 for C = r I - A discretization, and everything else vectors on the spatial discretization.

@ChrisRackauckas
Copy link
Member Author

This issue is on DifferentialEquations.jl, not DiffEqOperators.jl. There was never a plan to limit it to affine boundary conditions.

@jlperla
Copy link

jlperla commented Aug 8, 2018

My mistake, I was confused. I don't have much to add on a general interface, as problems in economics tend to be of a very particular set of sub-types. What I will say, though, from my limited knowledge is that I think it may be too ambitious to try to come up with design for arbitrary PDEs at this point, since the methods for solving them are so radically different. Having a shared set of types/interfaces/etc. for common things (e.g. domains, grids, spaces, etc.) is a different question. If ApproxFun, DiffEqOperators, etc. share those things, then it is a step forward.

@ChrisRackauckas
Copy link
Member Author

I think it may be too ambitious to try to come up with design for arbitrary PDEs at this point, since the methods for solving them are so radically different.

Solving and specification are two completely different things. We don't have to commit to solve every representable PDE, just have a language to specify what problem to solve. What I already showed in the slides can do that if you allow the AbstractDomain to be something that can be package defined as well, so it's not very ambitious.

@ChrisRackauckas
Copy link
Member Author

Supersceded by being partially done, and the higher level part is #469

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants