|
| 1 | +# Adaptive tracing |
| 2 | + |
| 3 | +Gradus.jl has a variety of skies and image planes that it supports, which act as a method for representing the view of a particular observer in the system. The difference in definition that Gradus uses is that a _sky_ represents either a full sphere (or hemisphere) of possible trajectories, whereas a _image plane_ is a subset of the sphere that projects onto a two-dimensional plane. Image planes are only really used in the context of calculating observational signatures from the perspective of telescopes, whereas skies are used as intermediary representations to calculate e.g. illumination patterns or visibilities. |
| 4 | + |
| 5 | +To integrate over a sky, each angle pair ``(\theta, \phi)`` is used to calculate a direction vector, which is transformed by the local basis into an initial velocity vector that is sensitive to the motion of the emitter. These trajectories can be heavily distorted due to relativistic beaming if the emitter is moving at relativistic speeds. |
| 6 | + |
| 7 | +One of the main problem with integrating over a sky in any kind of numerically meaningful way is that the angles, if sampled uniformly over ``\theta \sim \mathcal{U}(0, \pi)`` and ``\phi \sim \mathcal{U}(0, 2\pi)``, then the majority of trajectories will be scrunched near the poles. To avoid this issue, one can instead sample ``\cos \theta \sim \mathcal{U}(-1, 1)``, which acts to impose the ``\sin \theta`` probability density term. |
| 8 | + |
| 9 | +This however still has the problem that often very few regions of the sky actually represent something interesting. For example, say we were calculating the illumination pattern of the disc for an emitter close the black hole -- we do not care about those trajectories that fall into the black hole or escape to infinity, only those that hit the disc. However, a hand-wavey 50% of the sky will be either the black hole or infinity, so it is wasting precious CPU cycles calculating those trajectories when we know they are not relevant to our result. |
| 10 | + |
| 11 | +This is where _adaptive tracing_ is useful. It traces an initial coarse grid of _pilot geodesics_ that can be used to work out where information of interest is in the sky. |
| 12 | + |
| 13 | +To illustrate this, we'll use a pre-defined adaptive sky implementation, but at the end of these instructions will be a section on [Custom refinement criteria](@ref). |
| 14 | + |
| 15 | +```julia |
| 16 | +using Gradus, Makie, CairoMakie |
| 17 | + |
| 18 | +m = KerrMetric(1.0, 0.998) |
| 19 | +# the ring-corona can also be used to represent any off-axis point |
| 20 | +corona = RingCorona(; r = 10.0, h = 4.0) |
| 21 | +d = ThinDisc(0.0, Inf) |
| 22 | + |
| 23 | +sky = AdaptiveSky(m, corona, d) |
| 24 | +``` |
| 25 | + |
| 26 | +The [`AdaptiveSky`](@ref) uses a grid of 3x3 cells, where each cell can be |
| 27 | +continuously refined to smaller 3x3 cells. It refers to each refinement as a `level`, where `level == 1` are the top 9 cells. |
| 28 | + |
| 29 | +We need to prime the refined grid, so we trace three levels, corresponding to ``9^3 = 729`` trajectories. |
| 30 | + |
| 31 | +```@docs |
| 32 | +Gradus.trace_initial! |
| 33 | +``` |
| 34 | + |
| 35 | +```julia |
| 36 | +# trace an initial grid of points |
| 37 | +trace_initial!(sky) |
| 38 | +``` |
| 39 | + |
| 40 | +The printout may record a greater number of values than you expect. This is |
| 41 | +because it is recording the number of values it has stored, and some cells |
| 42 | +double up in the implementation (though they are not traced, their memory is |
| 43 | +duplicated currently). |
| 44 | + |
| 45 | +We can visualise the sky using one of the unpacking functions |
| 46 | + |
| 47 | +```@docs |
| 48 | +Gradus.unpack_sky |
| 49 | +``` |
| 50 | + |
| 51 | +The `V` type of the sky we have constructed is [`CoronaGridValues`](@ref). |
| 52 | + |
| 53 | +```julia |
| 54 | +function plot_sky(sky; kwargs...) |
| 55 | + phi, cos_theta, values = unpack_sky(sky) |
| 56 | + theta = acos.(cos_theta) |
| 57 | + # get the radius that each geodesic hit |
| 58 | + Rs = [v.r for v in values] |
| 59 | + |
| 60 | + fig, ax, _ = scatter(phi, theta; color = log10.(Rs), kwargs...) |
| 61 | + ax.xlabel = "ϕ" |
| 62 | + ax.ylabel = "θ" |
| 63 | + fig |
| 64 | +end |
| 65 | + |
| 66 | +plot_sky(sky) |
| 67 | +``` |
| 68 | + |
| 69 | + |
| 70 | +Not all of the points have been drawn, since those that either fell into the black hole or went to inifinity had their radius set to `NaN`. This is a feature of the particular [`AdaptiveSky`](@ref) we have used, but you can handle this however you like in your own implementation. |
| 71 | + |
| 72 | +Lets say we are now interested in the edges of the disc, and want to refine those points. We can define a _refine_ condition that is given pairs of neighbouring points and asks whether the cells they are in need to be refined. |
| 73 | + |
| 74 | +In our case, if one or the other is `NaN` (but not both), we want to refine those cells. We'll implement the full function, but there are utilities to help as well like [`Gradus.refine_function`](@ref). |
| 75 | +```julia |
| 76 | +function refine_edges(sky::AdaptiveSky, i1::Int, i2::Int) |
| 77 | + v1 = sky.values[i1] |
| 78 | + v2 = sky.values[i2] |
| 79 | + if isnan(v1.r) && isnan(v2.r) |
| 80 | + false # do not refine |
| 81 | + else |
| 82 | + isnan(v1.r) || isnan(v2.r) |
| 83 | + end |
| 84 | +end |
| 85 | + |
| 86 | +trace_step!(sky; check_refine = refine_edges, verbose = true) |
| 87 | +``` |
| 88 | + |
| 89 | +Plotting the sky again |
| 90 | + |
| 91 | +```julia |
| 92 | +plot_sky(sky; markersize = 6) |
| 93 | +``` |
| 94 | + |
| 95 | +!!! note |
| 96 | + |
| 97 | + There is currently a bug where the bottom row of cells are always refined |
| 98 | + when checking `NaN`. This will be fixed in future versions. |
| 99 | + |
| 100 | + |
| 101 | + |
| 102 | +We can run this step a couple more times: |
| 103 | + |
| 104 | +```julia |
| 105 | +for i in 1:2 |
| 106 | + trace_step!(sky; check_refine = refine_edges, verbose = true) |
| 107 | +end |
| 108 | + |
| 109 | +plot_sky(sky; markersize = 2) |
| 110 | +``` |
| 111 | + |
| 112 | + |
| 113 | + |
| 114 | +Cool, now let's inpaint the rest using an interpolation scheme: |
| 115 | + |
| 116 | +```@docs |
| 117 | +Gradus.fill_sky_values |
| 118 | +``` |
| 119 | + |
| 120 | +!!! warning |
| 121 | + |
| 122 | + The interpolation scheme used is very poor at the moment, and effectively does column-wise interpolation, without considering other surrounding neighbours. This will also be fixed in future versions. |
| 123 | + |
| 124 | +```julia |
| 125 | +phi, theta, grid = fill_sky_values(sky, 1080) |
| 126 | +Rs = [v.r for v in grid] |
| 127 | + |
| 128 | +fig, ax, _ = heatmap(phi, theta, log10.(Rs)') |
| 129 | +ax.xlabel = "ϕ" |
| 130 | +ax.ylabel = "θ" |
| 131 | +fig |
| 132 | +``` |
| 133 | + |
| 134 | + |
| 135 | + |
| 136 | +What if we had a refinemenet criteria based on the relative values of neighbours? |
| 137 | + |
| 138 | +```julia |
| 139 | +function refine_distant(sky::AdaptiveSky, i1::Int, i2::Int) |
| 140 | + v1 = sky.values[i1] |
| 141 | + v2 = sky.values[i2] |
| 142 | + if isnan(v1.r) && isnan(v2.r) |
| 143 | + false # do not refine |
| 144 | + else |
| 145 | + # refine if they are more than 10% different |
| 146 | + !isapprox(v1.r, v2.r, rtol = 0.1) |
| 147 | + end |
| 148 | +end |
| 149 | + |
| 150 | +# create a fresh sky |
| 151 | +sky = AdaptiveSky(m, corona, d) |
| 152 | +trace_initial!(sky) |
| 153 | +for i in 1:3 |
| 154 | + trace_step!(sky; check_refine = refine_distant, verbose = true) |
| 155 | +end |
| 156 | + |
| 157 | +plot_sky(sky; markersize = 2) |
| 158 | +``` |
| 159 | + |
| 160 | + |
| 161 | + |
| 162 | +Now our inpainting should look more convincing |
| 163 | + |
| 164 | +```julia |
| 165 | +phi, theta, grid = fill_sky_values(sky, 1080) |
| 166 | +Rs = [v.r for v in grid] |
| 167 | + |
| 168 | +fig, ax, _ = heatmap(phi, theta, log10.(Rs)') |
| 169 | +ax.xlabel = "ϕ" |
| 170 | +ax.ylabel = "θ" |
| 171 | +fig |
| 172 | +``` |
| 173 | + |
| 174 | + |
| 175 | + |
| 176 | +## Custom adaptive refinement |
| 177 | + |
| 178 | +```@docs |
| 179 | +Gradus.AdaptiveSky |
| 180 | +Gradus.trace_step! |
| 181 | +Gradus.refine_function |
| 182 | +Gradus.fine_refine_function |
| 183 | +Gradus.Grids.AdaptiveCell |
| 184 | +Gradus.Grids.AdaptiveGrid |
| 185 | +``` |
0 commit comments