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.sliceFunction
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 array
Mera.offaxis_sliceFunction
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_sequenceFunction
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 radius fov) are empty.
  • aperture=:square — a slightly larger sphere (radius √2·fov, enclosing the ±fov square 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.savemapFunction
savemap(p::DataMapsType, filename; verbose=true) -> String
loadmap(filename; verbose=true) -> DataMapsType

Save / 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 p

The 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.loadmapFunction
loadmap(filename; verbose=true) -> DataMapsType

Load 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-project

JLD2 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_basisFunction
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_losFunction
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):

  1. explicit los 3-vector,
  2. inclination/azimuth — tilt the view away from a reference axis by inclination (0 ⇒ looking straight down the axis, 90° ⇒ perpendicular to it) and rotate around the axis by azimuth. axis defaults to the box :z; use :x/:y/:z, a 3-vector, or :angmom (the object's angular momentum L, for disks). The reference axis is kept pointing "up".
  3. spherical angles (theta, phi) about the box axes (los=[sinθcosφ, sinθsinφ, cosθ]),
  4. preset direction: :x/:y/:z, :faceon (look along L), :edgeon (⟂ L, up = ).

: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.