One function that will give you the solution.
The purpose of this package is to solve one-dimensional partial differential equations of parabolic type (heat/diffusion type equations). The package can handle one equation or a system, linear and non-linear cases.
In the most general way, the problem is formulated like this. For the vector of unknown fields
where the diffusion coefficient, "outer" diffusion coefficient, and source function are all functions of the field, spatial, and temporal coordinates:
The equation must be supplemented with initial and boundary conditoins. The initial condition:
and for boundary conditions there are three options.
- Periodic (only works for all fields at once, i.e. you
cannot set periodic BCs for
$u_1$ and Dirichlet BCs for$u_2$ ):
- Dirichlet:
- Neumann:
It is possible to apply Dirichlet and Neumann conditions in a mixed way, e.g. different BCs for
different fields
You feed the source functions and diffusion coefficients, as well as initial/boundary conditions, and get the
solution on a grid of
The solver is written using numpy and scipy.
Normal installation:
git clone https://github.com/Alvkuzin/diff_pde
cd diff_pde
pip install .For the installation and contributing, replace the last line with
pip install -e .The main idea is very simple: to solve for
- an m-element list of source functions
$f(u, t, x)$ , - an m-element list of coefficients D, each is either a scalar or a function
$D(u, t, x)$ , - optinally, an m-element list of coefficients
$D_\mathrm{out}$ , each is either a scalar or a function$D_\mathrm{out}(u, t, x)$ , while by default they are set to unity, - an m-element list of initial conditions: functions
$u_0(x)$ , - an m-element list of 2-element lists of boundary conditions:
$[g_\mathrm{left}(t), g_\mathrm{right}(t)]$ , - a spatial grid
$x_\mathrm{grid}$ as a numpy array; it defines nods at which the fields will be defined as grid functions, - a time grid at which the fields are to be returned.
As a return, you get the m-element list of fields, each is a numpy array (x.size, t.size).
Let's assume there's a homogenious metallic rod of length
Here the coefficient
from diff_pde import solve_diff_pde
import numpy as np
D = 3
L = 3.0
T_init = 1
T_right = 0.5
def T0(x):
x = np.asarray(x)
return np.ones(x.size) * T_init
xi = 0.0
xf = L
Nx=501
x_grid = np.linspace(xi, xf, Nx)
ti = 0
tf = 5
Nt = 303
t_ev = np.linspace(ti, tf, Nt)
x_ , [T,] = solve_diff_pde(rhs_list = [lambda u, t, x: x*0],
D_list = [D], # since D is a scalar here, it can be passed as `D_out_list=[D]` instead.
x_grid=x_grid,
t_span=[ti, tf],
t_eval=t_ev,
u0_list=[T0],
bc_type=[[
"neumann", # on the left
"dirichlet" # on the right
]
],
bound_conds = [[
lambda t: 0*t, # no-flux on the left
lambda t: 0*t + T_right # constant T_right on the right
]
],
)In the end, you have an array T of shape: (x_.size, t_ev.size).
You can run the code simple_heat_anim.py from terminal to see this solution animated.
The Brusselator equations describe an autocatalyctic reaction
of two chemicals, which concentrations we shall denote
For boundary conditions, it makes sense to adopt either
periodic BCs (as if a small domain of a bigger picture is being simulated)
or Neumann ones (absense of flux through the boundaries). For illustration purposes, let's
take diffusion coeffients
from diff_pde import solve_diff_pde
import numpy as np
A = 2
B = 4
Dx0 = 1.6e-3
Dy0 = 8e-3
xi = 0.0
xf = 3.0
sgm = 0.1
Nx=501
ti = 0
tf = 150
Nt = int(3 * (tf - ti))
t_ev = np.linspace(ti, tf, Nt)
def fx(u, t, x):
X_, Y_ = u
return A - (B+1) * X_ + X_**2 * Y_
def fy(u, t, x):
X_, Y_ = u
return B * X_ - X_**2 * Y_
def x0(x):
return A + 0.1 * np.cos(2 * np.pi * (x - xi) / (xf - xi) )**2
def y0(x):
return B/A + 0.1 * np.sin(2 * np.pi * (x - xi) / (xf - xi) )**2
def Dx(u, t, x):
X_, Y_ = u
return (1+x)/(2+x) * Dx0 * (1 + 1/X_) / (1 + 1/Y_)
def Dy(u, t, x):
X_, Y_ = u
return (1.1+x)/(2.1+x) * Dy0 * (1.1 + 0.99/X_) / (1.01 + 0.98/Y_)
x_grid = np.linspace(xi, xf, Nx)
x_ , (X_, Y_) = solve_diff_pde(rhs_list = [fx, fy],
D_list = [Dx, Dy],
x_grid=x_grid,
t_span=[ti, tf],
t_eval=t_ev,
u0_list=[x0, y0],
rtol=1e-7,
bc_type="neumann"
)You can run the code brusselator_anim.py from terminal to see this solution animated.
Let us reformulate slightly the problem from the first example. Imagine two rods,
with lengths
To find the
distribution of the temperature on this resulting rod, we would need to evolve
from diff_pde import solve_diff_pde
import numpy as np
D1 = 8
D2 = 0.5
L1 = 2.0
L2 = 1.0
T_init_1 = 1
T_init_2 = 2
T_right = 1.5
def T_init(x):
x = np.asarray(x)
return (T_init_1 * np.heaviside(-(x-L1), 0.5) +
T_init_2 * np.heaviside((x-L1), 0.5)
)
def D(u, t, x):
x = np.asarray(x)
return (D1 * np.heaviside(-(x-L1), 0.5) +
D2 * np.heaviside((x-L1), 0.5)
)
xi = 0.0
xf = L1 + L2
Nx=501
x_grid = np.linspace(xi, xf, Nx)
ti = 0
tf = 5
Nt = 303
t_ev = np.linspace(ti, tf, Nt)
x_ , [T_,] = solve_diff_pde(rhs_list = [lambda u, t, x: x*0],
D_list = [D],
x_grid=x_grid,
t_span=[ti, tf],
t_eval=t_ev,
u0_list=[T_init],
bc_type=[[
"neumann", # on the left
"dirichlet" # on the right
]
],
bound_conds = [[
lambda t: 0*t, # no-flux on the left
lambda t: 0*t + T_right # constant T_right on the right
]
],
)You can run the code discontin_heat_anim.py from terminal to see this solution animated.
This code is written using the method of lines. It means that the desired solution
where
and the partial differential equation becomes the system of ODEs:
This system is solved using scipy.integrate.solve_ivp, which allows us to avoid thinking how
is this system actually being solved. It is important to use method='BDF' (default) or method='Radau',
or any other implicit methods which for the equation
For a system of equations for fields [$u_1,...,u_m$], the right-hand side is calculated as described above for each field and then concatenated into one vector, and the m fields are also packed into a one big vector [$u_0...u_N,u_{N+1},...,u_{mN}$] for which the ODE is solved with the concatenated right-hand.
Internally, the solution is looked for at the spatial grid
Outsourcing the ODEs to solve_ivp allows not to think about time steps, as they are made automaticly by the solver,
which returns a solution at any desired
I attempted to write a code with the second-order spatial approximations. I tested it on some 'good' functions, and the metric
indeed behaves as
rtol/atol that may be provided to the solve_diff_pde refer to the arguments passed to the solve_ivp, e.g.
they do not guarantee these relative/absolute tolerance for a solution.