Projections API Reference

Functions for creating 2D projections from 3D simulation data.

Exported Functions

Main Projection Function

Function: projection, create 2D projections from 3D simulation data

The projection function uses Julia's multiple dispatch to provide specialized implementations for different data types. Since the complete API documentation is extensive, this section provides focused guidance for each data type.

Periodic boxes

On a periodic run a structure sitting on a box face is split across opposite edges of the map. periodic_recenter rolls a finished projection around the boundary so it appears whole, which is exact for an axis-aligned map because a whole-pixel shift is a translation of the box.

Mera.@project — Macro
@project data quantity... [keyword=value...]

Project several quantities and bind each map to a variable of that name.

projection returns one object whose maps is a dictionary, so pulling several quantities out of it is a line of lookups:

pj = projection(gas, [:sd, :T], myargs=args)
sd = pj.maps[:sd]; T = pj.maps[:T]

This does the same in one line, and asks for the quantities together, which is also the form that lets the projection use its threads (see Performance):

@project gas sd T myargs=args
# sd and T are now the maps, and proj is the full projection object

Keywords pass straight through, so off-axis works the same way:

@project gas sd inclination=60 azimuth=30 binning=:exact

The full object is bound as proj, so the extent, units and everything else stay reachable:

heatmap(proj.extent[1:2], proj.extent[3:4], sd)

Like @loadall, this binds names in your scope. Inside a function or a package, prefer the explicit form above.

See also: projection, @loadall.

Mera.periodic_recenter — Function
periodic_recenter(m; center=[0., 0., 0.], direction=:z, range_unit=:standard, verbose=verbose_mode)

Roll a projection so that center sits in the middle of the map.

On a periodic run a structure on a box face appears split across opposite edges of a projection. This shifts the map around the periodic boundary so it appears whole, and relabels extent and cextent so both are measured from center, which is then at (0, 0).

The shift is by whole pixels, so no value is altered and no interpolation happens: the sum over the map, and hence any total it represents, is unchanged. Only the frame moves.

center follows the usual convention: box fractions with range_unit=:standard (the default), or a physical length with range_unit=:kpc and friends. [:bc] means the box centre. A shift is only meaningful on an axis that actually wraps; if getinfo determined the run is not periodic, this warns rather than refusing, since the map may have come from somewhere else.

Pass the same direction you gave projection. A projection does not record which axis it looked along, so this cannot be inferred: with direction=:z the map spans x and y, with :x it spans y and z, with :y it spans x and z. Giving the wrong one rolls along the wrong axes.

Off-axis projections are not supported and are refused. Rolling only works because a whole-pixel shift of an axis-aligned map is the same thing as translating the box by a lattice vector. An off-axis camera plane is tilted with respect to the box, so no shift of its pixels corresponds to a periodic translation, and the result would be wrong rather than merely approximate.

p = projection(gas, :sd, :Msol_pc2)
q = periodic_recenter(p, center=[0., 0., 0.])   # the blast at the origin, made whole
heatmap(q.extent[1:2], q.extent[3:4], q.maps[:sd])

See also: projection.

See Periodic Boxes for what else needs care on a wrapping run.

Performance & Threading Functions

Data Type Support

Hydro and RT projections

The hydro methods dispatch on Union{HydroDataType, RtDataType}, the same call works on an object from getrt.

# Single variable, code units / with a unit
projection(dataobject::Union{HydroDataType, RtDataType}, var::Symbol)
projection(dataobject::Union{HydroDataType, RtDataType}, var::Symbol, unit::Symbol)

# Several variables, one unit each / one unit for all
projection(dataobject::Union{HydroDataType, RtDataType}, vars::Array{Symbol,1}, units::Array{Symbol,1})
projection(dataobject::Union{HydroDataType, RtDataType}, vars::Array{Symbol,1}, unit::Symbol)

Gravity (combined form)

Gravity quantities are projected by passing the gravity object alongside the hydro one, the cells come from the hydro object, the quantity from gravity:

projection(hydro::HydroDataType, gravity::GravDataType, var::Symbol, unit::Symbol)

Common keyword arguments

These are the grid (hydro / gravity / RT) keywords. The particle projection differs: its binning default is :cic, its weighting is a bare Symbol, and it has no nmax, depth, split, refine or refine_to at all (passing one is a MethodError). It adds parttypes, thickness/offset and nlos. See the particle section below and the off-axis cheatsheet for the full split.

KeywordWhat it does
pxsize=[value, :unit]physical size of a map pixel, the preferred way to set resolution
resgrid cells per side instead of a physical pixel size
lmaxsets the default resolution, res = 2^lmax. It does not coarsen the data: the deposit always runs over every level the object holds
direction:x, :y, :z (default :z), or :faceon/:edgeon, which derive the orientation from the data's own angular momentum
los, up, theta, phi, inclination, azimuthoff-axis line of sight: see Off-axis
weightinghow intensive quantities are averaged (see the note below)
mode:standard is a weighted mean, Σ(q·w)/Σw; :sum is Σ(q·w) on grid data (a plain per-pixel total Σq only for :ekin, :etherm and :volume) and Σq on particles. Neither divides by area: only :sd does that, in both modes. :sd and :mass ignore mode entirely
maska boolean array from getmask, applied before projecting; combines with fov
fov, fov_unit, aperturea rotation-invariant camera-plane window; fov=[wx,wy] for a rectangle (needs aperture=:square)
depth, depth_unita slab cut along the line of sight, off-axis only
splitweight partial cells by the volume inside the depth slab or fov sphere, instead of a hard cell-centre cut
refine, refine_tosubdivide the cells the fov sphere crosses so the edge sharpens; need fov and split=true
angmom_radiusrestrict the :faceon/:edgeon/:angmom orientation sum to a sphere; on grid and particle data
binning, nmaxhow a rotated cell is deposited: :ngp, :cic, :overlap (default), :exact
center, range_unitwhich part of the box to project, and in what units
data_center, data_center_unitorigin the map axes and cylindrical/spherical quantities are measured from
xrange, yrange, zrangerestrict the projected volume
max_threadscap the threads used
myargspass a bundle instead of repeating keywords: see Bundling Arguments
`weighting` has a different type for particles

Hydro, gravity and RT take an array: weighting=[:mass], weighting=[:volume], or [:quantity, unit]. Particle projections take a bare symbol: weighting=:mass, :volume, :sph or :voronoi. Passing a symbol to a hydro projection raises TypeError: expected Vector, got Symbol.

Key features:

  • AMR-aware grid mapping with conservative mass preservation
  • Variable-based parallel processing (8+ threads)
  • Mass-weighted averaging for intensive quantities

Common variables: :rho, :T, :sd, :mass, :v, :p, :cs, the box-frame velocity dispersions (:σx, :σy, :σz, :σ, and the cylindrical and spherical ones, all available on both paths), and off-axis only: :vlos/:σlos, :vcamx/:σcamx, :vcamy/:σcamy, :ncell

Tutorial: Hydro Projections, complete examples and usage

Particle Data Projections (PartDataType)

Key Method Signatures:

# Single variable with default units
projection(dataobject::PartDataType, var::Symbol)

# Single variable with custom units
projection(dataobject::PartDataType, var::Symbol, unit::Symbol)

# Multiple variables with custom units
projection(dataobject::PartDataType, vars::Array{Symbol,1}, units::Array{Symbol,1})

# Multiple variables with same units
projection(dataobject::PartDataType, vars::Array{Symbol,1}, unit::Symbol)

Key features:

  • Mass-weighted binning for discrete particles
  • getvar with :age returns stellar ages relative to the snapshot time
  • mode=:sum gives the per-pixel total instead of a weighted mean, which is how you get a real mass map: projection(part, :mass, :Msol; mode=:sum). The default :standard returns the mass-weighted mean particle mass, because a particle has no volume to make :mass extensive.
  • parttypes=[:stars] or [:dm] selects a family, and it sets the camera too, so parttypes=[:dm], direction=:faceon orients on the dark matter alone

Common variables: :mass, :age, :sd, :v, :birth, :metal, :id, :family, and off-axis :vlos/:σlos, :vcamx/:σcamx, :vcamy/:σcamy, :npart

Tutorial: Particle Projections, complete examples and usage

Quick Usage Examples

# Hydro data projections
hydro = gethydro(info, ...)
projection(hydro, :rho, :g_cm3)              # Density projection
projection(hydro, :sd, :Msol_pc2)            # Surface density
projection(hydro, [:T, :v], [:K, :km_s])     # Multi-variable

# Particle data projections  
particles = getparticles(info, ...)
projection(particles, :age, :Myr)            # Stellar age
projection(particles, :sd, :Msol_pc2)        # Stellar surface density
projection(particles, :mass, :Msol; mode=:sum)   # mass per pixel (the default is a MEAN)

General Projection Types

Both data types support:

  • Density projections: Surface density maps (:sd)
  • Mass-weighted projections: Intensive quantities with proper averaging
  • Velocity projections: Velocity fields and dispersion maps
  • Custom derived quantities: Temperature, pressure, kinematic analysis

Volume rendering

volumerender gives every cell a colour and an opacity from the value of one quantity and composites the line of sight front to back, so dense gas hides what is behind it. The opacity is set per unit length, so the picture does not change with the pixel size or the zoom. volumelook resolves the colours and the opacity once, volumeorbit turns the view around the object with that look fixed, and volumepath moves the camera between keyframes (angles, centre and zoom), both ready for a movie.

img = volumerender(gas, :rho; unit=:nH, colorrange=(1e-3, 1e2), colormap=:inferno,
                   opacity_gamma=2, tau_unit_length=[300, :pc],
                   inclination=70, axis=:angmom, center=[:bc], range_unit=:kpc, pxsize=[80, :pc])
save("galaxy.png", rotl90(img.image))

This is an illustration tool. For maps with physical units use projection.

Mera.volumerender — Function
volumerender(gas, quantity; look=nothing, supersample=:auto, <look keywords>, <view keywords>)
    -> VolumeRenderType

Volume rendering of one quantity: every cell gets a colour and an opacity from its value, and the line of sight is composited front to back, so dense gas in front hides what is behind it.

img = volumerender(gas, :rho; unit=:nH, colorrange=(1e-3, 1e2), colormap=:inferno,
                   tau_unit_length=[100, :pc], inclination=70, axis=:angmom,
                   center=[:bc], range_unit=:kpc, pxsize=[50, :pc])
save("galaxy.png", rotl90(img.image))

Opacity is defined per unit length (tau_unit_length), so the picture does not change when you change pxsize or zoom. Without it the strength is taken from the data.

A quantity that fills the whole volume, such as temperature, easily turns into fog: give it a long tau_unit_length (a few hundred pc or more), a narrow colorrange around what you want to see, and opacity_gamma > 1 so that only the top of the range becomes opaque.

iso=[1, 10, 100] puts a surface at each of those values: only gas near them is opaque, so you see nested shells instead of a cloud. With opacity=(0, 0) nothing else is drawn at all.

If the picture comes out dark, the gas is collecting little light along the ray. Any of these helps, in this order: raise exposure; shorten tau_unit_length so the gas is more opaque; narrow the colorrange onto the values you care about; lower opacity_gamma toward 1 so faint gas contributes; or raise the low end of opacity. Choose a colour map that is bright at its low end, such as :viridis or :cividis, rather than one that starts black.

Pass look= from volumelook to keep colours and opacity fixed across the frames of a movie; volumeorbit does that for a turning view.

:vlos, the velocity along the line of sight, works as a quantity even though it is not a getvar field: the renderer knows where the camera is. It is signed, so use scale=:linear, a symmetric colorrange and a diverging colour map such as :vik or :berlin. Positive means moving away from you, as in projection.

View keywords are those of the off-axis projection: direction, los, up, inclination, azimuth, axis, position_angle, angle_unit, xrange/yrange/zrange, center, range_unit, pxsize or res, mask, verbose, max_threads.

This is an illustration, not a measurement: the colours and opacity are choices. For physical maps, such as a surface density or an optical depth, use projection. Mera's hints about velocity frames and centres are silenced while a picture is made, since they are about physics rather than about drawing; call getvar yourself to see them.

Mera.volumelook — Function
volumelook(gas, quantity; unit=:standard, scale=:log, colorrange=nothing,
           percentiles=(1, 99.9), colormap=:inferno, opacity=(0.0, 1.0), opacity_gamma=1.0,
           iso=[], iso_width=0.05, iso_alpha=0.8, exposure=1.0, tone=:aces,
           tau_unit_length=nothing, tau_target=1.5, background=:black, <view keywords>)
    -> VolumeLook

Resolve how a quantity looks: the colour range, the colour map, and how opaque the gas is.

  • colorrange=(lo, hi) in the quantity's unit; without it the given percentiles of the cell values are used. With scale=:log the range is spaced logarithmically.
  • opacity=(a_lo, a_hi) are the ends of the opacity ramp across the colour range, and opacity_gamma bends it (> 1 keeps low values more transparent). Values below the range are invisible when a_lo = 0.
  • iso=[1.0, 10.0, 100.0] adds a surface at each of those values, in the quantity's unit: gas whose value is close to one of them becomes more opaque, so the shell stands out while everything else stays see-through. iso_width is the half width of a shell measured in the units of the colour range (so in dex when scale=:log), and iso_alpha is the opacity added at its centre. Both take either one number for all shells or one per shell; a surface sitting where the data is thin needs a wider band than one where it is dense. A wide outer shell hides the inner ones when they are equally opaque, so give the outer one a smaller value, for example iso_alpha=[0.15, 0.3, 0.6]. With opacity=(0, 0) you see the surfaces alone.
  • exposure brightens the finished picture, like the exposure of a camera. A thin, see-through rendering is dark simply because little light is collected; exposure=3 makes it three times brighter.
  • tone decides what happens to light brighter than white. :aces (default) and :reinhard bend it smoothly, so bright regions fade toward white and keep their detail, the way a camera or a film renderer behaves. :linear cuts it off instead, which holds colours at full saturation and flattens anything bright into one solid patch. Use :linear only when you want the raw numbers on screen.
  • tau_unit_length=[100, :pc] sets what full opacity means: optical depth 1 over that length. Without it the strength is taken from the data, so that the median optical depth of the map is tau_target.

The view keywords (direction, inclination, axis, center, range_unit, pxsize, res, xrange…) are only used for that automatic strength, so pass the ones you will render with.

Pass the result to volumerender as look= to keep every frame of a movie identical in colour and opacity.

Mera.volumeorbit — Function
volumeorbit(gas, quantity; angles=0:2:358, sweep=:azimuth, inclination=70, axis=:angmom,
            look=nothing, fov=nothing, fov_unit=:standard, aperture=:circle,
            center=[:bc], parallel_frames=false, <volumerender keywords>)
    -> Vector{VolumeRenderType}

Render a turning sequence with one fixed look and one fixed frame, ready for a movie.

Like rotation_sequence it selects a sphere around center, so every frame shares the same field of view and the object does not appear to zoom. The look is resolved once from the first angle (or taken from look=), so colours and opacity do not flicker.

frames = volumeorbit(gas, :rho; unit=:nH, angles=0:2:358, inclination=70,
                     center=[:bc], fov=20, fov_unit=:kpc, pxsize=[80, :pc])
mkpath("frames")
for (k, f) in enumerate(frames)
    save(joinpath("frames", "f" * lpad(k, 4, '0') * ".png"), rotl90(f.image))
end
moviefromframes("frames", "orbit.gif"; fps=25)

sweep is :azimuth, :inclination or :position_angle. parallel_frames=true renders frames concurrently with one thread each, which fills the cores when there are many frames and uses more memory.

Mera.volumepath — Function
volumepath(gas, quantity; keyframes, frames=120, res=512, fov_unit=:kpc, axis=:angmom,
           look=nothing, aperture=:square, smooth=true, loop=false,
           parallel_frames=false, <volumerender keywords>)
    -> Vector{VolumeRenderType}

Render a camera path: the view moves between keyframes and every frame comes out the same size, ready for a movie.

Each keyframe is a named tuple; anything you leave out keeps its previous value:

fieldmeaning
inclination, azimuth, position_anglethe view angles, in degrees, about axis
fovhalf the width of the picture, in fov_unit; smaller means closer
centerwhere the camera looks, [x, y, z] in fov_unit, or [:bc]

Angles and centres are interpolated in a straight line, the field of view geometrically (so a zoom runs at a constant rate), and smooth=true eases each segment in and out. loop=true returns to the first keyframe at the end, for a movie that repeats without a jump.

frames = volumepath(gas, :rho; unit=:nH, colorrange=(1e-3, 1e2), tau_unit_length=[300, :pc],
    keyframes = [(inclination=0,  azimuth=0,   fov=20),      # face on, wide
                 (inclination=80, azimuth=180, fov=20),      # tilt and turn
                 (inclination=80, azimuth=360, fov=4)],      # turn further and move in
    frames=180, res=512, fov_unit=:kpc)

mkpath("frames")
for (k, f) in enumerate(frames)
    save(joinpath("frames", "f" * lpad(k, 4, '0') * ".png"), rotl90(f.image))
end
moviefromframes("frames", "path.gif"; fps=30)

The camera is orthographic: it looks at the object from outside and cannot fly through it. For a camera inside the gas you need a perspective renderer.

For a plain turn around an object use volumeorbit, which needs no keyframes.

Mera.VolumeLook — Type
VolumeLook

How a quantity is turned into colour and opacity, resolved to numbers by volumelook: quantity, unit, scale, colorrange, colormap, the opacity ramp and its opacity_gamma, the sigma (per cm) that full opacity means, and the background colour.

Reuse one look for every frame of a movie, so the colours and the opacity do not change from frame to frame.

Mera.VolumeRenderType — Type
VolumeRenderType

The result of volumerender.

  • image: the picture, Matrix{RGB{Float64}} indexed [ix, iy] like a projection map. Save it with save("frame.png", rotl90(m.image)).
  • alpha: how much of each pixel is filled, 1 − transmission, between 0 and 1
  • value: the value of the quantity you actually see, averaged along the ray and weighted by what reaches you (NaN where nothing is visible)
  • look: the VolumeLook used, so the next frame can share it
  • extent, pixsize: code length, measured from the centre of the view (getextent)
  • los, up, cam_right, center: the camera; los points toward the observer
  • supersample, boxlen, scale, info

Drawing the AMR grid on a map

Overlay the cell boundaries of a refinement level on a finished projection, useful for showing where resolution changes relative to a structure. gridoverlay! draws into an existing axis; gridoverlay returns the segments so you can draw them yourself.

Mera.gridoverlay — Function
gridoverlay(dataobject; level=:max, direction=:z, center=[:boxcenter], range_unit=:standard,
            xrange=[missing,missing], yrange=[missing,missing], zrange=[missing,missing],
            unit=:standard) -> (segments, extent, level)

Cell-boundary line segments of the AMR cells at one refinement level, viewed along direction (:x/:y/:z), for overlaying the AMR structure on a map. level is :max (default, the finest), :min, or an integer. The cell edges are de-duplicated.

The result is the 2-D footprint of that level's cells (collapsed along direction), so it suits both a projection and a slice:

  • slice — pass a thin zrange (the slice plane) → the exact in-plane cell grid there;
  • projection — use the full column → where that level's cells project to along the line of sight.

(The two coincide when the refined region is a column-aligned box, and differ for irregular refinement.)

Off-axis views work too: pass the same view keywords as projection — los/up, inclination/azimuth (or theta/phi, position_angle), axis=:angmom, or direction=:faceon/:edgeon. Each cell centre is projected through the camera basis and drawn as a cell-sized square (an approximate indicator; the true tilted-cube silhouette is a hexagon). Off-axis overlays are not edge-de-duplicated, so on dense AMR pick a coarser level or a sub-region to keep the segment count manageable.

Returns a NamedTuple: segments (a Vector{NTuple{4,Float64}} of (x1,y1,x2,y2) in the plane coordinates and unit), extent [xmin,xmax,ymin,ymax], and the level used. Plot with linesegments! — or, after using Makie, the convenience gridoverlay!(ax, go).

p  = projection(gas, :sd)
go = gridoverlay(gas; level=:max)         # the finest-cell grid, where it exists
# overlay go.segments on the heatmap of p.maps[:sd]
Mera.gridoverlay! — Function
gridoverlay!(ax, go; color=(:white,0.3), linewidth=0.4)

Draw a gridoverlay result go onto a Makie axis ax (the AMR cell boundaries). Available after using Makie/CairoMakie.

Performance & Threading

Mera.benchmark_projection_hydro — Function
benchmark_projection_hydro(gas_data, thread_counts::Vector{Int}, n_runs::Int=10, output_file::String="") → Dict

Execute comprehensive AMR hydro projection benchmark with robust statistical analysis.

This function serves as the main coordinator for hydro projection performance testing. It performs AMR structure analysis, data quality validation, executes both single-variable and multi-variable projection benchmarks across specified thread counts, and exports results in multiple formats with comprehensive statistical analysis.

Benchmark Methodology

  • Single-Variable Test: Surface density projection (:sd → Msun/pc²)
  • Multi-Variable Test: 10 simultaneous variable projections: vars = [:v, :σ, :σx, :σy, :σz, :vrcylinder, :vϕcylinder, :σrcylinder, :σϕcylinder, :cs]
  • Statistical Robustness: several repetitions per configuration with coefficient of variation
  • Quality Control: Success rate monitoring (>80% threshold for reliable data)
  • Memory Profiling: Peak memory usage and garbage collection analysis

Threading Analysis

Evaluates performance across thread counts with derived metrics:

  • Speedup: Performance improvement vs single-threaded execution
  • Efficiency: Speedup per thread (percentage of ideal scaling)
  • Memory Scaling: Memory usage patterns across thread configurations

Output Files Generated

  • {output_file}.csv: Structured data for spreadsheet analysis and plotting
  • {output_file}.json: Machine-readable structured data for programmatic access
  • {output_file}_summary.txt: Human-readable performance report with insights

Arguments

  • gas_data: HydroDataType object from loaddata() or gethydrodata()
  • thread_counts::Vector{Int}: Thread counts to benchmark [1, 2, 4, 8, 16, ...]
  • n_runs::Int=10: Statistical repetitions per configuration (10 for robust analysis)
  • output_file::String="": Output filename base (auto-generated timestamp if empty)

Returns

Dictionary containing complete benchmark results with keys:

  • n_threads, test_type, mean_time, std_time, speedup, efficiency
  • mean_memory, success_rate, min_time, max_time, n_runs

Example Usage

# Load RAMSES hydro data
gas_data = loaddata(300, "/path/to/ramses/output/", :hydro)

# Run comprehensive benchmark (single + multi-variable)
results = benchmark_projection_hydro(gas_data, [1, 2, 4, 8, 16], 10, "performance_test")

# Results saved as:
# - performance_test.csv (for plotting with plot_results.jl)
# - performance_test.json (for programmatic analysis)  
# - performance_test_summary.txt (human-readable report)

Performance Insights

The benchmark automatically analyzes threading efficiency and provides guidance:

  • Identifies optimal thread counts for your system and data size
  • Detects threading bottlenecks and memory constraints
  • Quantifies single vs multi-variable projection performance differences
  • Provides statistical confidence intervals for all measurements

Integration Workflow

  1. Data Loading: Use Mera's loaddata() for your RAMSES simulation
  2. Benchmarking: Execute this function with desired thread counts
  3. Visualization: Use plot_results.jl to create performance dashboards
  4. Analysis: Review summary.txt for optimization recommendations
Mera.show_threading_info — Function
show_threading_info()

Display information about Julia threading configuration and recommendations.


Not sure which of Mera's map-making tools you want? Projections: which tool compares them and says when to reach for each.

@project projects several quantities in one line and binds each map to a name of its own. See Pipelines.

For complete function documentation: see the Complete API Reference.

Function Reference

Mera.projection — Function
projection()

Display an overview of variable symbols accepted by the projection interface for hydro and particle data as well as derived quantities. This zero-argument form is a helper to discover valid field names before calling one of the many method overloads such as:

projection(hydro::HydroDataType, :rho; direction=:z, res=256)
projection(particles::PartDataType, [:mass, :vz]; weighting=:mass)

Actual data projections are implemented in specialized method definitions located in projection_hydro.jl and projection_particles.jl (and gravity combo variants). Those methods accept keywords like direction, res, xrange, yrange, zrange, center, weighting, show_progress, and unit selection arguments. This summary call prints the canonical / alias variable names and returns nothing.

AMR Hydro Projection Functions

This module provides high-performance functionality for projecting AMR (Adaptive Mesh Refinement) hydrodynamic simulation data onto regular 2D grids. The projection engine handles multi-level AMR data with proper coordinate transformations, geometric mapping, and optimized parallel processing.

Architecture Overview

The projection system uses variable-based parallelization where each thread processes one variable across all AMR levels. This approach eliminates the costly combining phase that traditional chunked parallelization requires, resulting in significant performance improvements.

Cosmological runs

For a cosmological simulation, lengths, extents and (surface) densities here are in the proper (physical) frame at the snapshot's scale factor aexp (RAMSES unit_l/unit_d are proper). Convert to comoving with the proper_to_comoving_* helpers (× or ÷ powers of aexp). The cosmology-aware derived gas field :overdensity (= ρ/ρ̄_b − 1) can be projected like any other hydro variable.

Key Design Principles:

  • Thread Safety: No shared mutable state between threads
  • Memory Efficiency: Direct allocation without memory pools
  • Performance: Variable-based parallelization eliminates combining overhead
  • Conservation: Mass-preserving cell-to-pixel mapping
  • Flexibility: Support for multiple projection directions and coordinate systems

Core Functionality

Data Projection Features:

  • Multi-resolution mapping: Projects AMR cells from different refinement levels onto uniform grids
  • Variable projection: Supports density, surface density, velocity, pressure, temperature and derived quantities
  • Flexible grid sizing: Custom resolution, pixel size, or automatic sizing based on AMR levels
  • Spatial filtering: Range-based data selection in x, y, z dimensions with thin slice support
  • Weighting schemes: Mass weighting (default), volume weighting, or custom weighting functions
  • Direction control: Project along x, y, or z directions with proper coordinate remapping

AMR-Specific Features:

  • Conservative mapping: Mass-conserving cell-to-pixel mapping with geometric overlap calculations
  • Level-specific processing: Individual handling of each AMR refinement level for accuracy
  • Boundary handling: Robust treatment of cell boundaries and partial overlaps
  • Coordinate transformations: Automatic handling of different AMR coordinate systems

Main Projection Function

Create high-performance 2D projections of AMR hydro data with full control over resolution, spatial ranges, and processing options. This function automatically selects between sequential and variable-based parallel processing based on data characteristics.

Function Signature

projection(dataobject::HydroDataType, vars::Array{Symbol,1};
           units::Array{Symbol,1}=[:standard],
           lmax::Real=dataobject.lmax,
           res::Union{Real, Missing}=missing,
           pxsize::Array{<:Any,1}=[missing, missing],
           mask::Union{Vector{Bool}, MaskType}=[false],
           direction::Symbol=:z,
           los::Union{Array{<:Real,1}, Nothing}=nothing,
           up::Union{Array{<:Real,1}, Nothing}=nothing,
           theta::Union{Real, Nothing}=nothing,
           phi::Union{Real, Nothing}=nothing,
           inclination::Union{Real, Nothing}=nothing,
           azimuth::Union{Real, Nothing}=nothing,
           position_angle::Union{Real, Nothing}=nothing,
           axis::Union{Symbol, Array{<:Real,1}, Nothing}=nothing,
           angle_unit::Symbol=:deg,
           binning::Symbol=:overlap,
           nmax::Int=64,
           fov=nothing,
           fov_unit::Symbol=:standard,
           aperture::Symbol=:circle,
           depth=nothing,
           depth_unit::Symbol=:standard,
           angmom_radius=nothing,
           split::Bool=false,
           refine::Int=0,
           refine_to=nothing,
           weighting::Array{<:Any,1}=[:mass, missing],
           mode::Symbol=:standard,
           xrange::Array{<:Any,1}=[missing, missing],
           yrange::Array{<:Any,1}=[missing, missing],
           zrange::Array{<:Any,1}=[missing, missing],
           center::CenterType=[0., 0., 0.],
           range_unit::Symbol=:standard,
           data_center::CenterType=[missing, missing, missing],
           data_center_unit::Symbol=:standard,
           verbose::Bool=true,
           show_progress::Bool=true,
           verbose_threads::Bool=false,
           max_threads::Int=Threads.nthreads(),
           myargs::ArgumentsType=ArgumentsType())

return AMRMapsType

Arguments

Required Parameters:

  • dataobject::HydroDataType: AMR hydro simulation data loaded by Mera.jl
    • Must contain spatial coordinates and hydro variables
    • Supports RAMSES, ENZO, and other AMR formats
  • vars::Array{Symbol,1}: Variables to project (e.g., [:rho, :vx, :vy] or [:sd])
    • Multiple variables trigger automatic variable-based parallelization
    • Single variables use optimized sequential processing

Grid Resolution Control:

  • res::Union{Real, Missing}: pixel count per dimension across the whole box, so the pixel size is boxlen/res and a windowed projection returns only the pixels the window covers (a ±5 kpc window in a 100 kpc box at res=64 gives an 8×8 map, not 64×64). This is why res pairs with lmax (res = 2^lmax). Use pxsize=[size, unit] to set the pixel size directly and get a window-sized map.
    • Higher values increase precision but require more memory
    • Recommended: 256-1024 for most applications
  • lmax::Real: Use 2^lmax pixels when res not specified (default: dataobject.lmax)
    • Automatically matches finest AMR level resolution
  • pxsize::Array: Physical pixel size [value, unit] (overrides res/lmax)
    • Direct control over spatial resolution
    • The effective pixel is boxlen/ceil(boxlen/pxsize) — exactly the requested size only when it divides the box length (both the axis-aligned and off-axis paths share this)
    • Pixel COUNTS still differ between paths at the same pxsize: axis-aligned maps frame the requested window (or the object's stored ranges) on the box-anchored lattice, while off-axis maps auto-fit the rotated data's bounding box plus an AMR-aware border (one pixel + half the coarsest selected cell per side) — pin the frame explicitly if you need comparable map dimensions

Spatial Range Control:

  • xrange/yrange/zrange::Array: Spatial bounds [min, max] relative to center
    • Define the physical region to project (e.g., [-10, 10] for ±10 units)
    • Units controlled by range_unit parameter
  • center::Array: Projection center coordinates (use [:bc] for box center)
    • Can be physical coordinates or special values like [:bc], [:com]
  • range_unit::Symbol: Units for ranges/center (:kpc, :Mpc, :pc, :standard, etc.)
    • Ensures consistent spatial scaling across different simulations
  • direction::Symbol: Axis-aligned projection direction (:x, :y, :z)
    • Determines which spatial dimension is integrated over
    • Also accepts the disk presets :faceon / :edgeon (off-axis, see below)

Off-axis projection (arbitrary line of sight):

Give any of the following to project along an arbitrary line of sight instead of an axis. When none are given, the axis-aligned path above runs unchanged. Angles are in degrees by default (angle_unit=:rad to switch). There are five ways to say where the camera looks, and they are alternatives: give exactly one. Which to reach for:

  • an object you want face-on or inclined → inclination/azimuth with axis=:angmom

  • a box-aligned view → direction=:x/:y/:z

  • a direction you already hold as a vector, e.g. one frozen from an earlier snapshot so a time series keeps a fixed orientation → los=

  • spherical angles in the box frame, the usual physics convention → theta/phi. This is the same family as los=; theta/phi name the direction with two angles instead of three components, and unlike inclination/azimuth it is measured from the box axes rather than from a reference axis you choose.

  • inclination, azimuth (user-oriented): tilt the view. position_angle is not an alias of azimuth: azimuth turns the camera about the reference axis and changes the line of sight, while position_angle rolls the finished image about it and leaves m.los alone away from a reference axis by inclination (0°⇒down the axis, 90°⇒⟂ to it) and rotate around it by azimuth.

  • axis: reference axis for inclination/azimuth. Default :z (box vertical — assumes nothing about the contents, good for clouds/filaments/cosmic web). :angmom measures from the object's own angular momentum L (then 0°=face-on, 90°=edge-on); or give :x/:y/a 3-vector. NOTE: :angmom (and :faceon/:edgeon) are only a meaningful "disk normal" for a rotating disk, and L is computed about center — so center on the object (its centre of mass) for L to be the true spin; off-centre it is contaminated by bulk motion.

  • direction=:faceon/:edgeon: shortcuts for inclination=0/90 with axis=:angmom.

  • los::Vector: explicit line-of-sight (viewing) direction, e.g. los=[1,1,1] (need not be normalized)

  • theta, phi: spherical angles about the box axes; los=[sinθcosφ, sinθsinφ, cosθ].

  • up::Vector: optional camera up-vector. With inclination/azimuth the default keeps the reference axis upright. With los/theta/phi and no up, the hint is world +z, falling back to +y when the line of sight is itself the z axis, so a box-axis los reproduces the matching direction= preset.

  • angle_unit::Symbol: :deg (default) or :rad

  • binning::Symbol: how rotated cells are deposited onto the camera plane —

    • :overlap (default) — per-cell footprint supersampling (ns = ceil(cellsize/pixel) sub-points per cube axis, capped at nmax); AMR-aligned (no moiré, no interior holes), converges to :exact, and is usually faster than :exact. Cells coarser than the nmax cap (ns would exceed nmax) deposit each sub-cell as a footprint-sized top-hat so they still tile the camera plane without gaps — fine cells keep the sharp ±1px deposit.
    • :exact — analytic box-spline footprint: integrates the line-of-sight column (chord length through the cube) over each pixel exactly; no supersampling cap, the reference for fidelity.
    • :cic — fast preview, bilinear deposit of cell centres; speckles/moiré on coarse AMR cells
    • :ngp — fast preview, nearest-pixel deposit (sharp)
  • nmax::Int: :overlap supersampling cap (default 64) — max sub-points per cube axis. Raise for fewer artifacts on very coarse cells (slower, ∝ nmax³), lower for speed.

    All are mass-conserving. Off-axis supports the standard hydro/RT fields, :sd/:mass, the velocity dispersions (box frame and camera frame) and the :ncell counter. Only :r_cylinder, :r_sphere and :ϕ still need an axis direction: they describe the pixel's own geometry, and an off-axis pixel is a slanted column with no single value.

Data Processing Options:

  • weighting::Array: Variable for weighting [quantity, unit] (default: [:mass])
    • Controls how cell values are averaged: mass-weighted, volume-weighted, etc.
    • e.g. [:volume] for a volume-weighted average, or [:mass] (default)
  • mode::Symbol: Processing mode (:standard or :sum)
    • :standard → weighted averages (typical for intensive quantities)
    • :sum → accumulative totals (for extensive quantities like mass)
  • mask::Union{Vector{Bool}, MaskType}: Boolean mask to exclude cells
    • Filter out unwanted regions or apply custom selection criteria
  • units::Array{Symbol,1}: Output units for projected variables
    • Convert results to desired physical units automatically

Advanced Options:

  • data_center/data_center_unit: Alternative center for data calculations
    • When different from projection center (useful for coordinate transformations)
  • verbose::Bool: Print diagnostic information during processing (default: true)
    • Shows progress, memory usage, and basic threading information
  • show_progress::Bool: Display progress bar for level-by-level processing (default: true)
    • Visual feedback for long-running projections
  • verbose_threads::Bool: Show detailed multithreading diagnostics (default: false)
    • Enable for debugging parallel performance or thread behavior
  • myargs::ArgumentsType: Struct to pass multiple arguments simultaneously
    • Convenient for passing common parameter sets

Method Variants

The projection function supports multiple calling patterns for convenience:

# Single variable projection
projection(dataobject, :rho)                    # Density with default settings
projection(dataobject, :rho, unit=:g_cm3)      # Density in specific units

# Multiple variables with same units  
projection(dataobject, [:v, :vx, :vy], :km_s) # Multiple vars, single unit

# Multiple variables with different units
projection(dataobject, [:rho, :sd], [:g_cm3, :Msol_pc2]) # Different units per variable

# Surface density projection (special handling)
projection(dataobject, :sd, :Msol_pc2)         # Surface density in solar masses per pc²

Usage Examples

Basic Density Projection

# Simple density map of full simulation box (sequential processing)
density_map = projection(gas, :rho, unit=:g_cm3, res=512)

# High resolution central region with optimal settings
density_map = projection(gas, :rho, unit=:g_cm3, 
                        xrange=[-10, 10], yrange=[-10, 10], 
                        center=[:bc], range_unit=:kpc, res=1024)

Multi-Variable Analysis (Parallel Processing)

# Velocity field analysis (automatic variable-based parallelization)
velocity_maps = projection(gas, [:vx, :vy, :vz], unit=:km_s,
                          direction=:z, res=512)
# Output: 🧵 Using parallel processing with 3 threads (one per variable)

# Combined density and velocity (optimal parallel performance)
hydro_maps = projection(gas, [:rho, :vx, :vy], [:g_cm3, :km_s, :km_s],
                       xrange=[-5, 5], yrange=[-5, 5], 
                       center=[:bc], range_unit=:kpc)
# Output: ✅ Parallel projection completed successfully

Advanced AMR Projections

# High-precision thin slice (demonstrates AMR coordinate handling)
thin_slice = projection(gas, :sd, :Msol_pc2,
                       zrange=[0.49, 0.51], center=[:bc],
                       range_unit=:standard, direction=:z, res=1024)

# Volume-weighted projection for physical accuracy
volume_proj = projection(gas, :rho, 
                        weighting=[:volume, :cm3],
                        mode=:sum, res=512)

# Large multi-variable projection (optimal parallel performance)
comprehensive = projection(gas, [:rho, :vx, :vy, :vz, :cs], 
                          [:g_cm3, :km_s, :km_s, :km_s, :km_s],
                          res=2048, verbose_threads=true)
# Shows detailed threading diagnostics for performance analysis

Direction-Specific Projections

# X-direction projection (YZ plane) - parallel processing for multiple variables
x_proj = projection(gas, [:rho, :vx], [:g_cm3, :km_s],
                   direction=:x, yrange=[-10, 10], zrange=[-5, 5],
                   center=[:bc], range_unit=:kpc)

# Y-direction projection (XZ plane) - sequential processing for single variable
y_proj = projection(gas, :sd, :Msol_pc2,
                   direction=:y, xrange=[-20, 20], zrange=[-10, 10],
                   center=[:bc], range_unit=:kpc)

Threading Control and Performance Monitoring

# Basic thread information (always shown with verbose=true)
density_map = projection(gas, :rho, :g_cm3, res=512)
# Output: Available threads: 8
#         Requested max_threads: 8
#         Processing mode: Sequential (single variable)

# Detailed threading diagnostics for performance analysis
multi_var = projection(gas, [:rho, :vx, :vy, :vz], res=1024, 
                      verbose_threads=true)
# Output: Available threads: 8
#         Requested max_threads: 8
#         Processing mode: Variable-based parallel (4 threads)
#         🧵 Thread allocation: rho→T1, vx→T2, vy→T3, vz→T4
#         ✅ Parallel projection completed successfully
#         Performance: 2.1M cells/sec, Efficiency: 91.7%
#
# Note: verbose_threads=true shows detailed per-thread performance metrics

Hide all output with verbose=false

densitymap = projection(gas, :rho, :gcm3, res=512, verbose=false) # No threading output at all


#### Physical Pixel Size Control (pxsize)

julia

High-resolution projection with 10 pc pixels

highres = projection(gas, :rho, :gcm3, pxsize=[10., :pc], xrange=[-1, 1], yrange=[-1, 1], center=[:bc], range_unit=:kpc)

Ultra-high resolution with 1 pc pixels for detailed structure

ultrahigh = projection(gas, :sd, :Msolpc2, pxsize=[1., :pc], xrange=[-500, 500], yrange=[-500, 500], center=[:bc], range_unit=:pc)

Large-scale map with 100 pc pixels for overview

overview = projection(gas, [:rho, :temperature], [:gcm3, :K], pxsize=[100., :pc], xrange=[-10, 10], yrange=[-10, 10], center=[:bc], rangeunit=:kpc)

Custom units: 0.1 kpc (100 pc) pixels

customscale = projection(gas, :vx, :kms, pxsize=[0.1, :kpc], xrange=[-5, 5], yrange=[-5, 5], center=[:bc], range_unit=:kpc)

Very fine scale: sub-parsec resolution

finedetail = projection(gas, :density, :gcm3, pxsize=[0.1, :pc], xrange=[-10, 10], yrange=[-10, 10], center=[:bc], range_unit=:pc)


### Return Value

Returns `AMRMapsType` (alias: `HydroMapsType`) containing:
- **`.maps`**: Dictionary of projected variable maps (2D arrays)
- **`.extent`**: extent `[xmin, xmax, ymin, ymax]` in **code length** units. The map *values* may be
  physical (e.g. `:Msol_pc2`); the axes are NOT — multiply by `proj.scale.kpc`/`.pc`/… for a physical
  plotting extent, or use [`getextent`](@ref)`(proj, :kpc)`. `.cextent` is the same, centred on `.center`.
- **`.pixsize`**: pixel size in **code length** units (× `proj.scale.<unit>` for physical)
- **`.lmax_projected`**: Maximum AMR level included in projection
- **`.ranges`**: normalized `[0,1]` coordinate ranges (this is the fractional field, unlike `.extent`)
- **`.center`**: projection centre as a **box fraction** (0 to 1), not a length. Off-axis it
  records the resolved centre; on the axis-aligned path it comes back empty. A `slice`
  result's `.center` is a different quantity with the same name, in code units

### Radiative transfer (RT) projections

The same `projection` function accepts an `RtDataType` object (`rt = getrt(info)`)
and shares the AMR engine above. Two RT-specific behaviours apply:

- **Default weighting is `:volume`** (not `:mass`): RT fields carry no cell mass, so
  a mass weight is meaningless. Passing `weighting=[:mass]` is silently promoted to
  `[:volume]`. Override explicitly with e.g. `weighting=[:Np1]` to flux-weight by the
  photon density.
- **`mode=:standard`** (default) gives the **volume-weighted average** of the field
  along the line of sight (per pixel). **`mode=:sum`** gives the volume-weighted
  **sum** per pixel — i.e. Σ q·V_cell over the column, so for `:Np1` (a number
  density) it is proportional to the **total photon count** projected onto each
  pixel (the whole map sums to the box photon number), not the column density
  ∫q dz. For a mass-style **column density** use a `HydroDataType` with `:sd`
  (which divides by the pixel area); RT fields have no `:sd` analogue.

Typical RT maps:

julia rt = getrt(info) gas = gethydro(info)

Photon-count map of group 1 (volume-weighted sum per pixel)

npsum = projection(rt, :Np1, mode=:sum, center=[:bc], rangeunit=:kpc)

Reduced-flux map (beam vs. isotropic), volume-weighted average

fmap = projection(rt, :reducedflux1, center=[:bc])

Mock recombination-line emission map (∝ ∫ n_HII² dz) — a HYDRO quantity

em = projection(gas, :emrecomb, mode=:sum, center=[:bc], rangeunit=:kpc)

Ionization map xHII (hydro passive scalar located via the RT descriptor)

xmap = projection(gas, :xHII, center=[:bc])


RT photon fields and the hydro ionization state live on **separate** objects; project
each on its own object (analogous to gravity vs. hydro). Use `getvar(rt, …)` /
`getvar(gas, …)` for the per-cell quantities documented under `getvar`.

Off-axis projections:

!!! note "`xrange`/`yrange`/`zrange` are WORLD-space; `fov` is camera-space"
    The ranges select a **box in simulation coordinates**, and the off-axis frame is the bounding
    box of that region *after rotation*. Two consequences that surprise people:

    * omit `zrange` and the **full box depth** folds into the image height as you tilt — a
      ±22 kpc window came out ±45 kpc at i=30° and ±55 kpc at i=60° on a 100 kpc box;
    * the window's own **faces are visible** as straight edges across the map, because a sight
      line just outside the box clips only a corner of it.

    Neither is an error — it is what a world-space selection looks like from an angle — but if
    you want a *fixed camera-plane frame*, use `fov` instead:

    ```julia
    # world-space box: frame grows with tilt, window faces visible
    projection(gas, :sd; inclination=60, xrange=[-22,22], yrange=[-22,22], range_unit=:kpc)

    # camera-plane frame: identical at every angle (rotation-invariant sphere selection)
    projection(gas, :sd; inclination=60, fov=22, fov_unit=:kpc, aperture=:square)
    ```

    `aperture=:circle` (default) frames a sphere of radius `fov`, so the frame's corners are
    empty; `aperture=:square` selects radius `√2·fov` and crops to the ±`fov` square, giving a
    full rectangular frame that is **pixel-identical at every viewing angle** — what a gallery or
    an orbit sequence needs. Mera points this out once per session when it sees an off-axis view
    with a windowed `xrange`/`yrange` and no `zrange`.

    !!! note "`fov` integrates a chord, not a slab"
        Because the selection is a sphere of radius `R` (`fov`, or `√2·fov` for `:square`), a ray
        at in-plane distance `d` from the centre integrates `depth(d) = 2√(R² − d²)`. The column
        is deepest on axis and falls to **zero at the frame boundary** — for `:square` that is the
        four corners, which sit exactly on the sphere. On a 100 kpc box, `fov=15, :square` gives
        42.4 kpc of depth at the centre, 30.0 kpc at the middle of an edge and 0 at the corners.

        `depth` fixes this when it matters: it cuts along the **line of sight** itself, so the
        column integrates the same distance everywhere in the frame.

!!! note "Camera-space depth: `depth` / `depth_unit`"
    `depth=2, depth_unit=:kpc` keeps a slab of half-thickness 2 kpc about `center`, measured
    **along the viewing direction**. `depth=[-2, 6]` is an explicit window, so the foreground and
    the background need not match. Every other window (`xrange`/`yrange`/`zrange`) is world space
    and therefore does not follow the camera; this one does, which is what an off-axis column
    density needs if it is to mean a fixed depth.

    It applies to off-axis views only. On a box-axis view the line of sight IS a box axis, so
    `zrange` already cuts exactly this slab, and `depth` raises there and says so.

!!! note "Partial cells: `split`, `refine`, `refine_to`"
    Every off-axis cut keeps a cell **whole** if its centre is inside and drops it whole
    otherwise. `split=true` weights it instead by the fraction of its volume that really is
    inside the `depth` slab or the `fov` sphere, which is what makes stacked slabs re-sum and a
    small aperture hold the right mass.

    `split` fixes the total; it does not move the mass. A half-in cell still deposits its
    (reduced) share over its whole footprint, so the edge is still a staircase. `refine=N`
    octree-subdivides the cells the `fov` **sphere** crosses, localising the boundary to
    `cellsize/2^N` so the rendered edge sharpens; `refine_to=[length, unit]` subdivides until the
    boundary cells reach a target size instead. Both need `fov` and `split=true`, and both
    conserve: refining moves mass, it does not create any. Cost grows with the boundary area, at
    most `8^N` per boundary cell.

    For a region you select yourself, `subregion` takes the same `split`/`refine`/`refine_to`.

!!! note "A rectangular frame: `fov=[wx, wy]`"
    `fov` is normally one half-width and the frame is square. Two half-widths give a rectangle,
    which is what an edge-on disc wants: in a square frame most of the pixels are empty sky.
    Requires `aperture=:square`, because a rectangle has no single radius while `:circle` selects
    a disc, which has one. A scalar `fov` behaves exactly as before.

!!! note "Orienting on a region: `angmom_radius`"
    `axis=:angmom` and the `direction=:faceon`/`:edgeon` presets compute the orientation from the
    angular momentum of **the data that gets drawn**, about `center`: the object you pass, after
    `mask`, and inside the `fov` sphere when `fov` is given. `depth` and
    `xrange`/`yrange`/`zrange` do not narrow it. Load a whole box and the sum is
    dominated by the material with the longest lever arm, which is usually not the disc you meant.
    `angmom_radius=[10, :kpc]` restricts that sum to a sphere about `center`. For an axis from
    other data (all components, or a selection you do not want to draw), compute it with
    [`angmom`](@ref) and pass it as `axis=L`.

    The vector is per object: gas and stars each have their own, so `:faceon` on one is a
    different camera from `:faceon` on the other.

!!! note "How well sampled is a pixel: `:ncell` and `:npart`"
    Ask for `:ncell` (cells) or `:npart` (particles) alongside any other variable and the result
    carries a map of what fed each pixel, filled by the same deposit as the value. The meaning
    depends on the kernel: `:ngp` counts one per cell or particle, so the map sums to exactly how
    many there were; `:cic` counts one per pixel of its 2x2 stencil; `:overlap` counts one per
    sub-point and `:exact` one per pixel covered, so for those two it reads as sampling density
    rather than an object tally.

    The count is what separates a pixel you can quote from one built out of a single object: a
    pixel holding one particle has no spread, so its dispersion is exactly zero.
        Harmless for an object centred in frame, but make `fov` comfortably larger than any
        diffuse column, profile or scale height you intend to measure.

`binning` chooses how a rotated cell is shared among pixels. **The default `:overlap` is already
the accurate one** — reach for `:cic`/`:ngp` only when you want a fast preview. All four conserve
the total; they differ in *where* they put it:

| `binning` | | |
|---|---|---|
| `:overlap` | **default** | footprint supersampling — AMR-aligned, no moiré, converges to `:exact` |
| `:exact` | reference | analytic box-spline footprint; the fidelity yardstick |
| `:cic` | preview | bilinear deposit of the cell centre; speckles/moiré on coarse cells |
| `:ngp` | fastest | nearest-pixel deposit of the cell centre |

julia gas = gethydro(info)

Look along an arbitrary line of sight (accurate :overlap deposit, by default)

m = projection(gas, :sd, :Msolpc2, los=[1,1,1], center=[:bc], rangeunit=:kpc)

Explicitly ask for a fast preview instead

m = projection(gas, :sd, :Msol_pc2, los=[1,1,1], binning=:cic, center=[:bc])

Spherical angles instead of a vector (degrees)

m = projection(gas, :sd, theta=60, phi=30, angle_unit=:deg, center=[:bc])

Disk seen face-on / edge-on (line of sight from the gas angular momentum)

fo = projection(gas, :sd, direction=:faceon, center=[:bc], rangeunit=:kpc) eo = projection(gas, :sd, direction=:edgeon, center=[:bc], rangeunit=:kpc) ```

The off-axis camera basis is stored on the returned map (m.los, m.up, m.cam_right, m.center; m.direction == :offaxis). The cell→pixel deposit uses the standard nearest-grid-point / cloud-in-cell assignment scheme (Hockney & Eastwood 1988, Computer Simulation Using Particles); :overlap extends CIC with per-cell footprint supersampling. All deposits conserve the projected total to machine precision.

Project variables or derived quantities from the particle-dataset:

  • projection to a grid related to a given level

  • overview the list of predefined quantities with: projection()

  • select variable(s) and their unit(s)

  • limit to a maximum range

  • give the spatial center (with units) of the data within the box (relevant e.g. for radius dependency)

  • relate the coordinates to a direction (x,y,z) — or project along an arbitrary off-axis line of sight via los=[..], spherical angles theta/phi (angle_unit=:rad/:deg), or the disk presets direction=:faceon/:edgeon (line of sight from the particle angular momentum). The off-axis camera basis is stored on the returned map (.los, .up, .cam_right, .center; .direction==:offaxis).

  • thickness / thickness_unit, offset / offset_unit: project a slab rather than the full depth. A cutting plane through point particles is empty by construction (a particle has no extent), so the useful analogue of a slice is a projection of finite depth along the line of sight: thickness sets that depth and offset moves the slab, the same way offset moves the plane in offaxis_slice. Both default to range_unit. A non-positive thickness is refused rather than silently returning an empty map.

    Point particles have no footprint, so binning=:cic (default) / :ngp apply (:overlap and :exact fall back to :cic). See the hydro projection docstring for details.

  • select between mass (default), volume, SPH-kernel, or Voronoi (nearest-generator) weighting

  • pass a mask to exclude elements (cells/particles/...) from the calculation

  • toggle verbose mode

  • toggle progress bar

  • pass a struct with arguments (myargs)

projection(   dataobject::PartDataType, vars::Array{Symbol,1};
                units::Array{Symbol,1}=[:standard],
                lmax::Real=dataobject.lmax,
                res::Union{Real, Missing}=missing,
                pxsize::Array{<:Any,1}=[missing, missing],
                mask=[false],
                direction::Symbol=:z,
                los::Union{Array{<:Real,1}, Nothing}=nothing,
                up::Union{Array{<:Real,1}, Nothing}=nothing,
                theta::Union{Real, Nothing}=nothing,
                phi::Union{Real, Nothing}=nothing,
                inclination::Union{Real, Nothing}=nothing,
                azimuth::Union{Real, Nothing}=nothing,
                position_angle::Union{Real, Nothing}=nothing,
                axis::Union{Symbol, Array{<:Real,1}, Nothing}=nothing,
                angle_unit::Symbol=:deg,
                binning::Symbol=:cic,
                fov=nothing,
                fov_unit::Symbol=:standard,
                aperture::Symbol=:circle,
                parttypes::Array{Symbol,1}=[:all],
                thickness=nothing,
                thickness_unit::Symbol=:standard,
                offset=nothing,
                offset_unit::Symbol=:standard,
                angmom_radius=nothing,
                max_threads::Int=Threads.nthreads(),
                weighting::Symbol=:mass,
                mode::Symbol=:standard,
                angmom_radius=nothing,
                xrange::Array{<:Any,1}=[missing, missing],
                yrange::Array{<:Any,1}=[missing, missing],
                zrange::Array{<:Any,1}=[missing, missing],
                center::CenterType=[0., 0., 0.],
                range_unit::Symbol=:standard,
                data_center::CenterType=[missing, missing, missing],
                data_center_unit::Symbol=:standard,
                ref_time::Real=dataobject.info.time,
                verbose::Bool=true,
                show_progress::Bool=true,
                myargs::ArgumentsType=ArgumentsType()  )

return PartMapsType

Arguments

Required:

  • dataobject: needs to be of type: "PartDataType"
  • var(s): select a variable from the database or a predefined quantity (see field: info, function projection(), dataobject.data)

Predefined/Optional Keywords:

  • unit(s): return the variable in given units
  • pxsize`: creates maps with the given pixel size in physical/code units (dominates over: res, lmax) : pxsize=[physical size (Number), physical unit (Symbol)]
  • res: pixel count per dimension across the whole box (so the pixel size is boxlen/res); if not given, lmax selects it as 2^lmax. A windowed projection therefore returns only the pixels the window covers — use pxsize=[size, unit] for a window-sized map.
  • lmax: create maps with 2^lmax pixels for each dimension
  • xrange: the range between [xmin, xmax] in units given by argument range_unit and relative to the given center; zero length for xmin=xmax=0. is converted to maximum possible length
  • yrange: the range between [ymin, ymax] in units given by argument range_unit and relative to the given center; zero length for ymin=ymax=0. is converted to maximum possible length
  • zrange: the range between [zmin, zmax] in units given by argument range_unit and relative to the given center; zero length for zmin=zmax=0. is converted to maximum possible length
  • range_unit: the units of the given ranges: :standard (code units), :Mpc, :kpc, :pc, :mpc, :ly, :au , :km, :cm (of typye Symbol) ..etc. ; see for defined length-scales viewfields(info.scale)
  • center: in units given by argument range_unit; by default [0., 0., 0.]; the box-center can be selected by e.g. [:bc], [:boxcenter], [value, :bc, :bc], etc..
  • weighting: select between :mass weighting (default), :volume weighting, or :sph (smear each cell over an M4 kernel sized from its :volume; mass-conserving; needs a :volume column), or :voronoi (nearest-generator: sample each LOS through the nearest cell — sharp, genuinely moving-mesh; intensive maps exact, surface density approximate)
  • mode: :standard (default) returns a weighted mean; :sum returns the per-pixel total. This is how you get a real mass map from points: projection(part, :mass, :Msol; mode=:sum) sums to the mass of everything projected. The default is a mean because a particle has no volume to make :mass extensive, so the default map is the mass-weighted mean particle mass, useful as a mass-spectrum and equal-mass check but not a budget.
  • angmom_radius: restrict the orientation sum behind direction=:faceon/:edgeon and axis=:angmom to a sphere about center, e.g. [10, :kpc]. Without it the sum runs over everything loaded, where the outer material has the longest lever arm. mask and parttypes also narrow it, so parttypes=[:dm], direction=:faceon orients on the dark matter alone. angmom returns the vector itself if you want to compute it once and reuse it.
  • nlos: number of samples along each line of sight, :voronoi only. By default Mera steps at the scale of the cells — min(pixsize, ½·median(V)^⅓), capped at 4096 — because stepping at pixel scale walks over whole cells whenever cells are smaller than a pixel. Set it only to trade accuracy for speed, or to check convergence; verbose=true reports the value used.
  • :sph totals depend on how tightly the frame crops the data. Each cell is smeared over an M4 kernel and only the part landing on in-frame pixels is deposited — the wings that fall outside are dropped, which is physical, not a bug. So the same data in a tighter frame keeps less mass: on one test field ∫Σ dA / M was 1.000000 at half-widths 0.50/0.40/0.33 of the box, 0.9934 at 0.31 and 0.9604 at 0.30, as the frame started clipping the kernel. This is also why an axis-aligned and an off-axis :sph map of the same data can differ by ~1 %: the two routes frame differently (the off-axis extent is derived from the rotated data, not from your window). Compare them with los=[0,0,1], up=[0,1,0], which makes the off-axis camera reproduce the axis-aligned geometry — the disagreement then drops to ~0.01 %, and :mass agrees exactly. Leave margin around the data if you want the total to be frame-independent.
  • :voronoi on a cutout does not lose mass, even though ∫Σ dA looks short of msum. The two count different things: msum adds the whole mass of every cell whose generator lies in the region, including the part of that cell sticking out through the boundary, while the map integrates only what is actually inside. The shortfall is therefore a surface-to-volume effect and falls off as 1/L — measured on an AREPO zoom at 4.2 %, 3.4 %, 1.8 %, 1.0 % for half-widths of 200, 400, 800, 1600 ckpc/h. Refining nlos does not remove it (it converges to the same value), because it is not a sampling error. :mass does not show it only because point deposition dumps each cell's entire mass at its generator. To integrate a sub-volume exactly, select a region larger than the one you measure.
  • data_center: to calculate the data relative to the datacenter; in units given by argument `datacenterunit`; by default the argument datacenter = center ;
  • data_center_unit: :standard (code units), :Mpc, :kpc, :pc, :mpc, :ly, :au , :km, :cm (of typye Symbol) ..etc. ; see for defined length-scales viewfields(info.scale)
  • direction: axis-aligned :x, :y, :z, or the disk presets :faceon/:edgeon
  • off-axis view (any line of sight): inclination/azimuth (+ axis=:z/:angmom/vector), los=[lx,ly,lz], or theta/phi; position_angle rolls the image; angle_unit=:deg (default) or :rad. See the hydro projection docstring for the full description; for point particles binning=:overlap falls back to :cic.
  • fov / fov_unit / aperture: camera-plane framing, identical to the hydro path — fov is the frame half-width and selects a sphere about center (radius fov, or √2·fov for aperture=:square, which crops to a full rectangle that is pixel-identical at every viewing angle). Use it instead of xrange/yrange whenever frames must be comparable across angles or snapshots, and give fov_unit explicitly — it defaults to :standard, a box fraction. Note the sphere means a summed quantity integrates a chord that shrinks to zero at the frame boundary; see the hydro projection docstring.
  • mask: needs to be of type MaskType which is a supertype of Array{Bool,1} or BitArray{1} with the length of the database (rows)
  • ref_time: the age quantity relative to a given time (code_units); default relative to the loaded snapshot time
  • show_progress: print progress bar on screen
  • myargs: pass a struct of ArgumentsType to pass several arguments at once and to overwrite default values of lmax, xrange, yrange, zrange, center, rangeunit, verbose, showprogress

Defined Methods - function defined for different arguments

  • projection( dataobject::PartDataType, var::Symbol; ...) # one given variable
  • projection( dataobject::PartDataType, var::Symbol, unit::Symbol; ...) # one given variable with its unit
  • projection( dataobject::PartDataType, vars::Array{Symbol,1}; ...) # several given variables -> array needed
  • projection( dataobject::PartDataType, vars::Array{Symbol,1}, units::Array{Symbol,1}; ...) # several given variables and their corresponding units -> both arrays
  • projection( dataobject::PartDataType, vars::Array{Symbol,1}, unit::Symbol; ...) # several given variables that have the same unit -> array for the variables and a single Symbol for the unit

Examples

...

Mera.project — Function
project(data, var [, unit]; res=auto, kwargs...)                      # already-loaded hydro/particles
project(info::InfoType, var [, unit]; vars=auto, lmax=…, kwargs...)   # load hydro, then project
project(path::AbstractString, output::Integer, var [, unit]; kwargs...)  # getinfo + load + project

One-call projection. A high-level convenience that loads the data (when given an InfoType or a path+output) and projects in a single call — the ergonomic equivalent of yt.ProjectionPlot(ds, "z", field).

  • Smart resolution — if res is not given it defaults to 2^lmax capped at 1024, so a deep AMR run doesn't silently allocate an enormous map (pass res= to override; a note is printed when capped).
  • Loading — reads the full hydro state by default (fast); restrict with vars=[:rho] if you know exactly what the projection (and its weighting / view) needs.
  • All other keywords (direction, los, center, range_unit, weight, mode, pxsize, …) are forwarded to projection; the return value is the same map object.