/** * # API Reference * * This package based on this paper: [Gorski (2005)](http://iopscience.iop.org/article/10.1086/427976/pdf). * * The key things to understand the implementation are: * - Spherical coordinates in different representations such as `(alpha, delta)` * or `(theta, phi)` or `(X, Y, z)` are always normalised to `(z, a)`. * - The HEALPix spherical projection is used to map to `(t, u)` (see `za2tu` and `tu2za`). * See Section 4.4 and Figure 5 in the paper, where `(t, u)` is called `(x_s, y_s)`. * * - A simple affine transformation is used to map to `(f, x, y)` (see `tu2fxy` and `fxy2tu`), * where `f = {0 .. 11}` is the base pixel index and `(x, y)` is the position * within the base pixel in the (north-east, north-west) direction * and `(0, 0)` in the south corner. * - From `(f, x, y)`, the HEALPix pixel index in the "nested" scheme * is related via `fxy2nest` and `nest2fxy`, and in the "ring" scheme * via `fxy2ring` and `ring2fxy` in a relatively simple equations. * * To summarise: there are two geometrical transformations: * `(z, a)` <-> `(t, u)` is the HEALPix spherical projection, * and `(t, u)` <-> `(f, x, y)` is a 45 deg rotation and scaling for each * of the 12 base pixels, so that HEALPix pixels in `(x, y)` are unit squares, * and pixel index compuatations are relatively straightforward, * both in the "nested" and "ring" pixelisation scheme. * * ## Notations * *
* theta : colatitude (pi/2 - delta) [0 , pi]
* phi : longitude (alpha) [0, 2 pi)
* t : coord. of x-axis in spherical projection [0, 2 pi)
* u : coord. of y-axis in spherical projection [-pi/2, pi/2]
* z : cos(theta) [-1, 1]
* X : sin(theta) * cos(phi) [-1, 1]
* Y : sin(theta) * sin(phi) [-1, 1]
* a : phi [0, 2 pi)
* f : base pixel index {0 .. 11}
* x : north-east index in base pixel [0, nside)
* y : north-west index in base pixel [0, nside)
* p : north-east axis in base pixel [0, 1)
* q : north-west axis in base pixel [0, 1)
* j : pixel-in-ring index polar cap: {1 .. 4 i}
* equatorial belt: {1 .. 4 nside}
* i : ring index {1 .. 4 nside - 1}
*
*/
/**
* 3D Vector
*/
export type V3 = [number, number, number]
export function order2nside(order: number) {
return 1 << order
}
export function nside2order(nside: number) {
return ilog2(nside)
}
export function nside2npix(nside: number) {
return 12 * nside * nside
}
export function vec2pix_nest(nside: number, v: V3) {
const { z, a } = vec2za(v[0], v[1], v[2])
return za2pix_nest(nside, z, a)
}
export function vec2pix_ring(nside: number, v: V3) {
const { z, a } = vec2za(v[0], v[1], v[2])
return nest2ring(nside, za2pix_nest(nside, z, a))
}
export function ang2pix_nest(nside: number, theta: number, phi: number) {
const z = Math.cos(theta)
return za2pix_nest(nside, z, phi)
}
export function ang2pix_ring(nside: number, theta: number, phi: number) {
const z = Math.cos(theta)
return nest2ring(nside, za2pix_nest(nside, z, phi))
}
export function nest2ring(nside: number, ipix: number) {
const { f, x, y } = nest2fxy(nside, ipix)
return fxy2ring(nside, f, x, y)
}
export function ring2nest(nside: number, ipix: number) {
if (nside == 1) {
return ipix
}
const { f, x, y } = ring2fxy(nside, ipix)
return fxy2nest(nside, f, x, y)
}
export function ring2fxy(nside: number, ipix: number) {
const polar_lim = 2 * nside * (nside - 1)
if (ipix < polar_lim) { // north polar cap
const i = Math.floor((Math.sqrt(1 + 2 * ipix) + 1) / 2)
const j = ipix - 2 * i * (i - 1)
const f = Math.floor(j / i)
const k = j % i
const x = nside - i + k
const y = nside - 1 - k
return { f, x, y }
}
if (ipix < polar_lim + 8 * nside * nside) { // equatorial belt
const k = ipix - polar_lim
const ring = 4 * nside
const i = nside - Math.floor(k / ring)
const s = i % 2 == 0 ? 1 : 0
const j = 2 * (k % ring) + s
const jj = j - 4 * nside
const ii = i + 5 * nside - 1
const pp = (ii + jj) / 2
const qq = (ii - jj) / 2
const PP = Math.floor(pp / nside)
const QQ = Math.floor(qq / nside)
const V = 5 - (PP + QQ)
const H = PP - QQ + 4
const f = 4 * V + (H >> 1) % 4
const x = pp % nside
const y = qq % nside
return { f, x, y }
}
else { // south polar cap
const p = 12 * nside * nside - ipix - 1
const i = Math.floor((Math.sqrt(1 + 2 * p) + 1) / 2)
const j = p - 2 * i * (i - 1)
const f = 11 - Math.floor(j / i)
const k = j % i
const x = i - k - 1
const y = k
return { f, x, y }
}
}
export function pix2vec_nest(nside: number, ipix: number) {
const { f, x, y } = nest2fxy(nside, ipix)
const { t, u } = fxy2tu(nside, f, x, y)
const { z, a } = tu2za(t, u)
return za2vec(z, a)
}
export function pix2ang_nest(nside: number, ipix: number) {
const { f, x, y } = nest2fxy(nside, ipix)
const { t, u } = fxy2tu(nside, f, x, y)
const { z, a } = tu2za(t, u)
return { theta: Math.acos(z), phi: a }
}
export function pix2vec_ring(nside: number, ipix: number) {
return pix2vec_nest(nside, ring2nest(nside, ipix))
}
export function pix2ang_ring(nside: number, ipix: number) {
return pix2ang_nest(nside, ring2nest(nside, ipix))
}
// TODO: cleanup
export function query_disc_inclusive_nest(nside: number, v: V3, radius: number, cb: (ipix: number) => void) {
if (radius > PI_2) {
throw new Error(`query_disc: radius must < PI/2`)
}
const pixrad = max_pixrad(nside)
const d = PI_4 / nside
const { z: z0, a: a0 } = vec2za(v[0], v[1], v[2]) // z0 = cos(theta)
const sin_t = Math.sqrt(1 - z0 * z0)
const cos_r = Math.cos(radius) // r := radius
const sin_r = Math.sin(radius)
const z1 = z0 * cos_r + sin_t * sin_r // cos(theta - r)
const z2 = z0 * cos_r - sin_t * sin_r // cos(theta + r)
const u1 = za2tu(z1, 0).u
const u2 = za2tu(z2, 0).u
const cover_north_pole = sin_t * cos_r - z0 * sin_r < 0 // sin(theta - r) < 0
const cover_south_pole = sin_t * cos_r + z0 * sin_r < 0 // sin(theta - r) < 0
let i1 = Math.floor((PI_2 - u1) / d)
let i2 = Math.floor((PI_2 - u2) / d + 1)
if (cover_north_pole) {
++i1
for (let i = 1; i <= i1; ++i)
walk_ring(nside, i, cb)
++i1
}
if (i1 == 0) {
walk_ring(nside, 1, cb)
i1 = 2
}
if (cover_south_pole) {
--i2
for (let i = i2; i <= 4 * nside - 1; ++i)
walk_ring(nside, i, cb)
--i2
}
if (i2 == 4 * nside) {
walk_ring(nside, 4 * nside - 1, cb)
i2 = 4 * nside - 2
}
const theta = Math.acos(z0)
for (let i = i1; i <= i2; ++i)
walk_ring_around(nside, i, a0, theta, radius + pixrad, ipix => {
if (angle(pix2vec_nest(nside, ipix), v) <= radius + pixrad)
cb(ipix)
})
}
export function query_disc_inclusive_ring(nside: number, v: V3, radius: number, cb_ring: (ipix: number) => void) {
return query_disc_inclusive_nest(nside, v, radius, ipix => {
cb_ring(nest2ring(nside, ipix))
})
}
export function max_pixrad(nside: number) {
const unit = PI_4 / nside
return angle(
tu2vec(unit, nside * unit),
tu2vec(unit, (nside + 1) * unit),
)
}
function angle(a: V3, b: V3) {
return 2 * Math.asin(Math.sqrt(distance2(a, b)) / 2)
}
function tu2vec(t: number, u: number): V3 {
const { z, a } = tu2za(t, u)
return za2vec(z, a)
}
function distance2(a: V3, b: V3) {
const dx = a[0] - b[0]
const dy = a[1] - b[1]
const dz = a[2] - b[2]
return dx * dx + dy * dy + dz * dz
}
export type FXY = { f: number, x: number, y: number }
function walk_ring_around(nside: number, i: number, a0: number, theta: number, r: number, cb: (ipix: number) => void) {
if (theta < r || theta + r > PI)
return walk_ring(nside, i, cb)
const u = PI_4 * (2 - i / nside)
const z = tu2za(PI_4, u).z
const st = Math.sin(theta)
const ct = Math.cos(theta)
const sr = Math.sin(r)
const cr = Math.cos(r)
const w = Math.atan2(
Math.sqrt(-square(z - ct * cr) / (square(st) * sr * sr) + 1) * sr,
(-z * ct + cr) / st
)
if (w >= PI)
return walk_ring(nside, i, cb)
const t1 = center_t(nside, i, za2tu(z, wrap(a0 - w, PI2)).t)
const t2 = center_t(nside, i, za2tu(z, wrap(a0 + w, PI2)).t)
const begin = tu2fxy(nside, t1, u)
const end = right_next_pixel(nside, tu2fxy(nside, t2, u))
for (let s = begin; !fxy_compare(s, end); s = right_next_pixel(nside, s)) {
cb(fxy2nest(nside, s.f, s.x, s.y))
}
}
function center_t(nside: number, i: number, t: number) {
const d = PI_4 / nside
t /= d
t = (((t + i % 2) >> 1) << 1) + 1 - i % 2
t *= d
return t
}
function walk_ring(nside: number, i: number, cb: (ipix: number) => void) {
const u = PI_4 * (2 - i / nside)
const t = PI_4 * (1 + (1 - i % 2) / nside)
const begin = tu2fxy(nside, t, u)
let s = begin
do {
cb(fxy2nest(nside, s.f, s.x, s.y))
s = right_next_pixel(nside, s)
} while (!fxy_compare(s, begin))
}
function fxy_compare(a: FXY, b: FXY) {
return a.x == b.x && a.y == b.y && a.f == b.f
}
function right_next_pixel(nside: number, { f, x, y }: FXY) {
++x
if (x == nside) {
switch (Math.floor(f / 4)) {
case 0:
f = (f + 1) % 4
x = y
y = nside
break
case 1:
f = f - 4
x = 0
break
case 2:
f = 4 + (f + 1) % 4
x = 0
break
}
}
--y
if (y == -1) {
switch (Math.floor(f / 4)) {
case 0:
f = 4 + (f + 1) % 4
y = nside - 1
break
case 1:
f = f + 4
y = nside - 1
break
case 2: {
f = 8 + (f + 1) % 4
y = x - 1
x = 0
break
}
}
}
return { f, x, y }
}
export function corners_nest(nside: number, ipix: number) {
const { f, x, y } = nest2fxy(nside, ipix)
const { t, u } = fxy2tu(nside, f, x, y)
const d = PI_4 / nside
const xyzs: V3[] = []
for (const [tt, uu] of [
[0, d],
[-d, 0],
[0, -d],
[d, 0],
]) {
const { z, a } = tu2za(t + tt, u + uu)
xyzs.push(za2vec(z, a))
}
return xyzs
}
export function corners_ring(nside: number, ipix: number) {
return corners_nest(nside, ring2nest(nside, ipix))
}
// pixel area
export function nside2pixarea(nside: number) {
return PI / (3 * nside * nside)
}
// average pixel size
export function nside2resol(nside: number) {
return Math.sqrt(PI / 3) / nside
}
export function pixcoord2vec_nest(nside: number, ipix: number, ne: number, nw: number) {
const { f, x, y } = nest2fxy(nside, ipix)
const { t, u } = fxy2tu(nside, f, x, y)
const d = PI_4 / nside
const { z, a } = tu2za(t + d * (ne - nw), u + d * (ne + nw - 1))
return za2vec(z, a)
}
export function pixcoord2vec_ring(nside: number, ipix: number, ne: number, nw: number) {
return pixcoord2vec_nest(nside, ring2nest(nside, ipix), ne, nw)
}
function za2pix_nest(nside: number, z: number, a: number) {
const { t, u } = za2tu(z, a)
const { f, x, y } = tu2fxy(nside, t, u)
return fxy2nest(nside, f, x, y)
}
export function tu2fxy(nside: number, t: number, u: number) {
const { f, p, q } = tu2fpq(t, u)
const x = clip(Math.floor(nside * p), 0, nside - 1)
const y = clip(Math.floor(nside * q), 0, nside - 1)
return { f, x, y }
}
function wrap(A: number, B: number) {
return A < 0 ? B - (-A % B) : A % B
}
const PI2 = 2 * Math.PI
const PI = Math.PI
const PI_2 = Math.PI / 2
const PI_4 = Math.PI / 4
const PI_8 = Math.PI / 8
function sigma(z: number): number {
if (z < 0)
return -sigma(-z)
else
return 2 - Math.sqrt(3 * (1 - z))
}
/**
* HEALPix spherical projection.
*/
export function za2tu(z: number, a: number) {
if (Math.abs(z) <= 2. / 3.) { // equatorial belt
const t = a
const u = 3 * PI_8 * z
return { t, u }
}
else { // polar caps
const p_t = a % (PI_2)
const sigma_z = sigma(z)
const t = a - (Math.abs(sigma_z) - 1) * (p_t - PI_4)
const u = PI_4 * sigma_z
return { t, u }
}
}
/**
* Inverse HEALPix spherical projection.
*/
export function tu2za(t: number, u: number) {
const abs_u = Math.abs(u)
if (abs_u >= PI_2) { // error
return { z: sign(u), a: 0 }
}
if (abs_u <= Math.PI / 4) { // equatorial belt
const z = 8 / (3 * PI) * u
const a = t
return { z, a }
}
else { // polar caps
const t_t = t % (Math.PI / 2)
const a = t - (abs_u - PI_4) / (abs_u - PI_2) * (t_t - PI_4)
const z = sign(u) * (1 - 1 / 3 * square(2 - 4 * abs_u / PI))
return { z, a }
}
}
// (x, y, z) -> (z = cos(theta), phi)
function vec2za(X: number, Y: number, z: number) {
const r2 = X * X + Y * Y
if (r2 == 0)
return { z: z < 0 ? -1 : 1, a: 0 }
else {
const a = (Math.atan2(Y, X) + PI2) % PI2
z /= Math.sqrt(z * z + r2)
return { z, a }
}
}
// (z = cos(theta), phi) -> (x, y, z)
function za2vec(z: number, a: number): V3 {
const sin_theta = Math.sqrt(1 - z * z)
const X = sin_theta * Math.cos(a)
const Y = sin_theta * Math.sin(a)
return [X, Y, z]
}
export function ang2vec(theta: number, phi: number) {
const z = Math.cos(theta)
return za2vec(z, phi)
}
export function vec2ang(v: V3) {
const { z, a } = vec2za(v[0], v[1], v[2])
return { theta: Math.acos(z), phi: a }
}
// spherical projection -> f, p, q
// f: base pixel index
// p: coord in north east axis of base pixel
// q: coord in north west axis of base pixel
function tu2fpq(t: number, u: number) {
t /= PI_4
u /= PI_4
t = wrap(t, 8)
t += -4
u += 5
const pp = clip((u + t) / 2, 0, 5)
const PP = Math.floor(pp)
const qq = clip((u - t) / 2, 3 - PP, 6 - PP)
const QQ = Math.floor(qq)
const V = 5 - (PP + QQ)
if (V < 0) { // clip
return { f: 0, p: 1, q: 1 }
}
const H = PP - QQ + 4
const f = 4 * V + (H >> 1) % 4
const p = pp % 1
const q = qq % 1
return { f, p, q }
}
// f, p, q -> nest index
export function fxy2nest(nside: number, f: number, x: number, y: number) {
return f * nside * nside + bit_combine(x, y)
}
// x = (...x2 x1 x0)_2 <- in binary
// y = (...y2 y1 y0)_2
// p = (...y2 x2 y1 x1 y0 x0)_2
// returns p
export function bit_combine(x: number, y: number) {
assert(x < (1 << 16))
assert(y < (1 << 15))
return (
// (python)
// n = 14
// ' | '.join(['x & 1'] + [f'(x & 0x{2 ** (i+1):x} | y & 0x{2 ** i:x}) << {i + 1}' for i in range(n)] + [f'y & 0x{2**n:x} << {n+1}'])
x & 1 | (x & 0x2 | y & 0x1) << 1 | (x & 0x4 | y & 0x2) << 2 |
(x & 0x8 | y & 0x4) << 3 | (x & 0x10 | y & 0x8) << 4 | (x & 0x20 | y & 0x10) << 5 |
(x & 0x40 | y & 0x20) << 6 | (x & 0x80 | y & 0x40) << 7 | (x & 0x100 | y & 0x80) << 8 |
(x & 0x200 | y & 0x100) << 9 | (x & 0x400 | y & 0x200) << 10 | (x & 0x800 | y & 0x400) << 11 |
(x & 0x1000 | y & 0x800) << 12 | (x & 0x2000 | y & 0x1000) << 13 | (x & 0x4000 | y & 0x2000) << 14 |
(x & 0x8000 | y & 0x4000) << 15 | y & 0x8000 << 16
)
}
// x = (...x2 x1 x0)_2 <- in binary
// y = (...y2 y1 y0)_2
// p = (...y2 x2 y1 x1 y0 x0)_2
// returns x, y
export function bit_decombine(p: number) {
assert(p <= 0x7fffffff)
// (python)
// ' | '.join(f'(p & 0x{2**(2*i):x}) >> {i}' for i in range(16))
const x = (p & 0x1) >> 0 | (p & 0x4) >> 1 | (p & 0x10) >> 2 |
(p & 0x40) >> 3 | (p & 0x100) >> 4 | (p & 0x400) >> 5 |
(p & 0x1000) >> 6 | (p & 0x4000) >> 7 | (p & 0x10000) >> 8 |
(p & 0x40000) >> 9 | (p & 0x100000) >> 10 | (p & 0x400000) >> 11 |
(p & 0x1000000) >> 12 | (p & 0x4000000) >> 13 | (p & 0x10000000) >> 14 | (p & 0x40000000) >> 15
// (python)
// ' | '.join(f'(p & 0x{2**(2*i + 1):x}) >> {i+1}' for i in range(15))
const y = (p & 0x2) >> 1 | (p & 0x8) >> 2 | (p & 0x20) >> 3 |
(p & 0x80) >> 4 | (p & 0x200) >> 5 | (p & 0x800) >> 6 |
(p & 0x2000) >> 7 | (p & 0x8000) >> 8 | (p & 0x20000) >> 9 |
(p & 0x80000) >> 10 | (p & 0x200000) >> 11 | (p & 0x800000) >> 12 |
(p & 0x2000000) >> 13 | (p & 0x8000000) >> 14 | (p & 0x20000000) >> 15
return { x, y }
}
// f: base pixel index
// x: north east index in base pixel
// y: north west index in base pixel
function nest2fxy(nside: number, ipix: number) {
const nside2 = nside * nside
const f = Math.floor(ipix / nside2) // base pixel index
const k = ipix % nside2 // nested pixel index in base pixel
const { x, y } = bit_decombine(k)
return { f, x, y }
}
function fxy2ring(nside: number, f: number, x: number, y: number) {
const f_row = Math.floor(f / 4) // {0 .. 2}
const f1 = f_row + 2 // {2 .. 4}
const v = x + y
const i = f1 * nside - v - 1
if (i < nside) { // north polar cap
const f_col = f % 4
const ipix = 2 * i * (i - 1) + (i * f_col) + nside - y - 1
return ipix
}
if (i < 3 * nside) { // equatorial belt
const h = x - y
const f2 = 2 * (f % 4) - (f_row % 2) + 1 // {0 .. 7}
const k = (f2 * nside + h + (8 * nside)) % (8 * nside)
const offset = 2 * nside * (nside - 1)
const ipix = offset + (i - nside) * 4 * nside + (k >> 1)
return ipix
}
else { // south polar cap
const i_i = 4 * nside - i
const i_f_col = 3 - (f % 4)
const j = 4 * i_i - (i_i * i_f_col) - y
const i_j = 4 * i_i - j + 1
const ipix = 12 * nside * nside - 2 * i_i * (i_i - 1) - i_j
return ipix
}
}
// f, x, y -> spherical projection
export function fxy2tu(nside: number, f: number, x: number, y: number) {
const f_row = Math.floor(f / 4)
const f1 = f_row + 2
const f2 = 2 * (f % 4) - (f_row % 2) + 1
const v = x + y
const h = x - y
const i = f1 * nside - v - 1
const k = (f2 * nside + h + (8 * nside))
const t = k / nside * PI_4
const u = PI_2 - i / nside * PI_4
return { t, u }
}
export function orderpix2uniq(order: number, ipix: number): number {
/**
* Pack `(order, ipix)` into a `uniq` integer.
*
* This HEALPix "unique identifier scheme" is starting to be used widely:
* - see section 3.2 in http://healpix.sourceforge.net/pdf/intro.pdf
* - see section 2.3.1 in http://ivoa.net/documents/MOC/
*/
return 4 * ((1 << (2 * order)) - 1) + ipix
}
export function uniq2orderpix(uniq: number) {
/**
* Unpack `uniq` integer into `(order, ipix)`.
*
* Inverse of `orderpix2uniq`.
*/
assert(uniq <= 0x7fffffff)
let order = 0
let l = (uniq >> 2) + 1
while (l >= 4) {
l >>= 2
++order
}
const ipix = uniq - (((1 << (2 * order)) - 1) << 2)
return { order, ipix }
}
function ilog2(x: number) {
/**
* log2 for integer numbers.
*
* We're not calling Math.log2 because it's not supported on IE yet.
*/
let o = -1
while (x > 0) {
x >>= 1;
++o;
}
return o
}
const sign: (A: number) => number = (