Mapping SPH Data to Cartesian Grids
Mapping SPH particles to a grid instead of simply plotting color-coded particle positions shows the actual gas quantities as they are used in an SPH code: weighted with a kernel, according to their distance to each other. You can see this in the following plot, left are color-coded particle positions, right is the mean density in the SPH particles along the line of sight, interpolated to a grid.
You can map SPH data to a grid using the function sphMapping
, which comes in two flavors: CIC and TSC.
Define parameters for mapping
To map the data you need to define the mapping parameters via the mappingParameters
struct.
SPHtoGrid.mappingParameters
— TypemappingParameters( T::DataType=Float64;
x_lim::Vector{Float64} = [-1.0, -1.0],
y_lim::Vector{Float64} = [-1.0, -1.0],
z_lim::Vector{Float64} = [-1.0, -1.0],
center::Vector{Float64} = [-1.0, -1.0, -1.0],
x_size::Float64 = -1.0,
y_size::Float64 = -1.0,
z_size::Float64 = -1.0,
pixelSideLength::Float64 = -1.0,
Npixels::Int64 = 0)
Parameter object for sph to grid mapping. Define either *_lim
, or center
and *_size
. Resolution is defined by pixelSideLength
or Npixels
.
One way to set this up is by defining the limits of the map as
par = mappingParameters(xlim=[xmin, xmax],
ylim=[ymin, ymax],
zlim=[zmin, zmax],
Npixels=200)
or give a center position and the size in each direction
par = mappingParameters(center=[x0, y0, z0],
x_size=x_size,
y_size=y_size,
z_size=z_size,
Npixels=200)
Instead of Npixels you can also give the keyword argument pixelSideLength
if you prefer to define your image that way.
If you are mapping a periodic box you also can give the keyword boxsize
to enable periodic mapping.
par = mappingParameters(center=[x0, y0, z0],
x_size=x_size,
y_size=y_size,
z_size=z_size,
boxsize=boxsize,
Npixels=200)
CIC
For "Counts in Cell" (CIC) interpolation use the function sphMapping
with these input values:
SPHtoGrid.sphMapping
— MethodsphMapping( Pos::Array{<:Real}, HSML::Array{<:Real}, M::Array{<:Real},
Rho::Array{<:Real}, Bin_Quant::Array{<:Real},
Weights::Array{<:Real}=Rho;
param::mappingParameters,
kernel::AbstractSPHKernel,
show_progress::Bool=true,
parallel::Bool=false,
reduce_image::Bool=true,
return_both_maps::Bool=false,
filter_particles::Bool=true,
dimensions::Int=2,
calc_mean::Bool=false)
Maps the data in Bin_Quant
to a grid. Parameters of mapping are supplied in param
and the kernel to be used in kernel
.
Arguments
Pos
: Matrix (3xNpart) with particle positionsHSML
: Array with particle hsmlM
: Array with particle massesRho
: Array with particle densitiesBin_Quant
: Array with particle quantity to be mappedWeights
: Array with weights. Defaults to density-weightedparam
:mappingParameters
for this mapkernel
:AbstractSPHKernel
to be used for mappingshow_progress
: Show progress barparallel
: Run on multiple processorsreduce_image
: If weights need to be applied or not. Set tofalse
forpart_weight_physical
return_both_maps
: Returns the full image array. To be used with parallel mapping of subfilesfilter_particles
: Find the particles that are actually contained in the imagedimensions
: Number of mapping dimensions (2 = to grid, 3 = to cube)calc_mean
: Calculates the mean value along the line of sight. If set tofalse
the particle only contributes if itsBin_Quant
is larger than 0
Select Kernel
You also need to choose the kernel you used in the simulation. For this you need to install the package SPHKernels.jl. You can currently use these kernels:
k = Cubic()
k = Quintic()
k = WendlandC4()
k = WendlandC6()
k = WendlandC8()
Please see the SPHKernels docs for more details.
Mapping
With the setup done you can now map (e.g.) density of your data using the function above as:
image = sphMapping(x, hsml, m, rho, rho, param=par, kernel=k)
Replacing the second rho
with any other quantity would map that quantity of course. Please note: This function doesn't do any unit conversion for you, so you need to convert to the desired units beforehand. You can do this e.g. with GadgetUnits.jl.
Image now contains a 2D array with the binned data and can easily be plotted with imshow()
from any plotting package of your choosing.
The keyword parallel = true
causes the run to use multiple processors. For this you need to start julia with julia -p <N>
where <N>
is the number of processors in your machine, or define
using Distributed
addprocs(8)
# now you can load SPHtoGrid
using SPHtoGrid
Conserved quantities
Particles are mapped to a grid while also conserving the particle volume, following the algorithm described in Dolag et. al. 2006.
Weight functions
With the mapping you may decide to use a specific weighting function. For this you can pass the optional variable Weights
in sphMapping
.
You can either use your own weight functions or use one of the built-in ones:
SPHtoGrid.part_weight_one
— Functionpart_weight_one(N::Integer)
Equivalent to no weighting. Returns an Array of ones.
SPHtoGrid.part_weight_physical
— Functionpart_weight_physical(N::Integer, par::mappingParameters, x_cgs::Real=3.085678e21)
Physical weighting function in units of [cm/pix]. To be used with sphMapping
.
part_weight_physical(N::Integer, x_cgs::Real=3.085678e21)
Physical weighting function in units of [cm]. To be used with healpix_map
.
SPHtoGrid.part_weight_emission
— Functionpart_weight_emission(rho::Array{<:Real}, T_K::Array{<:Real})
Emission weighted mapping. Takes density in internal untis and temperature in K and computes weights.
SPHtoGrid.part_weight_spectroscopic
— Functionpart_weight_spectroscopic(rho::Array{<:Real}, T_K::Array{<:Real})
Spectroscopic weighted mapping from Mazotta+ 04. Takes density and temperature and computes weights.
SPHtoGrid.part_weight_XrayBand
— Functionpart_weight_XrayBand(T_K::Array{<:Real}, Emin::Real, Emax::Real)
Computes Xray weighted emission of a defined energy band. Emin and Emax are energies in eV.
Units
As you have to handle unit conversion yourself please note that internally the image contains two components: image
is the mapped pixel value and wimage
is just the geometry weighting of the particle which will be applied if reduce_image=true
in sphMapping
.
This means that the resulting images have the units:
image = [Bin_Quant] * [Weights] * [pix^2]
wimage = [Weights] * [pix^2]
Helper Function
If you're lazy like me and don't want to go through the entire process of image reduction and saving the fits file by hand you can use
SPHtoGrid.map_it
— Functionmap_it(pos_in, hsml, mass, rho, bin_q, weights, RM=nothing;
param::mappingParameters,
kernel::AbstractSPHKernel=WendlandC6(2),
snap::Integer=0,
units::AbstractString="",
image_prefix::String="dummy",
reduce_image::Bool=true,
parallel=true,
calc_mean::Bool=true, show_progress::Bool=true,
sort_z::Bool=false,
stokes::Bool=false,
projection="xy")
Small helper function to copy positions, map particles and save the fits file.
Arguments
pos_in
: Matrix (3xNpart) with particle positionshsml
: Array with particle hsmlmass
: Array with particle massesrho
: Array with particle densitiesbin_q
: Array with particle quantity to be mappedweights
: Array with weights. Defaults to density-weightedparam
:mappingParameters
for this mapkernel
:AbstractSPHKernel
to be used for mappingunits
: Unit string will be saved to FITS fileimage_prefix
: Name of the file to save, withou.fits
file-endingreduce_image
: If weights need to be applied or not. Set tofalse
forpart_weight_physical
parallel
: Run on multiple processors.calc_mean
: Calculates the mean value along the line of sight. If set tofalse
the particle only contributes if itsbin_q
is larger than 0.show_progress
: Show progress barsort_z
: Sort the particles according to their line-of-sight direction. Needed for polarisation mapping.stokes
: Set totrue
of you are mapping Stokes parameters to account for Faraday rotation of the polarisation angle.projection
: Which plane the position should be rotated in. Can also be an Array of 3 Euler angles (in [°]) (not used yet!)
Distributed mapping
If you want to map a region of the simulation that is too large to fit into memory you can instead produce a map of the individual sub-snapshots of a simulation and combine them in the end. For this you can use the helper function distributed_cic_map
. If run on multiple cores it will dynamically dispatch parallel jobs for individual sub-snaps.
SPHtoGrid.distributed_cic_map
— Functiondistributed_cic_map(cic_filename::String, Nsubfiles::Integer,
mapping_function::Function,
Nimages::Integer, param::mappingParameters;
reduce_image::Bool=true,
Ndim::Integer=2,
snap::Integer=0,
units::String="",
vtk::Bool=true)
Dynamically dispatches workers to compute one cic map per subfile, sum up the results and save to a fits file.
Arguments
cic_filename::String
: Name of the file under which the image should be savedNsubfiles::Integer
: Number of subfiles the snapshot is distributed over.mapping_function::Function
: The function to be executed per subfile. Must return aTuple
of the cic image(s) and the weight_image.Nimages::Integer=1
: Number of cic image that are constructed simultaniously. Only useful if you map multiple quantities at once, so it defaults to1
.param::mappingParameters
: Parameters for the mapping, needed to save the image in the usual way.reduce_image
: If the final image should be divided by the weight image set totrue
.Ndim::Integer=2
: Dimensions of image, must be either2
or3
.snap::Integer=0
: Number of the input snapshot, used for saving the image.units::String=""
: Units of the image, used for saving the image.
Example:
println("allocating cores")
using Distributed, ClusterManagers
# automatically decide if it needs to be run in slurm envirnoment
try
println("allocating $(ENV["SLURM_NTASKS"]) slurm tasks")
addprocs_slurm(parse(Int64, ENV["SLURM_NTASKS"]))
catch err
if isa(err, KeyError)
println("allocating 2 normal tasks")
addprocs(2)
end
end
println("done")
flush(stdout);
flush(stderr);
println("loading packages")
@everywhere using GadgetIO, GadgetUnits
@everywhere using Printf
@everywhere using SPHKernels, SPHtoGrid
println("done")
flush(stdout);
flush(stderr);
# snap base and unit conversion must be set constant here
@everywhere const global snap_base = "/path/to/sim/snapdir_011/snap_011"
@everywhere const global h = GadgetIO.read_header(snap_base)
@everywhere const global GU = GadgetPhysical(h)
# same for mapping parameters
@everywhere const gpos = [245040.45, 327781.84, 246168.69]
@everywhere const rvir = 10.0e3
@everywhere const xy_size = 2rvir
@everywhere const param = mappingParameters(center=gpos .* GU.x_physical,
x_size=xy_size * GU.x_physical,
y_size=xy_size * GU.x_physical,
z_size=xy_size * GU.x_physical,
Npixels=1024)
# function that does the actual mapping. Must only take the subfile as an argument!
@everywhere function Bfld_map_of_subfile(subfile)
println("B: subfile $subfile running on $(nthreads()) threads")
flush(stdout); flush(stderr)
# read the relevant data
hsml = read_block(snap_base * ".$subfile", "HSML", parttype=0) .* GU.x_physical
rho = read_block(snap_base * ".$subfile", "RHO", parttype=0) .* GU.rho_physical
m = read_block(snap_base * ".$subfile", "MASS", parttype=0) .* GU.m_physical
pos = read_block(snap_base * ".$subfile", "POS", parttype=0) .* GU.x_physical
# we want to map the magnetic field
B = read_block(snap_base * ".$subfile", "BFLD", parttype=0)
Babs = @. sqrt( B[1,:]^2 + B[2,:]^2 + B[3,:]^2 ) * 1.e6
# kernel used for mapping
kernel = WendlandC4(2)
map = sphMapping(pos, hsml, m, rho, Babs, rho,
show_progress=false, parallel=false, return_both_maps=true; # these settings are important!
param, kernel)
# enforce de-allocation of memory
pos = hsml = rho = m = nothing
B = Babs = nothing
GC.gc()
# return quantity map(s) and weight map individually
return map[:,1:end-1], map[:,end]
end
filename = "/path/to/savefile.fits"
# number of sub-snapshots
Nsubsnaps = 1024
# run the mapping
distributed_cic_map(filename, Nsubsnaps, Bfld_map_of_subfile, param)
Comparison to Smac
As a reference for the mapping we use Smac (Dolag et al. 2005). You can see the comparison with the relative error in the following images. Differences in the LOS integration stem from a relative shift of the SPHtoGrid image by one pixel. The bulk of the error in the density weighted map stems from a slightly different calculation of the temperature.
Line-of-sight integration | Density weighted |
---|---|
![]() | ![]() |