Overview

This page is a brief overview of how to use UltraDark. Other examples go into more detail.

UltraDark is built around UltraDark.AbstractGrids objects that contain coordinates and fields, and keep track of the relations between them. There are two types of grids included with UltraDark. UltraDark.Grids are built around regular Julia Core.Arrays. These are the grids used in most of the examples in this documentation. UltraDark.PencilGrids is built around PencilArrays.PencilArray and PencilFFTs. PencilGrids are useful for taking advantage of MPI parallelism when running in a cluster environment.

Warning

Note that there is significant overhead involved in using PencilGrids. It is best to stick to Grids unless you are running jobs accross multiple nodes.

You can define your own subtype of UltraDark.AbstractGrids if you wish to take advantage of other forms of parallelism or change the dynamics of the fields.

Let's start by creating a Grids object.

using UltraDark

const resol = 64
const box_length = 10.0

grids = Grids(box_length, resol);

grids is initially empty, as we can see by checking its mass

UltraDark.update_gravitational_potential!(grids) # ensure the density is up to date
UltraDark.mass(grids)
0.0

Initial conditions are set by modifying the field ψx of grids. Let's use UltraDark.Initialise.add_fdm_soliton! to add a soliton with nonzero velocity to the center of grid.

const mass = 10
const position0 = [0, 0, 0]
const velocity = [1, 0, 0]
const phase = 0
const t0 = 0

UltraDark.Initialise.add_fdm_soliton!(grids, mass, position0, velocity, phase, t0)

Now, as expected, the mass on grids is nonzero

UltraDark.update_gravitational_potential!(grids) # ensure the density is up to date
UltraDark.mass(grids)
9.999133748767056
Note

This does not exactly equal mass because we have used a coarse grid.

Let's also check the location of the soliton.

argmax(grids.ρx)
CartesianIndex(32, 32, 32)

The indices of the maximum are at half resol in each dimension; this is the center of the box.

After creating a AbstractGrids on which a simulation will happen and adding some matter to it, one must specify how the simulation should be carried out. The most important details are when and where to write output, and when the simulation should end.

const output_times = 0:0.5:2
const output_dir = joinpath(mktempdir(), "output")
output_config = OutputConfig(output_dir, output_times; box = true)

See UltraDark.Output.OutputConfig for more details of how to configure the output and UltraDark.Config.SimulationConfig to configure other aspects of the simulation.

Now we are ready to run a simulation by calling simulate!. Running this line will likely take some time, especially if UltraDark.jl has not been precompiled by your Julia installation.

@time simulate!(grids, output_config)
[ Info: Reached time 0.5000000000000002
[ Info: Reached time 1.0
[ Info: Reached time 1.5000000000000009
[ Info: Reached time 2.0
  4.010516 seconds (40.19 k allocations: 746.614 MiB, 0.48% gc time, 1.02% compilation time)

We initialised the soliton with a nonzero velocity. Let's check if the soliton moved during the simulation.

argmax(grids.ρx)
CartesianIndex(45, 32, 32)

Yes, it did – the maximum is now at a different location.

We can also check this by loading output files from output_dir and plotting them.

using CairoMakie
using LaTeXStrings
using NPZ

fig = Figure()

for (i, t_index) in enumerate([1, length(output_times)])
    rho= npzread(joinpath(output_dir, "rho_$(t_index).npy"))
    ax = Axis(fig[1, i], title=L"$t = %$(output_times[t_index])$", aspect=DataAspect())
    heatmap!(grids.x[:, 1, 1], grids.y[1, :, 1], rho[:, :, Int(resol//2)])
end
fig

After understanding this overview you may want to browse the example notebooks to see more complex simulations and analysis.

Please open an issue if you run into problems or have feature requests.