Geodesic integration strategies

Geodesics are integrated using a second-order ODE integration scheme, described below. Everything in Gradus.jl ends up calling tracegeodesics at some point to perform the integration. This function can be called directly, and the solution objects processes as a GeodesicPoint.

For example, many calculations with geodesics involve determining a Jacobian term

\[J = \left\lvert \frac{\partial (\theta, \phi) }{\partial (\alpha, \beta)} \right\rvert,\]

which tracks how the local angles on the sky of the emitter ($\theta, \phi$) change as the impact parameters on the image plane of the observer change ($\alpha, \beta$). Using automatic-differentiation, this is a simple calculation to perform, making use of Gradus.velocity_to_sky_angles:

using Gradus

function impact_to_sky_angles(m, x_obs, alpha, beta)
    d = ThinDisc(0.0, Inf)
    # map some impact parameters to a velocity vector
    v = map_impact_parameters(m, x_obs, alpha, beta)
    # perform the trace and unpack the GeodesicPoint
    gp = tracegeodesics(m, x_obs, v, d, 2x_obs[2]) |> unpack_solution
    # obtain the local sky angles
    theta, phi =  Gradus.velocity_to_sky_angles(m, gp.x, SVector(1.0, 0.0, 0.0 ,0.0), gp.v)
end


m = KerrMetric(1.0, 0.998)
x_obs = SVector{4}(0.0, 1e4, deg2rad(1), 0.0)

theta, phi = impact_to_sky_angles(m, x_obs, 5.0, 3.0)
(0.4146613361612117, -2.259714516283244)

To obtain the Jacobian term, we can instead use ForwardDiff.jl to track the derivatives for us. We have to do a little bit of type massaging, as ForwardDiff.jl expects everything to be of a vector type:

using ForwardDiff

J = ForwardDiff.jacobian(
    x -> SVector{2}(impact_to_sky_angles(m, x_obs, x...)...),
    SVector(5.0, 3.0)
)

J
2×2 StaticArraysCore.SMatrix{2, 2, Float64, 4} with indices SOneTo(2)×SOneTo(2):
 -0.0801882  -0.0494749
  0.125903   -0.129589

Utility functions

Gradus.velocity_to_sky_anglesFunction
velocity_to_sky_angles(
    m::AbstractMetric,
    x_source::SVector{4},
    v_source::SVector{4},
    v::SVector{4},
)

Map the velocity vector v to angles on the local sky $(\theta, \phi)$ of a source at position x_source moving with velocity v_source. Returns a tuple with (θ, ϕ).

See sky_angles_to_velocity for the inverse operation.

source
Gradus.sky_angles_to_velocityFunction
sky_angles_to_velocity(
    m::AbstractMetric,
    x_source::SVector{4},
    v_source::SVector{4},
    θ,
    ϕ
)

Convert angles on the local sky $(\theta, \phi)$ at some point x_source to a velocity vector. Also applies the Lorentz transformation due to the velocity v_source of x_source. Returns a four-velocity vector (SVector{4}) that is unconstrained. Use constrain_all or similar to normlise the four-velocity.

See velocity_to_sky_angles for the inverse operation.

source

Second-Order

The motivation behind the second-order methods is to permit the computation of geodesics in generic spacetimes, via the geodesic equation:

Gradus.compute_geodesic_equationFunction
compute_geodesic_equation(ginv, j1, j2, v)

Using the inverse metric ginv, the Jacobian of the metric for $r$ and $\theta$, j1 and j2 respectively, and velocity four-vector v, calculates the four-acceleration via the geodesic equation.

Returns the components of $\frac{\text{d}^2 u^\mu}{\text{d} \lambda^2}$ via

\[\frac{\text{d}^2 u^\mu}{\text{d} \lambda^2} + \Gamma^{\mu}_{\phantom{\mu}\nu\sigma} \frac{\text{d}u^\nu}{\text{d} \lambda} \frac{\text{d}u^\sigma}{\text{d} \lambda} = 0,\]

where $x^\mu$ is a position four-vector, $\Gamma^{\mu}_{\phantom{\mu}\nu\sigma}$ are the Christoffel symbols of the second kind, and $\lambda$ the affine parameter describing the curve.

The Christoffel symbols $\Gamma^{\mu}_{\phantom{\mu}\nu\sigma}$ are defined

\[\Gamma^{\mu}_{\phantom{\mu}\nu\sigma} = \frac{1}{2} g^{\mu\rho} \left( \partial_{\nu}g_{\rho \sigma} + \partial_{\sigma}g_{\rho \nu} - \partial_{\rho}g_{\sigma \nu} \right).\]

Limitations:

  • currenly pre-supposes static, axis-symmetric metric.
source

The above can be solved as a second-order ODE, subject to an initial position and initial velocity

\[u^\mu = \left(t, r, \theta, \phi \right), \quad \text{and} \quad \dot{u}^\mu = \left( \dot{t}, \dot{r}, \dot{\theta}, \dot{\phi} \right),\]

where the dot refers to the derivative with respect to $\lambda$. In general, the spatial components of the initial velocity are known a priori, and the time-component is determined via the constraint:

Gradus.constrain_timeFunction
constrain_time(g_comp, v, μ = 0.0, positive::Bool = true)

Constrains the time component of the four-velocity v, given metric components g_comp and effective mass μ.

\[g_{\sigma\nu} \dot{u}^\sigma \dot{u}^\nu = -\mu^2,\]

for $v^t$. The argument positive allows the sign of $\mu$ to be changed. true corresponds to time-like geodesics, false to space-like.

This function should rarely be directly called, and instead is invoked by constrain.

Limitations:

  • currenly pre-supposes static, axis-symmetric metric.
source

Methods

Gradus.TransferFunctionMethodType
TransferFunctionMethod()

Computing the underlying relativistic effects using Cunningham transfer functions. This method has a number of limitations related to the assumptions:

  • The disc must be axis-symmetric.
  • The accretion disc must have a velocity structure that depends only on radius.
  • The domain of integration must be in or above the equatorial plane (no false images).

The transfer function approach is often faster and converges to higher numerical accuracy than the other methods. They can also be pre-computed for use in spectral models for fitting.

source
Gradus.BinningMethodType
BinningMethod()

Compute the underlying relativistic effects by 'binning' the observer's plane into a number of regions, and tracing a single geodesic for each photon. Conceptually, this is like tracing a single photon for each pixel, however the regions need not be equi-rectangular, and may take on other shapes, see AbstractImagePlane.

This method is slow and computationally expensive, but has the benefit that it makes no assumptions about the model being computed.

It is internally used for testing conceptually more complex integration methods.

source

Image planes

Gradus.PolarPlaneType
function PolarPlane(
    grid::Abstract2DGrid;
    Nr = 400,
    Nθ = 100,
    r_min = 1.0,
    r_max = 250.0,
    θ_min = 0.0,
    θ_max = 2π,
)

Divide the image plane into a polar grid centered at $\alpha = 0$ and $\beta = 0$.

source
Gradus.CartesianPlaneType
function CartesianPlane(
    grid::Abstract2DGrid;
    Nx = 150,
    Ny = 150,
    x_min = 0.0,
    x_max = 150.0,
    y_min = 0.0,
    y_max = 150.0,
)

Represent the image plane as equi-rectangular regions.

source
Gradus.image_planeFunction
image_plane(plane::AbstractImagePlane)
image_plane(plane::AbstractImagePlane, x::SVector{4})

Return two vectors, representing the $\alpha$ and $\beta$ impact parameters that parameterise the image plane. Each pair of $\alpha$ and $\beta$ must correspond to an unnormalized_areas.

source
Gradus.trajectory_countFunction
trajectory_count(plane::AbstractImagePlane)

Return an integer that counts how many unique geodesics need to be calculated to map the plane. This should be equal to length(first(image_plane(plane))), and is used to pre-allocate buffers.

source
Gradus.unnormalized_areasFunction
unnormalized_areas(plane::AbstractImagePlane)

Return a vector where each element is the area (number) of a given geodesic element on the image plane. For a pixel image plane, each area will be a constant 1. These are used to weight the contributions of each region when calculating observational results.

source