Off-axis Projection & LOS API Reference
Docstrings for the off-axis projection and line-of-sight tools. The narrative guide is in Off-axis Projection; the pipeline (camera basis, deposit kernels, kinematics) is described in the docstrings below; off-axis views are selected through the same projection call documented in the Projections API.
Line-of-sight maps
slice is the cutting-plane function and the name the documentation uses: with axis-aligned keywords it returns the covering-grid cut, and with any off-axis view keyword (los/inclination/azimuth/…) it returns the camera-plane cut along that line of sight. offaxis_slice is an alias of it, kept so existing scripts keep working.
Mera.slice — Function
slice(obj, var, [unit]; ...) -> CoveringGridResult (axis-aligned) | NamedTuple (off-axis)A single, non-integrated cutting plane through AMR cell data (HydroDataType, GravDataType, RtDataType). One name, two modes, chosen automatically from the keywords:
Axis-aligned (default). slice_axis=:z, slice_pos=0.5, slice_unit=:standard, lmax=obj.lmax, center=[0.,0.,0.], xrange, yrange, zrange, range_unit=:standard, max_bytes=4e9, pos_unit=:standard, verbose=true. A single-cell-thick cut at slice_pos along slice_axis (:x/:y/:z), resampled to a uniform level-lmax buffer (cf. covering_grid for the 3-D version, projection for the integrated map). slice_pos is in slice_unit (:standard ⇒ a fraction of the box). Returns a CoveringGridResult whose grid[var] is a 2-D array.
Off-axis (cutting plane along any line of sight). Triggered by passing any off-axis view keyword — los/inclination/azimuth/axis/theta/phi/direction=:faceon/:edgeon/position_angle/up, or the output controls res/pxsize. The field is sampled on the camera plane through center for an arbitrary orientation (the same view keywords as projection), but as a nearest-cell sample, not an integral — resolution-dependent and not mass-conserving. Returns a NamedTuple with .map, .extent and the camera basis. Empty (NaN) pixels are expected where the plane carries no cell; pass xrange/yrange to fill the frame, or use projection for a conserved map. One variable at a time.
Empty (NaN) pixels are expected in off-axis mode, for two distinct reasons. (1) Without xrange/yrange the frame is the axis-aligned bounding box of the rotated view, and the plane∩box polygon cannot fill that rectangle — the corners and border are NaN. Pass a window inside the box (xrange=…, yrange=…) and the frame fills (0 % empty on a uniform grid). (2) At fine pxsize over coarse AMR cells, nearest-cell sampling leaves sub-percent pixel-scale gaps at refinement boundaries. For a gap-free, mass-conserving map use projection.
offaxis_slice is an alias of this function for the off-axis mode; slice is the name the documentation uses.
sl = slice(gas, :rho, :nH; slice_axis=:z, slice_pos=0.5) # axis-aligned mid-plane n_H map
sl[:rho] # 2-D array
oa = slice(gas, :rho, :nH; inclination=60, axis=:angmom, # off-axis cutting plane
xrange=[-16,16], yrange=[-16,16], range_unit=:kpc, pxsize=[0.3,:kpc])
oa.map # 2-D camera-plane arrayMera.offaxis_slice — Function
offaxis_slice(dataobject, var [, unit]; <view & range kwargs>, res=256, pxsize=nothing)Alias of slice — kept so existing scripts keep working, and for when you want the off-axis intent spelled out at the call site. slice(obj, var; los=…/inclination=…/…) dispatches here automatically whenever an off-axis view keyword is given, so the two are interchangeable and return the same NamedTuple.
Prefer slice: it is the one name for a cutting plane, axis-aligned or off-axis, and it is what the documentation uses. See slice for the full description, the view keywords, and why empty (NaN) pixels are expected.
Sequences, storage & export
Mera.rotation_sequence — Function
rotation_sequence(dataobject, var, [unit]; sweep=:azimuth, angles,
axis=:angmom, inclination=0, fov=nothing, fov_unit=:standard,
aperture=:circle, parallel_frames=false, center=[:bc], res=256, <projection kwargs>)
-> Vector{AMRMapsType}Render var from a sequence of viewing angles for an orbit movie, all sharing ONE truly fixed field of view so successive frames do not jitter or zoom. sweep selects which angle varies (:azimuth, :inclination, or :position_angle) and angles is the list of values (degrees by default).
Because the off-axis camera is orthographic (parallel rays, observer at infinity), the only control over what is in frame is the FOV, not a camera distance. The FOV must be rotation- invariant or the frame would breathe with angle, so a sphere of radius fov is selected about center. Omit fov (fov=nothing) to auto-fit the galaxy: the mass-enclosed 99% radius (so the frame fits the object rather than chasing the few sparse outermost cells / a diffuse halo), capped so the selection stays inside the box. The aperture chooses how the sphere is framed:
aperture=:circle(default) — the sphere shows as a circular aperture; the rectangular frame's corners (beyond radiusfov) are empty.aperture=:square— a slightly larger sphere (radius√2·fov, enclosing the±fovsquare at every angle) is selected and each frame cropped to that square → a full rectangular frame with no circular aperture and no data dropped inside it.
Threading. By default each frame's projection multithreads internally and the frames run sequentially. With parallel_frames=true the frames run concurrently (Threads.@threads) and each projection is single-threaded — this fills all cores when there are ≳ nthreads() frames and is typically ~1.5–2× faster for an orbit movie (it runs that many projections at once, so it uses proportionally more transient memory; results are identical to round-off).
Returns a Vector of map objects — one per angle — ready to montage or animate.
Mera.savemap — Function
savemap(p::DataMapsType, filename; verbose=true) -> String
loadmap(filename; verbose=true) -> DataMapsTypeSave / load a projection result (an AMRMapsType/PartMapsType from projection) to a JLD2 file — a lightweight, Julia-native persistence format (the .jld2 extension is added if missing). The whole object round-trips: every map and its unit, the extent/pixsize, the off-axis camera basis, and the simulation info — so a reloaded map still plots, re-projects, and carries provenance.
p = projection(gas, [:sd, :vx])
savemap(p, "maps.jld2")
p2 = loadmap("maps.jld2") # AMRMapsType, identical to pThe file uses the HDF5 container but stores the Julia object and is LZ4-compressed by default, so h5py cannot reconstruct the map from it. Reload with Mera; to hand a map to Python, write the array out yourself or use export_vtk. Older HDF5 readers.
Mera.loadmap — Function
loadmap(filename; verbose=true) -> DataMapsTypeLoad a projection result saved with savemap from a JLD2 file (the .jld2 extension is added if missing). The whole AMRMapsType/PartMapsType round-trips — maps, units, geometry, camera basis, and info — ready to plot, re-project, or carry provenance.
Save a projection result the Julia-native, JLD2 way:
p = projection(gas, [:sd, :vx])
savemap(p, "maps.jld2") # all maps + units + geometry + provenance
p2 = loadmap("maps.jld2") # → AMRMapsType, ready to plot/re-projectJLD2 files use the HDF5 container, but they store the Julia object rather than plain arrays and are LZ4-compressed by default, so h5py cannot reconstruct a map from one. Reload with Mera; to hand a map to Python, write the array out yourself or use export_vtk.
Camera kinematics (internal helpers)
These are not exported but underlie every off-axis call; documented for reference.
Mera.build_camera_basis — Function
build_camera_basis(los, up=nothing; roll=0.0) -> (right, up, w)Construct a right-handed orthonormal camera basis from a line-of-sight vector los (the viewing direction) and an optional up hint.
Returns three unit 3-vectors (right, up, w) where w = los/‖los‖ is the viewing direction, and right, up span the image plane (image x = right, image y = up). The basis is right-handed with right × up = w.
If up is nothing — or (anti)parallel to los — a deterministic auto-up is chosen (the world axis least parallel to los), so the result is fully reproducible.
roll (radians) rotates the image plane about the line of sight — i.e. it sets the orientation of the image on the "sky" (the astronomical position angle / camera roll). It leaves w unchanged and rotates (right, up) together, so it composes with any way of choosing los.
Convention check: los=[0,0,1], up=[0,1,0] ⇒ right=[1,0,0], up=[0,1,0], matching the axis-aligned direction=:z mapping (image x→sim x, image y→sim y).
Mera.resolve_los — Function
resolve_los(; los, theta, phi, inclination, azimuth, axis,
direction=:z, angle_unit=:deg, up=nothing, L=nothing) -> (los_vec, up_hint)Resolve a user-facing view specification into a (los_vec, up_hint) pair. Give exactly one of the alternatives below (a second one raises an error — no silent precedence). All angles are in angle_unit (:deg by default, or :rad):
- explicit
los3-vector, inclination/azimuth— tilt the view away from a referenceaxisbyinclination(0 ⇒ looking straight down the axis, 90° ⇒ perpendicular to it) and rotate around the axis byazimuth.axisdefaults to the box:z; use:x/:y/:z, a 3-vector, or:angmom(the object's angular momentumL, for disks). The reference axis is kept pointing "up".- spherical angles
(theta, phi)about the box axes (los=[sinθcosφ, sinθsinφ, cosθ]), - preset
direction::x/:y/:z,:faceon(look alongL),:edgeon(⟂L, up =L̂).
:faceon/:edgeon and axis=:angmom need the pre-computed L; the projection wiring supplies it via getvar(obj,[:lx,:ly,:lz]). The image roll (position_angle) is applied separately in build_camera_basis, so it is not a line-of-sight specifier here. Pure — touches no data.
Deposit kernels (internal)
The three engines behind binning=:cic/:ngp, :overlap and :exact — how a rotated AMR cell becomes pixel values. The trade-offs are demonstrated visually in Off-axis Projection.
Mera.deposit_rotated_cells_to_grid! — Function
deposit_rotated_cells_to_grid!(grid, weight_grid, x_cam, y_cam, values, weights, extent, res; kwargs...)Centre-only deposit kernel for off-axis projections (binning=:cic/:ngp): each rotated cell centre lands on its nearest pixel (:ngp) or is spread bilinearly over the 4 surrounding pixels (:cic). Fast preview quality — cells wider than a pixel leave speckle/moiré; the footprint-aware kernels below fix that. Exactly weight-conserving. Internal; see the "Off-axis Projection: How It Works Internally" docs page.
Mera.deposit_rotated_cells_overlap! — Function
deposit_rotated_cells_overlap!(grid, weight_grid, x_cam, y_cam, cellsize, values, weights, cam_right, cam_up, extent, res; nmax=64, max_threads=...)Footprint deposit kernel for off-axis projections (binning=:overlap, the default): each cube is split into ns³ sub-points (ns = ⌈cellsize/pixel⌉, capped at nmax), every sub-point rotated into the camera frame and CIC-deposited with weight 1/ns³; cells coarser than the cap deposit each sub-cube as a footprint-sized top-hat, so the camera plane tiles without holes at any viewing angle. Converges to the :exact kernel. Per-cell contributions sum to exactly 1 — the kernel is weight-conserving by construction. Threaded over contiguous cell chunks into per-thread grids (summed at the end). Internal; see the "Off-axis Projection: How It Works Internally" docs page.
Mera.deposit_rotated_cells_exact! — Function
deposit_rotated_cells_exact!(grid, weight_grid, x_cam, y_cam, cellsize, values, weights, cam_right, cam_up, cam_w, extent, res; max_threads=...)Analytic deposit kernel for off-axis projections (binning=:exact): for every pixel a cell covers, the line-of-sight chord length through the rotated cube is integrated over the pixel area (polygon clipping against the six cube faces), i.e. the true column integral — the fidelity reference the other kernels are compared against. Sub-pixel cells fall back to a CIC 4-pixel stencil. Weight-conserving; threaded like :overlap. Internal; see the "Off-axis Projection: How It Works Internally" docs page.