Boundary conditions (?) in MethodOfLines.jl PDE

Hi everyone,

I’m trying out the new capability to deal with integrals in MethodOfLines.jl (in the new main, v0.7.6). I’m trying to simply expand an ODE example into a PDE. For those of you interested, the ODE is a simple SIR compartmental model, and the PDE is one where individuals are structured by age. It’s very close to working, except the sum of the states over time should remain constant, yet they decline over time (see the last line). I thought that setting the derivative wrt a to 0 at the upper boundary would prevent mass from leaving the system. Can anyone spot where I’m going wrong?

using ModelingToolkit
using MethodOfLines
using DomainSets
using OrdinaryDiffEq
using Plots

β = 0.05
c = 10.0
γ = 0.25
S₀ = 990.0
I₀ = 10.0
dt = 0.1
tmin = 0.0
tmax = 40.0
da = 26.0
amin = 0.0
amax = 52.0*75.0

@parameters t a
@variables S(..) I(..) R(..)
Dt = Differential(t)
Da = Differential(a)
Ia = Integral(a in DomainSets.ClosedInterval(amin,amax))
N(t) = Ia(S(a,t)) + Ia(I(a,t)) + Ia(R(a,t))

eqs = [Dt(S(a,t)) + Da(I(a,t)) ~ - β * c * S(a,t) * Ia(I(a,t))/N(t),
       Dt(I(a,t)) + Da(I(a,t)) ~ β * c * S(a,t) * Ia(I(a,t))/N(t) - γ*I(a,t),
       Dt(R(a,t)) + Da(R(a,t)) ~ γ*I(a,t)]

bcs = [
        S(a,0.0) ~ S₀*da/(amax-amin),
        S(0.0,t) ~ 0.0,
        I(a,0.0) ~ I₀*da/(amax-amin),
        I(0.0,t) ~ 0.0,
        R(a,0.0) ~ 0.0,
        R(0.0,t) ~ 0.0,
        Da(S(amax,t)) ~ 0.0,
        Da(I(amax,t)) ~ 0.0,
        Da(R(amax,t)) ~ 0.0
        ]

domains = [t ∈ (tmin,tmax), a ∈ (amin,amax)]

@named pde_system = PDESystem(eqs, bcs, domains, [a, t], [S(a, t), I(a, t), R(a, t)])
discretization = MOLFiniteDifference([a=>da],t)

prob = MethodOfLines.discretize(pde_system, discretization)
sol = solve(prob, Tsit5(), saveat = dt)

Smat = sol.u[S(a, t)]
Imat = sol.u[I(a, t)]
Rmat = sol.u[R(a, t)]

sum(Smat .+ Imat .+ Rmat, dims=1)
1 Like

I haven’t checked any of these solutions but you could try:

  1. Lowering the tolerance of the ODE solver (abstol and reltol)
  2. Changing the solver to one better suited for conservation laws or PDEs ( SSPRK22, ROCK4)
  3. Using a Manifold Projection Callback

Thanks! Switching the solver didn’t help. Changing the parameter values does affect the conservation of mass, but I worry that I haven’t specified the model correctly, so using a ManifoldProjection may just be a sticking plaster.

Is there a typo in your equations? The advection term for S(a,t) is Da(I(a,t)), but should it not be Da(S(a,t)) ? I.e. people in all three categories S, I, R just age independently, all at the same rate.

Also, I would argue that the equations plus boundary/initial conditions are simply not mass-conserving. If you consider the temporal change of the mass N(a,t) = S(a,t) + I(a,t) + R(a,t) in the system, i.e. Dt(N(a,t)) = -Da(N(a,t)) and integrate it over the whole domain (to get the change of total mass), you find that the result at any time is given by N(amin,t) - N(amax,t). Since from the very beginning there is some mass at the right boundary, and the advection term will transport the mass there anyway, the loss of mass should never be zero.

From this calculation, to get zero loss of mass, the right boundary conditions would be that N(amax,t) ~ 0 although that does seem a bit counter intuitive to me as well – a steady influx and outflux with some “birth rate”/“death rate” would probably make more sense physically. Or localizing the initial condition well below amax and stopping the simulation when the first people reach that age … then either choice of b.c. doesn’t really matter and mass conservation should hold until that time is reached.

Otherwise the equations seem reasonable as far as I can tell…

This may be numerical dispersion from the upwind scheme, used for the advection terms - an unfortunate numerical artefact. You might try advection_scheme=WENOScheme() which is better, though is often unstable when used without a periodic condition, or at least 2 bcs on each boundary - we need better boundary handling for this.

Hi @Sevi

Thanks - you were right about the typo - the equations should be

eqs = [Dt(S(a,t)) + Da(S(a,t)) ~ - β * c * S(a,t) * Ia(I(a,t))/N(t),
       Dt(I(a,t)) + Da(I(a,t)) ~ β * c * S(a,t) * Ia(I(a,t))/N(t) - γ*I(a,t),
       Dt(R(a,t)) + Da(R(a,t)) ~ γ*I(a,t)]

as you say. However, this doesn’t fix the problem!

You’re also right about the loss of mass from the right boundary - however, I thought that I had dealt with this by setting the boundary conditions as follows:

bcs = [
        S(a,0.0) ~ S₀*da/(amax-amin),
        S(0.0,t) ~ 0.0,
        I(a,0.0) ~ I₀*da/(amax-amin),
        I(0.0,t) ~ 0.0,
        R(a,0.0) ~ 0.0,
        R(0.0,t) ~ 0.0,
        Da(S(amax,t)) ~ 0.0,
        Da(I(amax,t)) ~ 0.0,
        Da(R(amax,t)) ~ 0.0
        ]

The last 3 BCs are intended to prevent further flow out of the state, so over time, the mass at N(amax,t) should increase. The above also results in da(N(amax,t)) ~ 0 (or at least, that’s the intention).

Thanks for the tip about changing the advection scheme - still no luck however. Current code below:

using ModelingToolkit
using MethodOfLines
using DomainSets
using OrdinaryDiffEq
using Plots

β = 0.05
c = 10.0
γ = 0.25
S₀ = 990.0
I₀ = 10.0
dt = 0.1
tmin = 0.0
tmax = 40.0
da = 52.0
amin = 0.0
amax = 52.0*75.0

@parameters t a
@variables S(..) I(..) R(..)
Dt = Differential(t)
Da = Differential(a)
Ia = Integral(a in DomainSets.ClosedInterval(amin,amax))
N(t) = Ia(S(a,t)) + Ia(I(a,t)) + Ia(R(a,t))

eqs = [Dt(S(a,t)) + Da(S(a,t)) ~ - β * c * S(a,t) * Ia(I(a,t))/N(t),
       Dt(I(a,t)) + Da(I(a,t)) ~ β * c * S(a,t) * Ia(I(a,t))/N(t) - γ*I(a,t),
       Dt(R(a,t)) + Da(R(a,t)) ~ γ*I(a,t)]

bcs = [
        S(a,0.0) ~ S₀*da/(amax-amin),
        S(0.0,t) ~ 0.0,
        I(a,0.0) ~ I₀*da/(amax-amin),
        I(0.0,t) ~ 0.0,
        R(a,0.0) ~ 0.0,
        R(0.0,t) ~ 0.0,
        Da(S(amax,t)) ~ 0.0,
        Da(I(amax,t)) ~ 0.0,
        Da(R(amax,t)) ~ 0.0
        ]

domains = [t ∈ (tmin,tmax), a ∈ (amin,amax)]

@named pde_system = PDESystem(eqs, bcs, domains, [a, t], [S(a, t), I(a, t), R(a, t)])
discretization = MOLFiniteDifference([a=>da],t)

prob = MethodOfLines.discretize(pde_system, discretization, advection_scheme=WENOScheme())
sol = solve(prob, Tsit5(), saveat = dt)

Smat = sol.u[S(a, t)]
Imat = sol.u[I(a, t)]
Rmat = sol.u[R(a, t)]

plot(sol.t, sum(Smat,dims=1)',label="S",xlabel="Time",ylabel="Number")
plot!(sol.t, sum(Imat,dims=1)',label="I")
plot!(sol.t, sum(Rmat,dims=1)',label="R")

sum(Smat .+ Imat .+ Rmat, dims=1)

I see. But my point was that Da(N) ~ 0 is not the right boundary condition to begin with. Consider the change in N(t), so

\frac{\mathrm{d}}{\mathrm{d}t} N(t) = \int_{a_{\text{min}}}^{a_{\text{max}}} \mathrm{d}a \; \partial_t N(a,t) = - \int_{a_{\text{min}}}^{a_{\text{max}}} \mathrm{d}a \; \partial_a N(a,t) = N(a_{\text{min}},t) - N(a_{\text{max}},t)

(We just used the definition of N(t) and the equations above, where all the reaction terms cancel on the right-hand side).
If we want this to be zero, then not \partial_a N(a,t) has to vanish on the boundaries, but N(a,t) itself.

However, you will still see that mass will leave the system with these (“absorbing”) boundary conditions, N(a_{\text{max}},t) = 0, but that’s not a problem. The missing mass is just in the point a_{\text{max}}. We could also specify N(a_{\text{max}},t) = N_{0} - \int_{a_{\text{min}}}^{a_{\text{max}}} \mathrm{d}a \; N(a,t) to get the correct value, but that just makes the numerical solution more tricky I believe.

Not sure this is the only way of interpreting your equations, but it would make sense to me intuitively. This also comes up when studying first-passage-time problems, where the equations are somewhat similar. The picture there is that once an individual has reached a_{\text{max}} it cannot come back, so it should be “absorbed” at that boundary.

Also physically, this is what should happen. Your population doesn’t suddenly stop ageing at a certain point – instead they will age past the range that you can describe with the given model. But we can still just (manually) summarize all of these individuals and define their mass to be whatever is missing from the simulation.

I agree with what you’re saying, but my issue is how to implement these absorbing boundary conditions in the right way within MethodOfLines.jl. The textbook definitions of this model (see here) don’t mention the upper boundary condition (as they rarely give details of the numerical simulation of the model).

I tried a simpler example based on the linked paper (alongside the equivalent ODE) that treats everything as proportions, and so skips computation of , and I’m running into other problems now. I’m sure it’s just a silly mistake, but I can’t see it…

using OrdinaryDiffEq
using ModelingToolkit
using MethodOfLines
using DomainSets
using Plots

β = 0.5
γ = 0.25
S₀ = 0.99
I₀ = 0.01
dt = 0.1
tmin = 0.0
tmax = 40.0
da = 52.0
amin = 0.0
amax = 52.0*75.0

@parameters t a
@variables S(..) I(..)
Dt = Differential(t)
Da = Differential(a)
Ia = Integral(a in DomainSets.ClosedInterval(amin,amax))

eqs = [Dt(S(a,t)) + Da(S(a,t)) ~ - β * S(a,t) * Ia(I(a,t)),
       Dt(I(a,t)) + Da(I(a,t)) ~ β * S(a,t) * Ia(I(a,t)) - γ*I(a,t)]

bcs = [
        S(a,0.0) ~ S₀*da/(amax-amin),
        S(0.0,t) ~ 0.0,
        I(a,0.0) ~ I₀*da/(amax-amin),
        I(0.0,t) ~ 0.0,
        Da(S(amax,t)) ~ 0.0,
        Da(I(amax,t)) ~ 0.0
        ]

domains = [t ∈ (tmin,tmax), a ∈ (amin,amax)]

@named pde_system = PDESystem(eqs, bcs, domains, [a, t], [S(a, t), I(a, t)])
discretization = MOLFiniteDifference([a=>da],t)

prob_pde = MethodOfLines.discretize(pde_system, discretization, advection_scheme=WENOScheme())
sol_pde = solve(prob, Tsit5(), saveat = dt)

Smat = sol_pde.u[S(a, t)]
Imat = sol_pde.u[I(a, t)]

plot(sol_pde.t, sum(Smat,dims=1)',label="S",xlabel="Time",ylabel="Number")
plot!(sol_pde.t, sum(Imat,dims=1)',label="I")

function sir_ode(u,p,t)
    (S, I) = u
    (β, γ) = p
    dS = -β*S*I
    dI = β*S*I - γ*I
    [dS, dI]
end

prob_ode = ODEProblem(sir_ode, [S₀,I₀], (tmin,tmax), [β,γ])
sol_ode = solve(prob_ode, Tsit5(), saveat=dt)

Are you getting errors or what are the problems you’re getting?

By the way, try updating - whole domain integrals now correctly reduce dimensionality and I fixed the problems you found with using them in bcs

In your new example, the wrong problem is solved. Instead of

prob_pde = MethodOfLines.discretize(pde_system, discretization, advection_scheme=WENOScheme())
sol_pde = solve(prob, Tsit5(), saveat = dt)

it should read

prob_pde = MethodOfLines.discretize(pde_system, discretization, advection_scheme=WENOScheme())
sol_pde = solve(prob_pde, Tsit5(), saveat = dt)
1 Like

Also I’m not sure the advection scheme works as a kwarg for discretize, it has to be in your MOLFiniteDifference

Here’s one with the typo fixed.

# https://courses-archive.maths.ox.ac.uk/node/view_material/52367
using DifferentialEquations
using OrdinaryDiffEq
using ModelingToolkit
using MethodOfLines
using DomainSets
using Plots

β = 0.5
γ = 0.25
S₀ = 0.99
I₀ = 0.01
dt = 0.1
tmin = 0.0
tmax = 40.0
da = 52.0
amin = 0.0
amax = 52.0*75.0

@parameters t a
@variables S(..) I(..)
Dt = Differential(t)
Da = Differential(a)
Ia = Integral(a in DomainSets.ClosedInterval(amin,amax))

eqs = [Dt(S(a,t)) + Da(S(a,t)) ~ - β * S(a,t) * Ia(I(a,t)),
       Dt(I(a,t)) + Da(I(a,t)) ~ β * S(a,t) * Ia(I(a,t)) - γ*I(a,t)]

bcs = [
        S(a,0.0) ~ S₀*da/(amax-amin),
        S(0.0,t) ~ 0.0,
        I(a,0.0) ~ I₀*da/(amax-amin),
        I(0.0,t) ~ 0.0,
        Da(S(amax,t)) ~ 0.0,
        Da(I(amax,t)) ~ 0.0
        ]

domains = [t ∈ (tmin,tmax), a ∈ (amin,amax)]

@named pde_system = PDESystem(eqs, bcs, domains, [a, t], [S(a, t), I(a, t)])
discretization = MOLFiniteDifference([a=>da],t)

prob_pde = MethodOfLines.discretize(pde_system, discretization);
sol_pde = solve(prob_pde, saveat = dt)

Smat = sol_pde.u[S(a, t)]
Imat = sol_pde.u[I(a, t)]

plot(sol_pde.t, sum(Smat,dims=1)',label="S",xlabel="Time",ylabel="Number")
plot!(sol_pde.t, sum(Imat,dims=1)',label="I")

function sir_ode(u,p,t)
    (S, I) = u
    (β, γ) = p
    dS = -β*S*I
    dI = β*S*I - γ*I
    [dS, dI]
end

prob_ode = ODEProblem(sir_ode, [S₀,I₀], (tmin,tmax), [β,γ])
sol_ode = solve(prob_ode, Tsit5(), saveat=dt)

The PDE gives this trajectory:

pde

The trajectory should look closer to this:

ode

My original 3-state model (from which the above was derived) is close to what it should be:

using ModelingToolkit
using MethodOfLines
using DomainSets
using OrdinaryDiffEq
using Plots

β = 0.05
c = 10.0
γ = 0.25
S₀ = 990.0
I₀ = 10.0
dt = 0.1
tmin = 0.0
tmax = 40.0
da = 52.0
amin = 0.0
amax = 52.0*75.0

@parameters t a
@variables S(..) I(..) R(..)
Dt = Differential(t)
Da = Differential(a)
Ia = Integral(a in DomainSets.ClosedInterval(amin,amax))
N(t) = Ia(S(a,t)) + Ia(I(a,t)) + Ia(R(a,t))

eqs = [Dt(S(a,t)) + Da(S(a,t)) ~ - β * c * S(a,t) * Ia(I(a,t))/N(t),
       Dt(I(a,t)) + Da(I(a,t)) ~ β * c * S(a,t) * Ia(I(a,t))/N(t) - γ*I(a,t),
       Dt(R(a,t)) + Da(R(a,t)) ~ γ*I(a,t)]

bcs = [
        S(a,0.0) ~ S₀*da/(amax-amin),
        S(0.0,t) ~ 0.0,
        I(a,0.0) ~ I₀*da/(amax-amin),
        I(0.0,t) ~ 0.0,
        R(a,0.0) ~ 0.0,
        R(0.0,t) ~ 0.0,
        Da(S(amax,t)) ~ 0,
        Da(I(amax,t)) ~ 0,
        Da(R(amax,t)) ~ 0
        ]

domains = [t ∈ (tmin,tmax), a ∈ (amin,amax)]

@named pde_system = PDESystem(eqs, bcs, domains, [a, t], [S(a, t), I(a, t), R(a, t)])
discretization = MOLFiniteDifference([a=>da],t)

prob = MethodOfLines.discretize(pde_system, discretization);
sol = solve(prob, Tsit5(), saveat = dt);

Smat = sol.u[S(a, t)]
Imat = sol.u[I(a, t)]
Rmat = sol.u[R(a, t)]

plot(sol.t, sum(Smat,dims=1)',label="S",xlabel="Time",ylabel="Number")
plot!(sol.t, sum(Imat,dims=1)',label="I")
plot!(sol.t, sum(Rmat,dims=1)',label="R")

sum(Smat .+ Imat .+ Rmat, dims=1)

pde3

The difference is probably because my system is leaking mass from the upper boundary at amax, and I have to figure out how to model that in ModelingToolkit.

Thanks for the reference, that was quite helpful in understanding the problem better. Here are some thoughts

  • the reference doesn’t include upper boundary conditions, because they are not needed for the analytical solution – because the boundary is at infinity, we can get away with specifying no boundary condition
  • of course we can’t solve an infinite system like this on the computer, but if it should resemble the results from the reference, then the upper boundary should be very large compared to the maximum age at which individuals die – this way, there is no individual reaching the boundary and the boundary condition doesn’t matter. In the current setup, the initial condition spans the whole space, hence there are always weird effects from the boundaries

It just seems like an issue of the parameters. You might try decreasing the infection rate and death rate.

Is this from the ODE or PDE ? I’m guessing ODE, because of the initial conditions you chose.

I made some edits to your code (mainly changing the initial conditions – see below).

With that, we get from the full PDE solution basically the same behavior that you showed here:

Until the time when the first individuals hit the upper boundary, the whole system evolves like the ODE would predict, then we “leak” some mass. Of course you can choose any values, the only important thing is that the system boundary is well above the region where something actually happens. In the model from your reference, that is the case as the boundary is at infinity. This is the integrated mass:

And the individual masses (“space” / age vs current time). The initial condition is just a rectangular function, which gets advected in time and performs the SIR model individually. Everything behaves as expected until the wave hits the upper boundary. There is just some “smearing out” of the wave wich is probably the numerical dispersion that @xtalax mentioned:

And here the code to reproduce it

using OrdinaryDiffEq
using ModelingToolkit
using MethodOfLines
using DomainSets
using Plots

β = 0.05
γ = 0.005
S₀ = 1.0
I₀ = 0.001
dt = 0.1
tmin = 0.0
tmax = 4000.0
da = 52.0
amin = 0.0
amax = 52.0*75.0

min_age = 52.0 * 5.0
max_age = 52.0 * 40.0

theta(a) = (a > 0) ? 1 : 0
@register_symbolic theta(a)

@parameters t a
@variables S(..) I(..) R(..)
Dt = Differential(t)
Da = Differential(a)
Ia = Integral(a in DomainSets.ClosedInterval(amin,amax))

eqs = [
    Dt(S(a,t)) + Da(S(a,t)) ~ - β * S(a,t) * Ia(I(a,t)),
    Dt(I(a,t)) + Da(I(a,t)) ~   β * S(a,t) * Ia(I(a,t)) - γ*I(a,t),
    Dt(R(a,t)) + Da(R(a,t)) ~ γ * I(a,t),
    ]

bcs = [
        S(a,0.0) ~ S₀/(max_age - min_age) * theta(max_age - a) * theta(a - min_age),
        I(a,0.0) ~ I₀/(max_age - min_age) * theta(max_age - a) * theta(a - min_age),
        R(a,0.0) ~ 0.0,
        S(0.0,t) ~ 0.0,
        I(0.0,t) ~ 0.0,
        R(0.0,t) ~ 0.0,
        Da(S(amax,t)) ~ 0.0,
        Da(I(amax,t)) ~ 0.0,
        Da(R(amax,t)) ~ 0.0,
        ]

domains = [t ∈ (tmin,tmax), a ∈ (amin,amax)]

@named pde_system = PDESystem(eqs, bcs, domains, [a, t], [S(a, t), I(a, t), R(a,t)])
discretization = MOLFiniteDifference([a=>da],t)

prob_pde = discretize(pde_system, discretization)
sol_pde = solve(prob_pde, Tsit5(), saveat = dt)

integrate(component) = da .* sum(sol_pde[component], dims=1) |> transpose

plot(sol_pde[t], integrate(S(a,t)), label="S", xlabel="Time", ylabel="Number")
plot!(sol_pde[t], integrate(I(a,t)), label="I")
plot!(sol_pde[t], integrate(R(a,t)), label="R")
plot!(sol_pde[t], integrate(S(a,t)) .+ integrate(I(a,t)) .+ integrate(R(a,t)), label="I + S + R")

plot(
    heatmap(sol_pde[t], sol_pde[a], sol_pde[S(a,t)], title="S(a,t)"),
    heatmap(sol_pde[t], sol_pde[a], sol_pde[I(a,t)], title="I(a,t)"),
    heatmap(sol_pde[t], sol_pde[a], sol_pde[R(a,t)], title="R(a,t)"),
    xlabel="time passed (weeks)",
    ylabel="age (weeks)",
    layout=(3, 1),
    size=(900,1000),
)
2 Likes

All in all, the numerical solution makes sense to me (I didn’t try the WENO scheme yet, maybe it reduces the numerical dispersion?), but I’m not sure what you want to simulate in terms of physics/biology in the end.

I hope the comments above are helpful anyway, but feel free to tell me if not :slight_smile:

Dear @sevimora

Thanks! This looks great - I thought all that was needed was to (in effect) increase the boundary conditions to prevent leaking of mass over the (tmin,tmax) period, but your code on how to implement that in the BCs is super useful.

Now, back to trying to understand why the reduced system doesn’t work…