Radiation Transport with Julia
The radiation transport equation
In this workshop, we wish to use a programming example from radiation transport. Do not worry, if you do not understand all details. Essentially everything boils down to solving a simple matrix ordinary differential equation. And if that sounds complicated, we can also say it boils down to matrix vector multiplications that your code should execute in the end.
So let us give a little bit of background: The radiation transport equation describes radiation particles moving through a background material. In a one-dimensional geometry, particles can travel into directions and are located at a spatial position . One application which requires the simulation of radiation particles is radiation therapy, where photons are shot at a tumor to destroy cancerous cells. The radiation received by the patient is stored in a matrix , where is the number of spatial cells and is the number of velocities. Below you find an example of a treatment planning result from radiation oncology. The treatment is chosen to destroy the lung tumor without harming important organs.
Adding packages
Let us first generate the velocity grid, which are commonly chosen to be the so-called Gauss-Legendre points. Luckily, these points are already implemented in Julia. All we need to do is to install the Julia Package FastGaussQuadrature
. The points can be accessed and stored in a vector by calling v, w = gausslegendre(nv)
. Note that contains the Gauss-Legendre points and contains the so-called quadrature weights used to compute integrals. Recall that it can be advantageous to create a new environment for this project.
FastGaussQuadrature
package:
shell> mkdir LinearTransportSolver
shell> cd LinearTransportSolver
/home/jonas/Projects/LinearTransportSolver
(SummerSchool) pkg> activate .
Activating new project at `~/Projects/LinearTransportSolver`
(LinearTransportSolver) pkg> add FastGaussQuadrature
Now you can use all functionalities of the FastGaussQuadrature
package whenever you start Julia. For this, generate a script main.jl
and write
using FastGaussQuadrature
nv = 10
v, w = gausslegendre(nv)
You can print out your velocity grid by typing v
in the Julia environment or by adding println(v)
.
Array generation and access
Now, to generate the spatial positions on which we want to evaluate the solution we can use the range
command. Build a spatial grid , with , where contains the spatial positions from to .
nx = 101
x = collect(range(0, 1; length=nx))
The range
command will generate values from to . We use the collect
command to convert the returned object into a vector.
The next step is to set up our solution matrix . The matrix should have dimension . Initially, the solution is zero except for for all . Compute the described matrix.
zeros(n, m)
command. Hence, to create a matrix with dimension type
ψ = zeros(nx, nv)
Now to access this matrix at spatial index and velocity index , we can type ψ[j, i]
. Moreover, if we do not want to access a single index, but let's say indices to in the spatial domain at all indices in velocity, we can write ψ[5:10, :]
. Note that Julia uses indices starting at index , not . Now, put particles in the center of the domain having all possible velocities. That is,
ψ[50, :] = ones(nv)
Linear algebra operations
A next step would be to evolve these particles in time. This is done by solving an ordinary differential equation of the form
Here are tridiagonal matrices of the form
and
where . Set up these two matrices in your code.
LinearAlgebra
package:
(LinearTransportSolver) pkg> add LinearAlgebra
Then, the tridiagonal matrices can be implemented as
using LinearAlgebra
Δx = 1 / (nx - 1)
DPlus = (1 / Δx) * Tridiagonal(-ones(nx - 1), ones(nx), zeros(nx - 1))
DMinus = (1 / Δx) * Tridiagonal(zeros(nx - 1), -ones(nx), ones(nx - 1))
The matrices are diagonal matrices, where collects all negative velocities on the diagonal, i.e., and collects all positive velocities, i.e., . Implement these two matrices. To write your code for general , use the ceil
and floor
commands.
Diagonal(y)
. You can append two vectors and by [a; b]
. We can use the ceil
and floor
commands to determine the midpoint of your velocity vector by e.g. Int(ceil(nv))
. This determines and then transforms the resuling floating point number to an integer.
midMinus = Int(ceil(nv / 2))
midPlus = Int(floor(nv / 2))
VMinus = Diagonal([v[1:midMinus]; zeros(midPlus)])
VPlus = Diagonal([zeros(midMinus); v[(midPlus + 1):end]])
Recall that in Julia, you do not need to specify data types. However, sometimes it becomes handy or even necessary to do so. Here, we have explicitly stated that midMinus = Int(ceil(nv / 2))
must be an integer. This was needed, since we interpret midMinus
as an index in an array.
Lastly, the so called scattering matrix is given as
Implement this matrix. You can try to not use for
loops to test your element-wise operation skills.
G = ones(nv, nv) .* w - I
Now, to update your solution from to , we approximate the time derivative by . Hence, we then get
Implement the above formular using a time step size . Try to use dot operations whenever possible. You can already check how the solution has changed by inspecting ψ_new
in the center. You already know how this works. If you want you can interpret the physical process that you observe.
Δt = 0.01;
ψ_new = ψ .+ Δt .* (-DPlus * ψ * VPlus .- DMinus * ψ * VMinus .+ ψ * G)
To check the change of after a single time step around the center of the spatial domain you can for example type
julia> midIndex = Int(floor(nx / 2));
julia> ψ_new[(midIndex - 1):(midIndex + 1), :]
Loops
Now, we do not want to know the solution at , but at . If we just choose our approximation of the time derivative is inaccurate. Therefore, we wish to repeat the update with fourty times. Use a loop to implement the repeated updates
nT = 40
ψ_new = zeros(size(ψ));
for n in 1:nT
ψ_new .= ψ .+ Δt .* (-DPlus * ψ * VPlus .- DMinus * ψ * VMinus .+ ψ * G)
ψ .= ψ_new
end
Note that we are using .=
insead of =
to copy values from ψ_new
to ψ
and to write new values on ψ_new
. This ensures that no additional memory is allocated in very iteration step.
Now, we are interested in plotting . On a numerical level, we can compute this via
Φ = zeros(nx)
for j in 1:nx
Φ[j] = sum(ψ[j, :] .* w)
end
Plotting
Luckily, Julia can include the Plots
package. Install and add it to your code. Then run
using LaTeXStrings
using Plots; gr()
plot(x, Φ, labels=L"\Phi")
xlabel!("x")
The output that you should get is
What you see is the particle density over the spatial domain. Congratulations, you have just computed your first radiation transport problem using Julia. If you look at your code, you will see that it looks quite messy. you can add a comment test comment with # test comment
. Use the comment command to explain what you did and double check if you understood everything. Also, commonly the using
commands are executed in the first lines of the code, so move all these commands to the top.
Element-wise operations
To better understand element-wise operations, see how we computed the scattering matrix and see if you can write this in terms of for-loops. You can use the if
command.
Element-wise operations:
We have used
.=
and.*
commands. The dot denotes an element-wise operation. So assume you have two vectors and . Then,y = v .* w
will scalar multiply all elements and save them in a new vector , hence .Assume the vector already exists and you want to tell Julia to store
v .* w
element-wise into this existing vector. Then, you can again use the dot command asy .= v.*w
.These pointwise operations can be extended to multiple situations. Assume for example we have a matrix and a scalar . Then
A .- c
will substract from every element in . Moreover if ,y = v .* A
will scalar multiply to , i.e., .Dot-wise operations can be used for functions as well. For example,
y = exp.(A)
will evaluate the exponential function for every element in , that is, .
Then, the compact element-wise operation formulation of the scattering matrix becomes
G = zeros(nv, nv);
for i in 1:nv
for j in 1:nv
G[i, j] = w[i]
if i == j
G[i, j] = G[i, j] - 1
end
end
end
Full sample solution
using FastGaussQuadrature
using LaTeXStrings
using LinearAlgebra
using Plots; gr()
# define velocity grid according to gauss quadrature
nv = 10
v, w = gausslegendre(nv)
nx = 101
x = collect(range(0, 1; length=nx))
# setup initial condition
ψ = zeros(nx, nv)
ψ[50, :] = ones(nv)
# create stencil matrices
dx = 1 / (nx - 1)
DPlus = (1 / dx) * Tridiagonal(-ones(nx - 1), ones(nx), zeros(nx - 1))
DMinus = (1 / dx) * Tridiagonal(zeros(nx - 1), -ones(nx), ones(nx - 1))
# create system matrices
midMinus = Int(ceil(nv / 2))
midPlus = Int(floor(nv / 2))
VMinus = Diagonal([v[1:midMinus]; zeros(midPlus)])
VPlus = Diagonal([zeros(midMinus); v[(midPlus + 1):end]])
# create scattering matrix
G = ones(nv, nv) .* w - I
# allocate memory
ψ_new = zeros(size(ψ))
# advance in time
Δt = 0.01
nT = 40
for n in 1:nT
ψ_new .= ψ .+ Δt .* (-DPlus * ψ * VPlus .- DMinus * ψ * VMinus + ψ * G)
ψ .= ψ_new
end
# store phi for plotting
Φ = zeros(nx)
for j in 1:nx
Φ[j] = sum(ψ[j, :] .* w)
end
# plot phi
plot(x, Φ, labels=L"\Phi")
xlabel!("x")