1
1
import os
2
+ import struct
2
3
import urllib
4
+ import uuid
3
5
4
6
import numpy as np
7
+ from astropy import units as u
5
8
from astropy .io import fits
6
9
from astropy .wcs import WCS
7
10
from astropy_healpix import HEALPix , level_to_nside
11
+ from dask import array as da
8
12
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' ]
10
16
11
17
12
18
class HiPSArray :
@@ -19,17 +25,17 @@ def __init__(self, directory_or_url, level=None):
19
25
20
26
self ._properties = load_properties (directory_or_url )
21
27
22
- self ._tile_size = int (self ._properties ["hips_tile_width" ])
28
+ self ._tile_width = int (self ._properties ["hips_tile_width" ])
23
29
self ._order = int (self ._properties ["hips_order" ])
24
30
self ._level = self ._order if level is None else level
25
31
26
32
self ._tile_format = self ._properties ["hips_tile_format" ]
27
33
28
34
self ._nside = level_to_nside (self ._level )
29
35
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
31
37
32
- image_size = 5 * self ._nside * self ._tile_size
38
+ image_size = 5 * self ._nside * self ._tile_width
33
39
34
40
self .wcs = WCS (naxis = 2 )
35
41
self .wcs .wcs .ctype = "RA---HPX" , "DEC--HPX"
@@ -43,7 +49,7 @@ def __init__(self, directory_or_url, level=None):
43
49
self .dtype = float
44
50
self .ndim = 2
45
51
46
- self .chunksize = (self ._tile_size , self ._tile_size )
52
+ self .chunksize = (self ._tile_width , self ._tile_width )
47
53
48
54
self ._nan = np .nan * np .ones (self .chunksize , dtype = self .dtype )
49
55
@@ -83,3 +89,180 @@ def _get_tile(self, *, level, index):
83
89
return fits .getdata (filename ).astype (float )[::- 1 ]
84
90
else :
85
91
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
+ )
0 commit comments