Skip to content

Commit 6eda528

Browse files
committed
Added HiPS3D implementation
1 parent 1bb557a commit 6eda528

File tree

2 files changed

+216
-11
lines changed

2 files changed

+216
-11
lines changed

reproject/hips/dask_array.py

Lines changed: 188 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import os
2+
import struct
23
import urllib
4+
import uuid
35

46
import numpy as np
7+
from astropy import units as u
58
from astropy.io import fits
69
from astropy.wcs import WCS
710
from astropy_healpix import HEALPix, level_to_nside
11+
from dask import array as da
812

9-
from .utils import is_url, load_properties, tile_filename
13+
from .utils import is_url, load_properties, tile_filename, tile_filename_3d
14+
15+
__all__ = ['hips_as_dask', 'hips3d_as_dask']
1016

1117

1218
class HiPSArray:
@@ -19,17 +25,17 @@ def __init__(self, directory_or_url, level=None):
1925

2026
self._properties = load_properties(directory_or_url)
2127

22-
self._tile_size = int(self._properties["hips_tile_width"])
28+
self._tile_width = int(self._properties["hips_tile_width"])
2329
self._order = int(self._properties["hips_order"])
2430
self._level = self._order if level is None else level
2531

2632
self._tile_format = self._properties["hips_tile_format"]
2733

2834
self._nside = level_to_nside(self._level)
2935
self._hp = HEALPix(nside=self._nside, frame="icrs", order="nested")
30-
self._cdelt = 45 / self._tile_size / 2**self._level * 2**0.5
36+
self._cdelt = 45 / self._tile_width / 2**self._level * 2**0.5
3137

32-
image_size = 5 * self._nside * self._tile_size
38+
image_size = 5 * self._nside * self._tile_width
3339

3440
self.wcs = WCS(naxis=2)
3541
self.wcs.wcs.ctype = "RA---HPX", "DEC--HPX"
@@ -43,7 +49,7 @@ def __init__(self, directory_or_url, level=None):
4349
self.dtype = float
4450
self.ndim = 2
4551

46-
self.chunksize = (self._tile_size, self._tile_size)
52+
self.chunksize = (self._tile_width, self._tile_width)
4753

4854
self._nan = np.nan * np.ones(self.chunksize, dtype=self.dtype)
4955

@@ -83,3 +89,180 @@ def _get_tile(self, *, level, index):
8389
return fits.getdata(filename).astype(float)[::-1]
8490
else:
8591
return self._nan
92+
93+
94+
def freq2pix(order, freq):
95+
hash_value = get_hash(freq)
96+
return hash_value >> (59 - order)
97+
98+
99+
def get_hash(param_double):
100+
l1 = struct.unpack(">q", struct.pack(">d", param_double))[0]
101+
l2 = (l1 & 0x7FF0000000000000) >> 52
102+
l2 = (l2 - 929) << 52
103+
return (l1 & 0x800FFFFFFFFFFFFF) | l2
104+
105+
106+
def pix2freq(order, pix):
107+
delta_order = 59 - order
108+
nb_pix_in = pow2(delta_order)
109+
hash_value = (pix << delta_order) + nb_pix_in // 2
110+
return get_freq(hash_value)
111+
112+
113+
def pow2(exponent):
114+
return 1 << exponent
115+
116+
117+
def get_freq(hash_value):
118+
packed = struct.pack(">q", hash_value)
119+
return struct.unpack(">d", packed)[0]
120+
121+
122+
class HiPS3DArray:
123+
124+
def __init__(self, directory_or_url, level=None):
125+
126+
self._directory_or_url = directory_or_url
127+
128+
self._is_url = is_url(directory_or_url)
129+
130+
self._properties = load_properties(directory_or_url)
131+
132+
assert self._properties["dataproduct_type"] == "spectral-cube"
133+
134+
self._tile_width = int(self._properties["hips_tile_width"])
135+
self._tile_depth = int(self._properties["hips_tile_depth"])
136+
137+
self._order = int(self._properties["hips_order"])
138+
self._order_freq = int(self._properties["hips_order_freq"])
139+
140+
# FIXME: for now assume minimum order is 0
141+
142+
self._level = self._order if level is None else level
143+
self._level_freq = self._level + (self._order_freq - self._order)
144+
145+
self._tile_format = self._properties["hips_tile_format"]
146+
147+
self._nside = level_to_nside(self._level)
148+
self._hp = HEALPix(nside=self._nside, frame="icrs", order="nested")
149+
self._cdelt = 45 / self._tile_width / 2**self._level * 2**0.5
150+
151+
image_size = 5 * self._nside * self._tile_width
152+
153+
# For the image depth we could in principe do whole spectral domain but
154+
# this would make too many chunks for dask so we have to be more
155+
# sensible
156+
157+
# NOTE: em_min is given as wav but is minimum frequency
158+
159+
self._freq_min = (float(self._properties["em_min"]) * u.m).to_value(u.Hz, u.spectral())
160+
self._freq_max = (float(self._properties["em_min"]) * u.m).to_value(u.Hz, u.spectral())
161+
162+
# Now determine what the indices would be for this at the given spectral order
163+
164+
self._index_min = freq2pix(self._level_freq, self._freq_min)
165+
self._index_max = freq2pix(self._level_freq, self._freq_max) + 1
166+
167+
image_depth = (self._index_max - self._index_min) * self._tile_depth
168+
169+
# FIXME: make WCS 3D
170+
171+
self.wcs = WCS(naxis=2)
172+
self.wcs.wcs.ctype = "RA---HPX", "DEC--HPX"
173+
self.wcs.wcs.crval = 0, 0
174+
self.wcs.wcs.cdelt = self._cdelt, self._cdelt
175+
self.wcs.wcs.crpix = image_size / 2, image_size / 2
176+
self.wcs.wcs.crota = 0, 45
177+
self.wcs.wcs.set()
178+
179+
self.shape = (image_depth, image_size, image_size)
180+
self.dtype = float
181+
self.ndim = 2
182+
183+
self.chunksize = (self._tile_depth, self._tile_width, self._tile_width)
184+
185+
self._nan = np.nan * np.ones(self.chunksize, dtype=self.dtype)
186+
187+
def __getitem__(self, item):
188+
189+
# For now assume item is a list of slices. Find
190+
191+
ispec = (item[0].start + item[0].stop) // 2
192+
imid = (item[1].start + item[1].stop) // 2
193+
jmid = (item[2].start + item[2].stop) // 2
194+
195+
# Convert pixel coordinates to HEALPix indices
196+
197+
spatial_index = self._hp.skycoord_to_healpix(self.wcs.pixel_to_world(imid, jmid))
198+
199+
if spatial_index == -1:
200+
return self._nan
201+
202+
# Get spectral index
203+
204+
spectral_index = ispec // self._tile_depth + self._index_min
205+
206+
return self._get_tile(
207+
level=self._level, spatial_index=spatial_index, spectral_index=spectral_index
208+
)
209+
210+
def _get_tile(self, *, level, spatial_index, spectral_index):
211+
212+
filename_or_url = tile_filename_3d(
213+
spatial_level=self._level,
214+
spectral_level=self._level_freq,
215+
spatial_index=spatial_index,
216+
spectral_index=spectral_index,
217+
output_directory=self._directory_or_url,
218+
extension="fits",
219+
)
220+
221+
if self._is_url:
222+
try:
223+
filename, _ = urllib.request.urlretrieve(filename_or_url)
224+
except urllib.error.HTTPError:
225+
return self._nan
226+
elif not os.path.exists(filename_or_url):
227+
return self._nan
228+
else:
229+
filename = filename_or_url
230+
231+
with fits.open(filename) as hdulist:
232+
233+
hdu = hdulist[0]
234+
data = hdu.data
235+
236+
if data.shape != self.chunksize:
237+
238+
# Need to add padding
239+
240+
before = (hdu.header["TRIM3"], hdu.header["TRIM2"], hdu.header["TRIM1"])
241+
after = [
242+
(c - s - b)
243+
for (c, s, b) in zip(self.chunksize, data.shape, before, strict=False)
244+
]
245+
246+
data = np.pad(data, list(zip(before, after, strict=False)), constant_values=np.nan)
247+
248+
data = data[:, ::-1, :]
249+
250+
return data
251+
252+
253+
def hips_as_dask(directory_or_url, level=None):
254+
array_wrapper = HiPSArray(directory_or_url, level=level)
255+
return da.from_array(
256+
array_wrapper,
257+
chunks=array_wrapper.chunksize,
258+
name=str(uuid.uuid4()),
259+
)
260+
261+
262+
def hips3d_as_dask(directory_or_url, level=None):
263+
array_wrapper = HiPS3DArray(directory_or_url, level=level)
264+
return da.from_array(
265+
array_wrapper,
266+
chunks=array_wrapper.chunksize,
267+
name=str(uuid.uuid4()),
268+
)

reproject/hips/utils.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
pixel_resolution_to_nside,
1212
)
1313

14-
__all__ = ["tile_header", "tile_filename", "make_tile_folders", "determine_healpix_level"]
14+
__all__ = [
15+
"tile_header",
16+
"tile_filename",
17+
"tile_filename_3d",
18+
"make_tile_folders",
19+
"determine_healpix_level",
20+
"is_url",
21+
]
1522

1623

1724
def tile_header(*, level, index, frame, tile_size):
@@ -54,22 +61,37 @@ def tile_header(*, level, index, frame, tile_size):
5461
return header
5562

5663

57-
def _rounded_index(index):
64+
def _rounded_spatial_index(index):
5865
return 10000 * (index // 10000)
5966

6067

68+
def _rounded_spectral_index(index):
69+
return 10 * (index // 10)
70+
71+
6172
def tile_filename(*, level, index, output_directory, extension):
6273
return os.path.join(
6374
output_directory,
6475
f"Norder{level}",
65-
f"Dir{_rounded_index(index)}",
76+
f"Dir{_rounded_spatial_index(index)}",
6677
f"Npix{index}.{extension}",
6778
)
6879

6980

81+
def tile_filename_3d(
82+
*, spatial_level, spectral_level, spatial_index, spectral_index, output_directory, extension
83+
):
84+
return os.path.join(
85+
output_directory,
86+
f"Norder{spatial_level}_{spectral_level}",
87+
f"Dir{_rounded_spatial_index(spatial_index)}_{_rounded_spectral_index(spectral_index)}",
88+
f"Npix{spatial_index}_{spectral_index}.{extension}",
89+
)
90+
91+
7092
def make_tile_folders(*, level, indices, output_directory):
7193

72-
rounded_indices = np.unique(_rounded_index(indices))
94+
rounded_indices = np.unique(_rounded_spatial_index(indices))
7395
for index in rounded_indices:
7496
dirname = os.path.dirname(
7597
tile_filename(level=level, index=index, output_directory=output_directory, extension="")
@@ -106,7 +128,7 @@ def determine_healpix_level(wcs_in, tile_size):
106128
return target_level
107129

108130

109-
def _is_url(directory):
131+
def is_url(directory):
110132
return directory.startswith("http://") or directory.startswith("https://")
111133

112134

@@ -118,7 +140,7 @@ def save_properties(directory, properties):
118140

119141
def load_properties(directory_or_url):
120142

121-
if _is_url(directory_or_url):
143+
if is_url(directory_or_url):
122144
properties_filename, _ = urllib.request.urlretrieve(f"{directory_or_url}/properties")
123145
else:
124146
properties_filename = os.path.join(directory_or_url, "properties")

0 commit comments

Comments
 (0)