Simulation
Main Simulation Functions
simulate_waveDM
WaveDM.simulate_waveDM — Function
Test function for Milky Way-like simulations with MOND.
This function sets up and runs a simulation of the Milky Way or dwarf galaxies with MOND effects.
Arguments
model: Simulation model type (:MW for Milky Way, :dwarf for dwarf galaxies)V: External potential functionFDM_mass_ratio: Ratio of FDM mass to total massFDM_radius_ratio: Ratio of FDM radius to total radiustitle: Simulation title for output filesXmax,Ymax,Zmax: Simulation box dimensions (in dimensionless units)Tmax: Total simulation time (in dimensionless units)Nt: Number of time stepsNx,Ny,Nz: Number of grid points in each dimensionNp: Number of particles for each baryon componentabsorb_coeff: Absorption coefficient at boundariesStepsBetweenSnapshots: Frequency of visualization updatesIC_vel: Initial velocity field (or path to load from file)IC_only: If true, return initial conditions without running simulationstatic: If true, no velocity, phases are all zerooutputdir: Directory for saving output filesmassRadius: Mass radius for the galaxybaryon_β,baryon_ρ0,baryon_r0: Baryonic density profile parametershalo_β,halo_ρ0,halo_r0,halo_α,halo_γ,halo_Q: Dark halo density profile parametersGravitySolver: Gravitational solver type (Tree() or DirectSummation())boundary: Boundary conditions (Periodic() or Vacuum())SofteningLength: Gravitational softening lengthbaryon_mode: How baryons are treated (:mesh, :particlesstatic, :particlesdynamic, :ignored)gpu: Enable GPU acceleration
Returns
- If
IC_only=true: Initial velocity fieldIC_vel - Otherwise: Results from
SPE3D_waveDMfunction
The high-level entry point. It builds the initial conditions (using AstroIC.jl), creates a SimulationGrid, calls SPE3D_waveDM to evolve the SPE, then handles visualization and post-processing. Most of the keyword arguments listed below can be supplied directly to simulate_waveDM; the low-level-only ones are marked in the tables.
SPE3D_waveDM
WaveDM.SPE3D_waveDM — Function
Main simulation function for 3D Schrödinger-Poisson equation with MOND.
This function handles the complete simulation workflow from initial condition setup to result visualization.
Arguments
Xmax,Ymax,Zmax: Simulation box dimensions (in dimensionless units)Tmax: Total simulation time (in dimensionless units)Nx,Ny,Nz: Number of grid points in each dimensionNt: Number of time stepsautoset_timestep: Automatically set timestep based on stability criteriaIC: Initial wavefunction or distribution functionV: External potential functionκ: Self-interaction parameterbaryon: Baryonic density contributionbaryon_mode: How baryons are treated (:ignored, :mesh, :particlesstatic, :particlesdynamic)baryon_potential: Pre-computed potentials of static baryonsax_b,ay_b,az_b: Baryonic acceleration componentsabsorb_coeff: Absorption coefficient at boundariesoutputdir: Directory for saving output filestitle: Simulation title for output filesboundary: Boundary conditions (Periodic() or Vacuum())SofteningLength: Gravitational softening lengthgpu: Enable GPU accelerationRealtime: Enable real-time visualizationStepsBetweenSnapshots: Frequency of visualization updatesFDM_mass_ratio: Ratio of FDM mass to total massFDM_radius_ratio: Ratio of FDM radius to total radius
Returns
ψ: Final wavefunctionfig: Overview plot figurefigMOND: MOND-related plot figurechi2RC: Chi-squared value for rotation curve fitdfProp: DataFrame with simulation propertiesdfAcc: DataFrame with acceleration dataaveraged_ψ2: Time-averaged wavefunction squared
The lower-level entry point. It expects you to provide the initial wavefunction IC and (optionally) the baryonic density/potential/forces yourself. Use simulate_waveDM unless you are integrating WaveDM.jl into a larger pipeline.
Defaults in the tables below refer to simulate_waveDM. The low-level SPE3D_waveDM has different defaults: Xmax = 5, Nx = 64, Tmax = 1, Nt = 32, absorb_coeff = 0, gpu = true, and IC = (x,y,z) -> exp(-x²-y²-z²). Additional low-level keywords are forwarded to SPE3D_waveDM; IC itself is not a high-level keyword (use IC_vel).
Simulation Workflow
flowchart LR
A[Grid setup] --> B[Initial conditions]
B --> C[Baryon setup<br/>mesh / static / dynamic]
C --> D[IC potential]
D --> E[Visualization init]
E --> F[KDK main loop]
F -->|every StepsBetweenSnapshots| E
F --> G[Outputs<br/>CSV, JLD2, PNG, MP4]- Grid setup — uniform Cartesian grid (
Nx × Ny × Nz, side lengths2*Xmaxetc.). - Initial conditions — the initial wavefunction (
IC_velat the high level,ICat the low level) andV(external potential) are evaluated on the grid. - Baryon setup — depending on
baryon_mode, baryons are placed on the mesh, sampled as static N-body particles, or co-evolved as dynamic N-body particles. - IC potential — the gravitational potential $\Phi_{\rm total}$ at $t = 0$ is computed (FFT for periodic; an isolated tree solve for vacuum in the current driver).
- Visualization init — observables for
Realtimeand the per-snapshot update are wired up. - KDK main loop —
apply_kick_step!→apply_drift_step!forNtsteps. - Outputs — per-step and final files (CSV for properties, JLD2 for $\psi$/$\Phi$, PNG/MP4 for visualization).
Key Parameters
Grid and resolution
| Keyword | Default | Description |
|---|---|---|
Xmax, Ymax, Zmax | 2.0, Xmax, Xmax | Half-side length of the box in dimensionless units (full side L = 2*Xmax); low-level default is 5 |
Nx, Ny, Nz | 256, Nx, Nx | Grid points per axis; powers of two are fastest; low-level default is 64 |
Tmax | 0.1 | Total simulation time in dimensionless units (or u"Gyr" if Unitful is loaded); low-level default is 1 |
Nt | 5000 | Number of time steps (overridden when autoset_timestep = true); low-level default is 32 |
bulk_perturb, bulk_size | true, 8 | Pooling of the initial velocity field; the paper's Crater II runs use bulk_size = 4 |
Initial conditions
| Keyword | Default | Description |
|---|---|---|
IC_vel | nothing | High-level initial wavefunction: nothing generates it from model; a String loads a JLD2 written by save_IC = true; an array is used directly |
IC | Gaussian $(x,y,z)\mapsto e^{-x^2-y^2-z^2}$ | Low-level (SPE3D_waveDM) initial wavefunction — a function of $(x,y,z)$ returning a complex number |
V | (x,y,z,ψ)->0 | External / self-interaction potential $V(\mathbf r, t, \psi)$ |
κ | 0 | Self-interaction strength. 0 (free), > 0 (repulsive), < 0 (attractive) |
baryon | 0 | Static baryon density (low-level; used only by baryon_mode = :mesh) |
mₐ | 0.2×10⁻²² eV/c² | Boson mass in physical units (drives the dimensionless scale). The paper's Crater II runs use mₐ = 50 * 1e-22 * 1.783e-36u"kg" |
Ωₘ₀ | 0.31 | Present-day matter density parameter |
MW_disk_RC | false | For model = :MW: drive the velocity field from the MW rotation curve |
baryon_fraction_limit | 1 | Upper bound on the baryon-to-total mass ratio used when generating model = :dwarf ICs |
Baryons
| Keyword | Default | Description |
|---|---|---|
baryon_mode | :mesh | One of :ignored, :mesh, :particles_static, :particles_dynamic. Presets with no baryonic model (e.g. :dwarf_UFDs, :dwarf_NFW, :dwarf_Zhao) only accept :ignored. The paper quotes :ignored as the default; the current code default is :mesh |
baryon_potential | nothing | Pre-computed $\Phi_{\rm baryon}$; skip the Poisson solve |
baryon_particles | nothing | StructArray of N-body particles (Star / Gas) for static or dynamic modes |
SofteningLength | 0.1u"kpc" | Gravitational softening length for the N-body force, of order the mean interparticle spacing $d=(V/N)^{1/3}$. The paper quotes ~1 kpc; the current code uses 0.1 kpc |
GravitySolver | Tree() | Tree() ($\mathcal O(N\log N)$, Gadget-2) or DirectSum() ($\mathcal O(N^2)$) |
baryon_mode values:
:ignored— no baryons. Use this explicitly for dark-matter-only halos and for presets that do not model baryons; the package default is:mesh.:mesh— baryons live on the same FFT grid; potential computed by the same FFT Poisson solver as the DM. Best for spatially extended baryons (e.g. dSph, ETG).:particles_static— baryons are sampled once as N-body particles and held fixed; their gravitational potential is added to $\Phi_{\rm total}$ at every step.:particles_dynamic— baryons are co-evolved with the DM; their potential is re-computed at every step and the DM acceleration is interpolated back to the particles. Most expensive, most realistic.
Tidal field
| Keyword | Default | Description |
|---|---|---|
MW_tidal_field | false | Add a time-dependent MW tidal field to $\Phi_{\rm total}$ |
LMC_tidal_field | false | Add LMC perturbation (requires MW_tidal_field = true) |
MW_tidal_interpolate | true | Use a pre-computed splined MW potential grid (MW_pot) for speed |
MW_pot | nothing | Path to a JLD2 file or a pre-built potential grid |
MW_pot_Xmax, MW_pot_Ymax, MW_pot_Zmax | 100.0u"kpc" | Half-side lengths of the MW potential interpolation box |
MW_pot_N | 512 | Resolution of the MW potential interpolation box |
export_MW_pot | false | If true, save the MW tidal-field interpolation grid to a JLD2 file |
tidal_BG_sim | nothing | Pre-built MW background N-body Simulation (bypasses MW_pot) |
tidal_lookback_time | 0.0u"Gyr" | How far back in time the tidal field is applied |
df_traj | empty DataFrame | Trajectory table (columns time, x, y, z) from the lookback step. Must be supplied when MW_tidal_field = true |
tidal_initial_pos, tidal_initial_vel | Crater II defaults | Initial Galactocentric position / velocity of the satellite |
particles_LMC | nothing | StructArray of LMC particles (alternative to analytic Plummer) |
Boundary conditions
| Keyword | Default | Description |
|---|---|---|
boundary | Periodic() | Periodic() (FFT-based) or Vacuum() (isolated; see Algorithms §4.2) |
absorb_coeff | 10 | Absorbing-layer coefficient $\alpha$; 0 disables the absorber. The low-level default is 0 |
Timestep
| Keyword | Default | Description |
|---|---|---|
autoset_timestep | false | Choose $\Delta t$ from the CFL criterion automatically |
autoset_timestep_ratio | 0.9 | Safety factor $\zeta_t$ for the auto-timestep |
Performance
| Keyword | Default | Description |
|---|---|---|
gpu | false | Use CUDA.jl for FFT and element-wise work. The low-level default is true |
distributed_memory | false | Use Distributed.jl DArray for grid arrays. The distributed FFT Poisson solver is not implemented in the current release, so the full SSFM loop still requires a single process |
pids | workers() | Worker process IDs for distributed runs |
KDK_flag | true | If true, use the second-order kick–drift–kick scheme |
Visualization
| Keyword | Default | Description |
|---|---|---|
Realtime | true | Open an interactive GLMakie window |
unicode_plot | false | Render a terminal heat-map of the density slice via UnicodePlots.jl |
unicode_heatmap_width | 150 | Width in characters of the terminal heat-map |
StepsBetweenSnapshots | 5 | Update the visualization and save frames every N steps |
size | (2400, 1400) | Figure size of the overview plot |
plotOptical | false | Plot baryonic (optical) properties in addition to wave-DM |
plotWaveDM | false | Plot the wave-DM-only view alongside the total |
plot_virial | false | Add a virial-energy axis to the live figure |
dynamic_colorrange | true | Auto-scale the colormap to the running maximum density |
extract_dwarf_granule | false | Post-process: extract the snapshot best fitting a target profile/RC |
extract_mode | :profile | :profile or :RC extraction mode |
extract_min_t | 0.2u"Gyr" | Earliest simulation time considered for extraction |
best_fit_halo_mass | false | If true, optimize the halo mass during the RC fit |
Output
| Keyword | Default | Description |
|---|---|---|
title | "WaveDM_MOND_MW" | Run title (used in output filenames); the low-level default is "WaveDM" |
outputdir | joinpath(@__DIR__, "output/MOND") | Where CSV / JLD2 / PNG / MP4 files are written; the low-level default is joinpath(@__DIR__, "output") |
save_IC | true | Save the initial conditions to disk |
save_phi | true | Save the final $\Phi$ and $\psi$ to JLD2 |
export_particles | false | If true, return the sampled baryon particles (no SPE run) |
IC_only | false | If true, return the initial wavefunction (no SPE run) |
Minimal Working Examples
1. Smoke test (CPU)
using WaveDM
simulate_waveDM(;
model = :MW,
baryon_mode = :ignored,
Xmax = 2.0,
Tmax = 0.001,
Nt = 3,
Nx = Ny = Nz = 64,
gpu = false,
Realtime = false,
unicode_plot = true,
outputdir = mktempdir(),
)2. GPU production run
using WaveDM
using Unitful
simulate_waveDM(;
model = :MW,
Xmax = 5.0,
Tmax = 5.0u"Gyr",
Nx = Ny = Nz = 256,
Nt = 5000,
autoset_timestep = true,
gpu = true,
Realtime = true,
plot_virial = true,
title = "MW_256_T5Gyr",
outputdir = "results/MW_256_T5Gyr",
)3. Crater II ultra-faint dwarf
using WaveDM
using Unitful
simulate_waveDM(;
model = :dwarf_UFDs,
Galaxy_id = 6, # Crater II
baryon_mode = :ignored, # Crater II is DM-dominated
mₐ = 50 * 1e-22 * 1.783e-36u"kg", # m₂₂ = 50
V = (x, y, z, ψ) -> 0.0,
Nx = 384,
Xmax = 20u"kpc",
Tmax = 6.0u"Gyr",
autoset_timestep = true,
absorb_coeff = 10,
bulk_size = 4, # value used in the paper
gpu = true,
Realtime = true,
title = "CraterII",
)4. Crater II with MW tidal field
using WaveDM
using Unitful
simulate_waveDM(;
model = :dwarf_UFDs,
Galaxy_id = 6,
baryon_mode = :ignored,
mₐ = 50 * 1e-22 * 1.783e-36u"kg",
V = (x, y, z, ψ) -> 0.0,
Nx = 384,
Xmax = 20u"kpc",
Tmax = 6.0u"Gyr",
autoset_timestep = true,
absorb_coeff = 10,
MW_tidal_field = true,
df_traj = df_traj, # from the lookback step (see Examples §4)
tidal_lookback_time = 6.0u"Gyr",
LMC_tidal_field = true, # include LMC perturbation
gpu = true,
title = "CraterII_MW_LMC",
)5. Baryons as dynamic N-body particles
using WaveDM
using AstroIC
using PhysicalParticles, Unitful
baryons = generate(AstroIC.ExponentialDisk(; ...)) # see AstroIC.jl
simulate_waveDM(;
Nx = 256,
Xmax = 5.0,
Tmax = 2.0u"Gyr",
autoset_timestep = true,
baryon_mode = :particles_dynamic,
baryon_particles = baryons,
GravitySolver = Tree(),
SofteningLength = 0.1u"kpc", # code default
gpu = true,
)6. Distributed run
The distributed FFT Poisson solver is not implemented in the current release: parallel_poisson raises an error for kind = :distributed. Distributed workers currently accelerate initial-condition generation and N-body work, but a full SPE run must use the serial/threads or GPU backend.
using Distributed
addprocs(8)
@everywhere using WaveDM
simulate_waveDM(;
Nx = Ny = Nz = 256,
Tmax = 2.0u"Gyr",
autoset_timestep = true,
distributed_memory = true, # use DArray for grid arrays
gpu = false, # GPU and distributed are not orthogonal but mixing
# them is an advanced use case
)Return Values
ψ, fig, figMOND, chi2RC, dfProp, dfAcc, averaged_ψ2 = simulate_waveDM(...)| Return | Description |
|---|---|
ψ | Final wavefunction (complex array, dimensionless units) |
fig | Overview Makie figure (snapshot of the live plot at end of run) |
figMOND | MOND / RAR diagnostic figure, or nothing if baryon_mode = :ignored |
chi2RC | $\chi^2$ of the rotation-curve fit, or nothing for DM-only runs |
dfProp | DataFrame with the time series of total mass, mass-fraction radii, virial energies, momenta |
dfAcc | DataFrame with the radial acceleration profile (DM-only or DM+baryon+MOND) |
averaged_ψ2 | Time-averaged $ |
Output Files
The simulation writes several file types into outputdir:
$(title), $(suffix) - Overview IC.png— initial snapshot$(title), $(suffix) - Overview.mp4— animation of the entire run (ifRealtime = true)$(title), $(suffix) - Overview Prop.png— final-state overview$(title), $(suffix) - RC.png/RAR.png— rotation curve / radial acceleration relation$(title), $(suffix) - Prop best fit.jld2— best-fit snapshot (ifextract_dwarf_granule = true)$(title), $(suffix) - acc.csv/acc best fit.csv— radial acceleration data$(title), $(suffix) - best_fit_time.csv— best-fit simulation time$(title), $(suffix) - timestep_*.jld2— per-snapshot ψ / Φ (ifaverage_snapshots = true)
The suffix is a human-readable summary of the run, e.g. Nx(384), Xmax(20.00), Nt(377), Tmax(6.00), DM_m(1.00).
Performance Considerations
- GPU vs CPU.
gpu = trueis most useful for grids $\gtrsim 256^3$. For tiny problems the kernel-launch overhead dominates and CPU is faster. On a 12 GB device, $N \gtrsim 512$ becomes memory-limited. - Distributed memory. The distributed FFT Poisson solver is not implemented yet; use distributed workers for IC generation and N-body work only.
- Grid size. Powers of two (64, 128, 256, …) maximize FFT performance. The code also runs on non-power-of-two grids.
- Timestep.
autoset_timestep = truekeeps $\Delta t$ near the CFL limit without manual tuning. StepsBetweenSnapshots. Larger values skip more visualization updates per step, which is useful when the plot is the bottleneck.- Memory. On GPU, $N^3$ complex arrays dominate. A $512^3$ single-precision grid is $\sim 2$ GiB; a $1024^3$ grid is $\sim 16$ GiB.
KDK_flag. Leave attrue. Setting it tofalsereduces the scheme to first-order Lie splitting and is provided only for code-experimentation purposes.