Coverage for PyFHD/gridding/gridding_utils.py: 76%
293 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-07-01 10:58 +0800
« prev ^ index » next coverage.py v7.9.1, created at 2025-07-01 10:58 +0800
1import numpy as np
2from numpy.typing import NDArray
3import PyFHD.gridding.filters as filters
4from PyFHD.pyfhd_tools.pyfhd_utils import rebin, histogram, array_match, meshgrid
5from scipy.signal import convolve
6from astropy.convolution import Box2DKernel
7from math import pi
8from logging import Logger
9import h5py
12def interpolate_kernel(
13 kernel_arr: NDArray[np.complex128],
14 x_offset: NDArray[np.integer],
15 y_offset: NDArray[np.integer],
16 dx0dy0: NDArray[np.float64],
17 dx1dy0: NDArray[np.float64],
18 dx0dy1: NDArray[np.float64],
19 dx1dy1: NDArray[np.float64],
20) -> NDArray[np.complex128]:
21 """
22 Perform a bilinear interpolation given the 2D derivatives of the baseline center to the nearest
23 hyperresolved pixel edge in {u,v} space. This will provide a quadratic estimate at the sample
24 location to smooth out the dependence on hyperresolved pixel size.
26 Parameters
27 ----------
28 kernel_arr : NDArray[np.complex128]
29 The 2D kernel for bilinear interpolation
30 x_offset : NDArray[np.integer]
31 The nearest pixel offset in the x (u) direction
32 y_offset : NDArray[np.integer]
33 The nearest pixel offset in the y (v) direction
34 dx0dy0 : NDArray[np.float64]
35 (1 - derivative to pixel edge in x) * (1 - derivative to pixel edge in y) for selected baselines
36 dx1dy0 : NDArray[np.float64]
37 (derivative to pixel edge in x) * (1 - derivative to pixel edge in y) for selected baselines
38 dx0dy1 : NDArray[np.float64]
39 (1 - derivative to pixel edge in x) * (derivative to pixel edge in y) for selected baselines
40 dx1dy1 : NDArray[np.float64]
41 (derivative to pixel edge in x) * (derivative to pixel edge in y) for selected baselines
43 Returns
44 -------
45 kernel: NDArray[np.complex128]
46 The interpolated 2D kernel
47 """
48 # x_offset and y_offset needed to be swapped around as IDL is column-major, while Python is row-major
49 kernel = kernel_arr[y_offset, x_offset] * dx0dy0
50 kernel += kernel_arr[y_offset, x_offset + 1] * dx1dy0
51 kernel += kernel_arr[y_offset + 1, x_offset] * dx0dy1
52 kernel += kernel_arr[y_offset + 1, x_offset + 1] * dx1dy1
54 return kernel
57def conjugate_mirror(
58 image: NDArray[np.complex128 | np.float64],
59) -> NDArray[np.complex128 | np.float64]:
60 """
61 Mirror an image about the origin and take the complex conjugate. The origin is considered to be the first
62 row, and thus is not repeated in the conjugate mirror.
64 Parameters
65 ----------
66 image: NDArray[np.complex128 | np.float64]
67 A 2D array of real or complex numbers
69 Returns
70 -------
71 conj_mirror_image: NDArray[np.complex128 | np.float64]
72 The 2D conjugate mirror of the input without the origin
73 """
74 # Flip image left to right (i.e. flips columns) & Flip image up to down (i.e. flips rows)
75 conj_mirror_image = np.flip(image)
76 # Shifts columns then rows by 1
77 conj_mirror_image = np.roll(conj_mirror_image, 1, axis=(1, 0))
78 # If any of the array is complex, or its a complex array, get the conjugates
79 if np.iscomplexobj(image):
80 conj_mirror_image = np.conjugate(conj_mirror_image)
81 return conj_mirror_image
84def baseline_grid_locations(
85 obs: dict,
86 psf: dict,
87 params: dict,
88 vis_weights: NDArray[np.float64],
89 logger: Logger,
90 bi_use: NDArray[np.integer] | None = None,
91 fi_use: NDArray[np.integer] | None = None,
92 fill_model_visibilities: bool = False,
93 interp_flag: bool = False,
94 mask_mirror_indices: bool = False,
95) -> dict:
96 """
97 Calculate the histogram of baseline grid locations in units of pixels whilst also
98 returning the minimum pixel number that an unflagged baseline contributes to (depending on the
99 size of the kernel). Optionally return the 2D derivatives for bilinear interpolation and the
100 indices of the unflagged baselines/frequencies.
102 Parameters
103 ----------
104 obs : dict
105 Observation metadata dictionary
106 psf : dict
107 Beam metadata dictionary
108 params : dict
109 Visibility metadata dictionary
110 vis_weights : NDArray[np.float64]
111 Weights (flags) of the visibilities
112 logger : Logger
113 PyFHD's logger
114 bi_use : NDArray[np.integer] | None, optional
115 Baseline index array for gridding, i.e even vs odd time stamps, by default None
116 fi_use : NDArray[np.integer] | None, optional
117 Frequency index array for gridding, i.e. gridding all frequencies for continuum images, by default None
118 fill_model_visibilities : bool, optional
119 Calculate baseline grid locations for all baselines, regardless of flags, by default False
120 interp_flag : bool, optional
121 Calculate derivatives for bilinear interpolation of the kernel to pixel locations, by default False
122 mask_mirror_indices : bool, optional
123 Exclude baselines mirrored along the v-axis, by default False
125 Returns
126 -------
127 baselines_dict : dict
128 Histogram of baseline grid locations, associated derivatives, and minimum contributing pixel location,
129 arranged in a dictionary
130 """
132 # Set up the return dictionary
133 baselines_dict = {}
135 # Retrieve information from the data structures
136 n_tile = obs["n_tile"]
137 n_freq = obs["n_freq"]
138 dimension = obs["dimension"]
139 elements = obs["elements"]
140 kbinsize = obs["kpix"]
141 min_baseline = obs["min_baseline"]
142 max_baseline = obs["max_baseline"]
143 b_info = obs["baseline_info"]
144 psf_dim = psf["dim"]
145 psf_resolution = psf["resolution"]
146 if isinstance(psf, h5py.File):
147 psf_dim = psf_dim[0]
148 psf_resolution = psf_resolution[0]
150 # Frequency information of the visibilities
151 if fill_model_visibilities:
152 fi_use = np.arange(n_freq)
153 elif fi_use is None:
154 fi_use = np.nonzero(b_info["freq_use"])[0]
155 frequency_array = b_info["freq"]
156 frequency_array = frequency_array[fi_use]
158 # Set the bi_use_flag, if we set it here, we need to return it
159 bi_use_flag = False
160 # Baselines to use
161 if bi_use is None:
162 bi_use_flag = True
163 # if the data is being gridded separately for the even/odd time samples
164 # then force flagging to be consistent across even/odd sets
165 if not fill_model_visibilities:
166 flag_test = np.sum(np.maximum(vis_weights, 0), axis=0)
167 bi_use = np.nonzero(flag_test)[0]
168 else:
169 tile_use = np.arange(n_tile) + 1
170 bi_use = array_match(
171 b_info["tile_a"].astype(int),
172 tile_use,
173 array_2=b_info["tile_b"].astype(int),
174 )
176 # Rather than calculating the flat indexes we want, lets just index the array
177 # by the frequency use and baseline_use indexes
178 rows, cols = np.meshgrid(fi_use, bi_use)
179 vis_weights_use = vis_weights[rows, cols].T
181 # Units in pixel/Hz
182 kx_arr = params["uu"][bi_use] / kbinsize
183 ky_arr = params["vv"][bi_use] / kbinsize
185 if not fill_model_visibilities:
186 # Flag baselines on their maximum and minimum extent in the full frequency range of the observation
187 # This prevents the sudden dissapearance of baselines along frequency
188 dist_test = np.sqrt(kx_arr**2 + ky_arr**2) * kbinsize
189 dist_test_max = np.max(obs["baseline_info"]["freq"]) * dist_test
190 dist_test_min = np.min(obs["baseline_info"]["freq"]) * dist_test
191 flag_dist_baseline = np.where(
192 (dist_test_min < min_baseline) | (dist_test_max > max_baseline)
193 )
194 del (dist_test, dist_test_max, dist_test_min)
196 # Create the other half of the uv plane via negating the locations
197 conj_i = np.where(ky_arr > 0)[0]
198 if conj_i.size > 0: 198 ↛ 203line 198 didn't jump to line 203 because the condition on line 198 was always true
199 kx_arr[conj_i] = -kx_arr[conj_i]
200 ky_arr[conj_i] = -ky_arr[conj_i]
202 # Center of baselines for x and y in units of pixels
203 xcen = np.outer(frequency_array, kx_arr)
204 ycen = np.outer(frequency_array, ky_arr)
206 # Pixel number offset per baseline for each uv-box subset
207 x_offset = np.fix(
208 np.floor((xcen - np.floor(xcen)) * psf_resolution) % psf_resolution
209 ).astype(np.int64)
210 y_offset = np.fix(
211 np.floor((ycen - np.floor(ycen)) * psf_resolution) % psf_resolution
212 ).astype(np.int64)
214 if interp_flag:
215 # Derivatives from pixel edge to baseline center for use in interpolation
216 dx_arr = (xcen - np.floor(xcen)) * psf_resolution - np.floor(
217 (xcen - np.floor(xcen)) * psf_resolution
218 )
219 dy_arr = (ycen - np.floor(ycen)) * psf_resolution - np.floor(
220 (ycen - np.floor(ycen)) * psf_resolution
221 )
222 baselines_dict["dx0dy0_arr"] = (1 - dx_arr) * (1 - dy_arr)
223 baselines_dict["dx0dy1_arr"] = (1 - dx_arr) * dy_arr
224 baselines_dict["dx1dy0_arr"] = dx_arr * (1 - dy_arr)
225 baselines_dict["dx1dy1_arr"] = dx_arr * dy_arr
227 # The minimum pixel in the uv-grid (bottom left of the kernel) that each baseline contributes to
228 xmin = (np.floor(xcen) + elements / 2 - (psf_dim / 2 - 1)).astype(np.int64)
229 ymin = (np.floor(ycen) + dimension / 2 - (psf_dim / 2 - 1)).astype(np.int64)
231 # Set the minimum pixel value of baselines which fall outside of the uv-grid tom -1 to exclude them
232 range_test_x_i = np.where((xmin <= 0) | (xmin + psf_dim - 1 >= elements - 1))
233 range_test_y_i = np.where((ymin <= 0) | (ymin + psf_dim - 1 >= dimension - 1))
234 if range_test_x_i[0].size > 0: 234 ↛ 237line 234 didn't jump to line 237 because the condition on line 234 was always true
235 xmin[range_test_x_i] = -1
236 ymin[range_test_x_i] = -1
237 if range_test_y_i[0].size > 0:
238 xmin[range_test_y_i] = -1
239 ymin[range_test_y_i] = -1
241 # Flag baselines which fall outside the uv plane
242 if not fill_model_visibilities:
243 if np.size(flag_dist_baseline) > 0: 243 ↛ 251line 243 didn't jump to line 251 because the condition on line 243 was always true
244 # If baselines fall outside the desired min/max baseline range at all during the frequency range
245 # then set their maximum pixel value to -1 to exclude them
246 xmin[:, flag_dist_baseline] = -1
247 ymin[:, flag_dist_baseline] = -1
248 del flag_dist_baseline
250 # Normally we check vis_weight_switch, but its always true here so... do this
251 flag_i = np.where(vis_weights_use <= 0)
252 if fill_model_visibilities:
253 n_flag = 0
254 else:
255 n_flag = np.size(flag_i)
256 if n_flag > 0:
257 xmin[flag_i] = -1
258 ymin[flag_i] = -1
260 if mask_mirror_indices:
261 # Option to exclude v-axis mirrored baselines
262 if conj_i.size > 0: 262 ↛ 267line 262 didn't jump to line 267 because the condition on line 262 was always true
263 xmin[:, conj_i] = -1
264 ymin[:, conj_i] = -1
266 # If xmin or ymin is invalid then adjust the baselines dict as necessary
267 if xmin.size == 0 or ymin.size == 0 or np.max([np.max(xmin), np.max(ymin)]) < 0: 267 ↛ 268line 267 didn't jump to line 268 because the condition on line 267 was never true
268 logger.warning("All data flagged or cut!")
269 baselines_dict["bin_n"] = 0
270 baselines_dict["n_bin_use"] = 0
271 baselines_dict["bin_i"] = -1
272 baselines_dict["ri"] = 0
273 else:
274 # Match all visibilities that map from and to exactly the same pixels and store them as a histogram in bin_n
275 # with their respective index ri. Setting min equal to 0, excludes flagged data (data set to -1).
276 for_hist = xmin + ymin * dimension
277 bin_n, _, ri = histogram(for_hist, min=0)
278 bin_i = np.nonzero(bin_n)[0]
280 # Update the baselines_dict which gets returned
281 baselines_dict["bin_n"] = bin_n
282 baselines_dict["bin_i"] = bin_i
283 baselines_dict["n_bin_use"] = bin_i.size
284 baselines_dict["ri"] = ri
285 # Add values to baselines dict
286 baselines_dict["xmin"] = xmin
287 baselines_dict["ymin"] = ymin
288 baselines_dict["x_offset"] = x_offset
289 baselines_dict["y_offset"] = y_offset
290 if bi_use_flag:
291 baselines_dict["bi_use"] = bi_use
293 # Return the dictionary
294 return baselines_dict
297def dirty_image_generate(
298 dirty_image_uv: NDArray[np.complex128],
299 pyfhd_config: dict,
300 logger: Logger,
301 uniform_filter_uv: NDArray[np.float64] | None = None,
302 mask: NDArray[np.integer] | None = None,
303 baseline_threshold: int | float = 0,
304 normalization: float | NDArray[np.float64] | None = None,
305 resize: int | None = None,
306 width_smooth: int | float | None = None,
307 degpix: float | None = None,
308 not_real: bool = False,
309 pad_uv_image: int | float | None = None,
310 weights: NDArray[np.float64] | None = None,
311 filter: NDArray[np.float64] | None = None,
312 beam_ptr: NDArray[np.complex128] | None = None,
313) -> tuple[
314 NDArray[np.complex128], NDArray[np.float64], float | NDArray[np.float64] | None
315]:
316 """
317 Generate a projected image from an input {u,v} plane through a 2D FFT. Optionally apply padding, masking,
318 filtering, and more.
320 Parameters
321 ----------
322 dirty_image_uv : NDArray[np.complex128]
323 A 2D {u,v} plane which generally includes the beam via a gridding kernel
324 pyfhd_config : dict
325 PyFHD's configuration dictionary containing all the options set for a PyFHD run
326 logger : Logger
327 PyFHD's logger
328 uniform_filter_uv : NDArray[np.float64] | None, optional
329 A 2D {u,v} gridded visibility number, by default None
330 mask : NDArray[np.integer] | None, optional
331 A 2D {u,v} mask to apply before image creation, by default None
332 baseline_threshold : int | float, optional
333 The maximum baseline length to include in units of pixels, by default 0
334 normalization : float | NDArray[np.float64] | None, optional
335 A value by which to normalize the image by, by default None
336 resize : int | None, optional
337 Increase the number of pixels by a factor and rebin the input {u,v} plane, by default None
338 width_smooth : int | float | None, optional
339 Smooth out the harsh baseline threshold by the given number of pixels, by default None
340 degpix : float | None, optional
341 Degrees per pixel, by default None
342 not_real : bool, optional
343 Flag to return a complex image, by default False
344 pad_uv_image : int | float | None, optional
345 Pad the {u,v} plane by this pixel amount to increase perceived resolution of the image, by default None
346 weights : NDArray[np.float64] | None, optional
347 Gridded {u,v} plane of visibility weights, necessary in some filtering schemes, by default None
348 filter : NDArray[np.float64] | None, optional
349 Image filter to apply, by default None
350 beam_ptr : NDArray[np.complex128] | None, optional
351 Weight by an additional factor of the beam for optimal weighting, by default None
353 Returns
354 -------
355 dirty_image : NDArray[np.complex128]
356 A 2D {l,m} directional-cosine image plane
357 filter : NDArray[np.float64]
358 The filter applied to the dirty image
359 normalization : float | NDArray[np.float64] | None
360 The normalization (if any) applied to the dirty image
361 """
363 # dimension is columns, elements is rows
364 elements, dimension = dirty_image_uv.shape
365 di_uv_use = dirty_image_uv
366 # If the baseline threshold has been set
367 if baseline_threshold is not None: 367 ↛ 400line 367 didn't jump to line 400 because the condition on line 367 was always true
368 if width_smooth is None:
369 width_smooth = np.floor(np.sqrt(dimension * elements) / 100)
370 rarray = np.sqrt(
371 (meshgrid(dimension, 1) - dimension / 2) ** 2
372 + (meshgrid(elements, 2) - elements / 2) ** 2
373 )
374 # Get all the values that meet the threshold
375 if baseline_threshold >= 0: 375 ↛ 378line 375 didn't jump to line 378 because the condition on line 375 was always true
376 cut_i = np.where(rarray.flatten() < baseline_threshold)
377 else:
378 cut_i = np.where(rarray.flatten() > np.abs(baseline_threshold))
379 # Create the mask array of ones
380 mask_bt = np.ones((elements, dimension))
381 # If there are values from cut, then use all those here and replace with 0
382 if np.size(cut_i) > 0:
383 mask_bt_flatiter = mask_bt.flat
384 mask_bt_flatiter[cut_i] = 0
385 # Get the kernel width
386 kernel_width = np.max([width_smooth, 1])
387 # In IDL if the kernel width is even one is added to make it odd
388 if kernel_width % 2 == 0:
389 kernel_width += 1
390 # Use a box width averaging filter over the mask, use valid so we can insert it in later
391 box_averages = convolve(mask_bt, Box2DKernel(kernel_width), mode="valid")
392 # Since IDL SMOOTH edges by default are the edges of the array used, ignore edges (its the reason why we used a valid convolve)
393 start = int(kernel_width // 2)
394 end = int(mask_bt.shape[1] - (kernel_width // 2))
395 mask_bt[start:end, start:end] = box_averages
396 # Apply boxed mask to the dirty image
397 di_uv_use *= mask_bt
399 # If a mask was supplied use that too
400 if mask is not None:
401 di_uv_use *= mask
403 # If a filter was supplied as a numpy array (we can adjust this to support different formats)
404 if filter is not None:
405 if isinstance(filter, np.ndarray): 405 ↛ 443line 405 didn't jump to line 443 because the condition on line 405 was always true
406 # If the filter is already the right size, use it
407 if np.size(filter) == np.size(di_uv_use):
408 di_uv_use *= filter
409 # Otherwise use a filter function
410 else:
411 if pyfhd_config["image_filter"] == "filter_uv_uniform": 411 ↛ 413line 411 didn't jump to line 413 because the condition on line 411 was always true
412 logger.info("Using filter_uv_uniform for dirty_image_generate")
413 elif pyfhd_config["image_filter"] == "filter_uv_hannning":
414 logger.warning(
415 "filter_uv_hanning hasn't been translated yet using filter_uv_uniform for dirty_image_generate instead"
416 )
418 elif pyfhd_config["image_filter"] == "filter_uv_natural":
419 logger.warning(
420 "filter_uv_natural hasn't been translated yet using filter_uv_uniform for dirty_image_generate instead"
421 )
423 elif pyfhd_config["image_filter"] == "filter_uv_radial":
424 logger.warning(
425 "filter_uv_radial hasn't been translated yet using filter_uv_uniform for dirty_image_generate instead"
426 )
428 elif pyfhd_config["image_filter"] == "filter_uv_tapered_uniform":
429 logger.warning(
430 "filter_uv_tapered_uniform hasn't been translated yet using filter_uv_uniform for dirty_image_generate instead"
431 )
433 elif pyfhd_config["image_filter"] == "filter_uv_optimal":
434 logger.warning(
435 "filter_uv_optimal hasn't been translated yet using filter_uv_uniform for dirty_image_generate instead"
436 )
437 # Since we only use filter_uniform at the moment, put the call to it here.
438 di_uv_use, filter = filters.filter_uv_uniform(
439 di_uv_use, vis_count=uniform_filter_uv, weights=weights
440 )
442 # Resize the dirty image by the factor resize
443 if resize is not None:
444 dimension *= resize
445 elements *= resize
446 # Ensure elements and dimension are integers as resize should be an int
447 # but if it's not this makes sure dimension and elements are ints afterwards
448 dimension = int(dimension)
449 elements = int(elements)
450 di_uv_real = di_uv_use.real
451 di_uv_img = di_uv_use.imag
452 # Use rebin to resize, apply to real and complex separately
453 di_uv_real = rebin(di_uv_real, (elements, dimension))
454 di_uv_img = rebin(di_uv_img, (elements, dimension))
455 # Combine real and complex back together
456 di_uv_use = di_uv_real + di_uv_img * 1j
458 # Apply padding if it was supplied
459 if pad_uv_image is not None:
460 # dimension_new = int(np.max([np.max([dimension, elements]) * pad_uv_image, np.max([dimension, elements])]))
461 # di_uv1 = np.zeros((dimension_new, dimension_new), dtype = "complex")
462 # di_uv1[dimension_new // 2 - elements // 2 : dimension_new // 2 + elements // 2,
463 # dimension_new // 2 - dimension // 2 : dimension_new // 2 + dimension // 2] = di_uv_use
464 di_uv1 = np.pad(
465 di_uv_use, (np.max([dimension, elements]) // 2) * (int(pad_uv_image) - 1)
466 )
467 di_uv_use = di_uv1 * (pad_uv_image**2)
469 # FFT normalization
470 if degpix is not None:
471 di_uv_use /= np.radians(degpix) ** 2
473 # Multivariate Fast Fourier Transform
474 dirty_image = np.fft.fftshift(
475 np.fft.fftn(np.fft.fftshift(di_uv_use), norm="forward")
476 )
477 if not not_real:
478 dirty_image = dirty_image.real
480 # filter_uv_optimal produces images that are weighted by one factor of the beam
481 # Weight by an additional factor of the beam to align with FHD's convention
482 if pyfhd_config["image_filter"] == "filter_uv_optimal" and beam_ptr is not None: 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true
483 dirty_image *= beam_ptr
485 # If we are returning complex, make sure its complex
486 if not_real:
487 dirty_image = dirty_image.astype(np.complex128)
488 else:
489 dirty_image = dirty_image.real
490 # Normalize by the matrix given, if it was given
491 if normalization is not None:
492 dirty_image *= normalization
494 # Return
495 return dirty_image, filter, normalization
498def grid_beam_per_baseline(
499 psf: dict,
500 pyfhd_config: dict,
501 logger: Logger,
502 uu: NDArray[np.float64],
503 vv: NDArray[np.float64],
504 ww: NDArray[np.float64],
505 l_mode: NDArray[np.float64],
506 m_mode: NDArray[np.float64],
507 n_tracked: NDArray[np.float64],
508 frequency_array: NDArray[np.float64],
509 x: NDArray[np.float64],
510 y: NDArray[np.float64],
511 xmin_use: int,
512 ymin_use: int,
513 freq_i: NDArray[np.integer],
514 bt_index: NDArray[np.integer],
515 polarization: int,
516 image_bot: int,
517 image_top: int,
518 psf_dim3: int,
519 box_matrix: NDArray[np.complex128],
520 vis_n: int,
521 beam_int: NDArray[np.complex128] | None = None,
522 beam2_int: NDArray[np.complex128] | None = None,
523 n_grp_use: NDArray[np.integer] | None = None,
524 degrid_flag: bool = False,
525):
526 """
527 Calculate the contribution of each baseline to the static {u,v} grid using the corrective phases in
528 image-space, as detailed in J. Line's thesis "PUMA and MAJICK: cross-matching and imaging techniques
529 for a detection of the epoch of reionisation"
531 Parameters
532 ----------
533 psf : dict
534 Beam metadata dictionary
535 pyfhd_config : dict
536 PyFHD's configuration dictionary containing all the options set for a PyFHD run
537 logger : Logger
538 PyFHD's logger
539 uu : NDArray[np.float64]
540 1D array of the u-coordinate of selected baselines in light travel time
541 vv : NDArray[np.float64]
542 1D array of the v-coordinate of selected baselines in light travel time
543 ww : NDArray[np.float64]
544 1D array of the w-coordinate of selected baselines in light travel time
545 l_mode : NDArray[np.float64]
546 Directional-cosine l of pixel centers of the hyperresolved beam
547 m_mode : NDArray[np.float64]
548 Directional-cosine m of pixel centers of the hyperresolved beam
549 n_tracked : NDArray[np.float64]
550 Directional-cosine n of pixel centers of the phase-tracked hyperresolved beam
551 frequency_array : NDArray[np.float64]
552 Array of selected frequencies in Hz
553 x : NDArray[np.float64]
554 1D array of gridding extent and resolution in the x-direction in wavelengths
555 y : NDArray[np.float64]
556 1D array of gridding extent and resolution in the y-direction in wavelengths
557 xmin_use : int
558 The minimum x-pixel that each selected baseline contributes to
559 ymin_use : int
560 The minimum y-pixel that each selected baseline contributes to
561 freq_i : NDArray[np.integer]
562 The current frequency index
563 bt_index : NDArray[np.integer]
564 The current baseline/time index
565 polarization : int
566 The current polarization index
567 image_bot : int
568 The bottom-most pixel index that the image-space hyperresolved beam contributes to
569 image_top : int
570 The top-most pixel index that the image-space hyperresolved beam contributes to
571 psf_dim3 : int
572 The pixel area of the psf footprint on the {u,v} grid
573 box_matrix : NDArray[np.complex128]
574 A 2D array of the number of visibilities to grid and the area of each visibility on the static {u,v} grid
575 vis_n : int
576 The number of visibilities to grid
577 beam_int : NDArray[np.complex128] | None, optional
578 The integral of the beam sensitivity in {u,v} space, by default None
579 beam2_int : NDArray[np.complex128] | None, optional
580 The integral of the squared beam sensitivity in {u,v} space, by default None
581 n_grp_use : NDArray[np.integer] | None, optional
582 The number of baselines in the current grouping, by default None
583 degrid_flag : bool, optional
584 Perform degridding instead of gridding, by default False
586 Returns
587 -------
588 box_matrix : NDArray[np.complex128]
589 The kernel values on the static {u,v} grid for each visibility
590 """
592 psf_dim = psf["dim"]
593 psf_resolution = psf["resolution"]
594 if isinstance(psf, h5py.File): 594 ↛ 595line 594 didn't jump to line 595 because the condition on line 594 was never true
595 psf_dim = psf_dim[0]
596 psf_resolution = psf_resolution[0]
597 # Loop over all visibilities that fall within the chosen visibility box
598 for ii in range(vis_n):
599 # Pixel center offset phases
600 deltau_l = l_mode * (
601 uu[bt_index[ii]] * frequency_array[freq_i[ii]] - x[xmin_use + psf_dim // 2]
602 )
603 deltav_m = m_mode * (
604 vv[bt_index[ii]] * frequency_array[freq_i[ii]] - y[ymin_use + psf_dim // 2]
605 )
606 # w term offset phase
607 w_n_tracked = n_tracked * ww[bt_index[ii]] * frequency_array[freq_i[ii]]
609 # Generate a UV beam from the image space beam, offset by calculated phases
610 psf_base_superres, _, _ = dirty_image_generate(
611 psf["image_info"]["image_power_beam_arr"][polarization]
612 * np.exp(2 * pi * (0 + 1j) * (-w_n_tracked + deltau_l + deltav_m)),
613 pyfhd_config,
614 logger,
615 not_real=True,
616 )
617 psf_base_superres = psf_base_superres[
618 image_bot : image_top + 1, image_bot : image_top + 1
619 ]
621 # A quick way to sum down the image by a factor of 2 in both dimensions.
622 # A 4x4 example where we sum down by a factor of 2
623 #
624 # 1 2 3 4 1 2 1 2 1 2 5 6 14 46 14 22
625 # 5 6 7 8 --> 3 4 --> 5 6 --> 9 10 13 14 --> 22 54 --> 46 54
626 # 9 10 11 12 9 10
627 # 13 14 15 16 5 6 13 14 3 4 7 8
628 # 7 8 11 12 15 16
629 # 3 4
630 # 9 10 7 8
631 # 11 12 11 12
632 # 15 16
633 # 13 14
634 # 15 16
635 d = psf_base_superres.shape
636 # Note columns and rows are swapped from IDL so nx is now rows!
637 nx = d[0] // psf_resolution
638 ny = d[1] // psf_resolution
639 # The same result of IDL in numpy is np.reshape, with shape swapping rows and columns, then doing transpose of this shape
640 psf_base_superres = np.reshape(
641 psf_base_superres, [psf_resolution * ny, nx, psf_resolution]
642 )
643 psf_base_superres = np.transpose(psf_base_superres, [1, 0, 2])
644 psf_base_superres = np.reshape(psf_base_superres, [ny, nx, psf_resolution**2])
645 psf_base_superres = np.sum(psf_base_superres, -1)
646 psf_base_superres = np.transpose(psf_base_superres)
647 psf_base_superres = np.reshape(psf_base_superres, psf_dim**2)
648 start = psf_dim3 * ii
649 end = start + psf_base_superres.size
650 box_matrix_iter = box_matrix.flat
651 box_matrix_iter[start:end] = psf_base_superres
653 # Subtract off a small clip, set negative indices to 0, and renomalize.
654 # This is a modification of the look-up-table beam using a few assumptions
655 # to make it faster/feasible to run.
656 # Modifications: done per group of baselines that fit within the current box,
657 # rather than individually. region_grow is not used to find a contiguous
658 # edge around the beam to cut because it is too slow.
659 if pyfhd_config["beam_clip_floor"]: 659 ↛ 674line 659 didn't jump to line 674 because the condition on line 659 was always true
660 psf_val_ref = np.sum(box_matrix, 1)
661 psf_amp = np.abs(box_matrix)
662 psf_mask_threshold_use = np.max(psf_amp) / psf["beam_mask_threshold"]
663 psf_amp -= psf_mask_threshold_use
664 psf_phase = np.arctan2(box_matrix.imag, box_matrix.real)
665 psf_amp = np.maximum(psf_amp, np.zeros_like(psf_amp))
666 box_matrix = psf_amp * np.cos(psf_phase) + (0 + 1j) * psf_amp * np.sin(
667 psf_phase
668 )
669 ref_temp = np.sum(box_matrix, -1)
670 box_matrix[:vis_n, :] *= np.reshape(
671 psf_val_ref / ref_temp, (psf_val_ref.size, 1)
672 )
674 if ( 674 ↛ 681line 674 didn't jump to line 681 because the condition on line 674 was never true
675 degrid_flag
676 and beam_int is not None
677 and beam2_int is not None
678 and n_grp_use is not None
679 ):
680 # Calculate the beam and beam^2 integral (degridding)
681 beam_int_temp = np.sum(box_matrix, 0) / psf_resolution**2
682 beam2_int_temp = np.sum(np.abs(box_matrix) ** 2, 0) / psf_resolution**2
683 for ii in range(np.size(freq_i)):
684 beam_int[freq_i[ii]] += beam_int_temp[ii]
685 beam2_int[freq_i[ii]] += beam2_int_temp[ii]
686 n_grp_use[freq_i[ii]] += 1
688 return box_matrix
691def visibility_count(
692 obs: dict,
693 psf: dict,
694 params: dict,
695 vis_weights: NDArray[np.float64],
696 logger: Logger,
697 fi_use: NDArray[np.integer] | None = None,
698 bi_use: NDArray[np.integer] | None = None,
699 mask_mirror_indices: bool = False,
700 no_conjugate: bool = False,
701 fill_model_visibilities: bool = False,
702) -> NDArray[np.float64]:
703 """
704 Calculate the number of contributing visibilities per pixel on the static {u,v} grid
706 Parameters
707 ----------
708 obs : dict
709 Observation metadata dictionary
710 params : dict
711 Visibility metadata dictionary
712 vis_weights : NDArray[np.float64]
713 Weights (flags) of the visibilities
714 pyfhd_config : dict
715 PyFHD's configuration dictionary containing all the options set for a PyFHD run
716 logger : Logger
717 PyFHD's logger
718 fi_use : NDArray[np.integer] | None, optional
719 Frequency index array for gridding, i.e. gridding all frequencies for continuum images, by default None
720 bi_use : NDArray[np.integer] | None, optional
721 Baseline index array for gridding, i.e even vs odd time stamps, by default None
722 mask_mirror_indices : bool, optional
723 Exclude baselines mirrored along the v-axis, by default False
724 no_conjugate : bool, optional
725 Do not perform the conjugate mirror of the {u,v} plane, by default False
726 fill_model_visibilities : bool, optional
727 Calculate baseline grid locations for all baselines, regardless of flags, by default False
729 Returns
730 -------
731 uniform_filter : NDArray[np.float64]
732 2D array of number of contributing visibilities per pixel on the {u,v} grid
733 """
735 # Retrieve info from the data structures
736 dimension = int(obs["dimension"])
737 elements = int(obs["elements"])
738 psf_dim = psf["dim"]
739 if isinstance(psf, h5py.File): 739 ↛ 740line 739 didn't jump to line 740 because the condition on line 739 was never true
740 psf_dim = psf_dim[0]
742 baselines_dict = baseline_grid_locations(
743 obs,
744 psf,
745 params,
746 vis_weights,
747 logger,
748 bi_use=bi_use,
749 fi_use=fi_use,
750 mask_mirror_indices=mask_mirror_indices,
751 fill_model_visibilities=fill_model_visibilities,
752 )
753 # Retrieve the data we need from baselines_dict
754 bin_n = baselines_dict["bin_n"]
755 bin_i = baselines_dict["bin_i"]
756 xmin = baselines_dict["xmin"]
757 ymin = baselines_dict["ymin"]
758 ri = baselines_dict["ri"]
759 n_bin_use = baselines_dict["n_bin_use"]
760 # Remove baselines_dict
761 del baselines_dict
763 uniform_filter = np.zeros((elements, dimension))
764 # Get the flat iterations of the xmin and ymin arrays.
765 xmin_iter = xmin.flat
766 ymin_iter = ymin.flat
767 for bi in range(n_bin_use):
768 idx = ri[ri[bin_i[bi]]]
769 # Should all be the same, but don't want an array
770 xmin_use = xmin_iter[idx]
771 ymin_use = ymin_iter[idx]
772 # Please note that due to precision differences, the result will be different compared to IDL FHD
773 uniform_filter[
774 ymin_use : ymin_use + psf_dim, xmin_use : xmin_use + psf_dim
775 ] += bin_n[bin_i[bi]]
777 if not no_conjugate:
778 uniform_filter = (uniform_filter + conjugate_mirror(uniform_filter)) / 2
780 return uniform_filter
783def holo_mapfn_convert(map_fn, psf_dim, dimension, elements=None, norm=1, threshold=0):
784 """
785 Convert pointer array holographic map function to a sparse matrix. May need to be deprecated.
786 The mapping functions were not translated into `visibility_grid` at the time of translation as
787 it wasn't clear what PyFHD was going to use for sparse/large arrays at the time. If you wish to
788 implement the mapping functions, I suggest using HDF5 chunk loading for the mapping function.
790 Parameters
791 ----------
792 map_fn: np.ndarray
793 Pointer array holographic map function
794 psf_dim: int, float
795 The number of pixels in one direction of the psf {u,v} footprint
796 dimension: int
797 The number of pixels in the u-direction
798 elements: None, optional
799 The number of pixels in the v-direction
800 norm: int
801 The normalization of the holographic mapping function (i.e. the number of visibilities)
802 threshold: int, float, optional
803 Include values from the holographic map function which are above a provided threshold
805 Returns
806 -------
807 map_fn: np.recarray
808 Sparse array holographic map function
809 """
810 # Set up all the necessary arrays and numbers
811 if elements is None:
812 elements = dimension
813 psf_dim2 = 2 * psf_dim
814 psf_n = psf_dim**2
815 sub_xv = meshgrid(psf_dim2, 1) - psf_dim
816 sub_yv = meshgrid(psf_dim2, 2) - psf_dim
817 # Generate an array of shape elements x dimension
818 n_arr = np.zeros((elements, dimension))
819 # Replace the values of n_arr with the number from each array in mapfn that exceeds the threshold
820 for xi in range(dimension - 1):
821 for yi in range(elements - 1):
822 temp_arr = map_fn[xi, yi]
823 n1 = np.size(np.where(abs(temp_arr) > threshold))
824 n_arr[xi, yi] = n1
825 # Get the ones to use
826 i_use = np.where(n_arr)
827 # Get the amount we're using
828 i_use_size = np.size(i_use)
829 # If we aren't using any then return 0
830 if i_use_size == 0:
831 return 0
833 # Get the reverse indices
834 _, _, ri = histogram(i_use, min=0)
835 # Create zeros of the same size as what we're using
836 sa = np.zeros(i_use_size)
837 ija = np.zeros(i_use_size)
838 # Fill in the sa and ija arrays
839 for index in range(i_use_size):
840 i = i_use[index]
841 xi = i % dimension
842 yi = np.floor(i / dimension)
843 map_fn_sub = map_fn[xi, yi]
844 j_use = np.where(np.abs(map_fn_sub) > threshold)
846 xii_arr = sub_xv[j_use] + xi
847 yii_arr = sub_yv[j_use] + yi
848 sa[index] = map_fn_sub[j_use]
849 ija[index] = ri[ri[xii_arr + dimension * yii_arr]]
850 # Return as record array
851 mapfn = np.recarray(
852 ija,
853 sa,
854 i_use,
855 norm,
856 1,
857 dtype=[
858 (("ija", "IJA"), "int"),
859 (("sa", "SA"), "int"),
860 (("i_use", "I_USE"), "int"),
861 (("norm", "NORM"), "float"),
862 (("indexed", "INDEXED"), ">i2"),
863 ],
864 )
865 return mapfn
868def crosspol_reformat(image_uv: NDArray[np.complex128]) -> NDArray[np.complex128]:
869 """
870 Reformat the cross-polarizations (i.e. XY and YX) as pseudo Stokes Q and U. This helps to
871 avoid complex numbers in creating images -- however, this is an imperfect assumption.
873 Parameters
874 ----------
875 image_uv : NDArray[np.complex128]
876 A 2D {u,v} plane in four linear polarizations.
878 Returns
879 -------
880 image_uv : NDArray[np.complex128]
881 A 2D {u,v} plane in two linear polarizations, pseudo Stokes Q, and pseudo Stokes U.
882 """
883 # instrumental -> Stokes
884 # Since inverse keyword in FHD isn't used or explained
885 # anywhere else, if you want it, add it as an option to
886 # the pyfhd_config with some help text
887 crosspol_image = (image_uv[2] - conjugate_mirror(image_uv[3])) / 2
888 image_uv[2] = crosspol_image
889 image_uv[3] = conjugate_mirror(crosspol_image)
890 return image_uv