Skip to content

Commit 99bd162

Browse files
committed
First example of defining C++/Python bindings interface
Define ``cpp_image_visibilities``
1 parent 2ea67fa commit 99bd162

File tree

6 files changed

+265
-2
lines changed

6 files changed

+265
-2
lines changed

src/fastimgproto/bindings/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""
2+
Define the Python wrapper-functions which provide an interface to the C++
3+
implementations.
4+
"""

src/fastimgproto/bindings/imager.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import astropy.units as u
2+
import numpy as np
3+
4+
from fastimgproto.gridder.gridder import convolve_to_grid
5+
from fastimgproto.imager import fft_to_image_plane
6+
from fastimgproto.gridder.conv_funcs import GaussianSinc
7+
8+
9+
class CppKernelFuncs(object):
10+
gauss_sinc = 'gauss_sinc'
11+
12+
13+
def cpp_image_visibilities(vis, uvw_lambda,
14+
image_size, cell_size,
15+
kernel_func_name=CppKernelFuncs.gauss_sinc,
16+
kernel_trunc_radius=3.0,
17+
kernel_support=3,
18+
kernel_oversampling=None,
19+
normalize=True):
20+
"""
21+
Convenience wrapper over _cpp_image_visibilities.
22+
23+
Functionality largely mirrors
24+
:func:`fastimgproto.imager.image_visibilities`, but the key difference is
25+
that instead of passing a callable kernel-function, you must choose
26+
``kernel_func_name`` from a limited selection of kernel-functions
27+
implemented in the C++ code. Currently, choices are limited to:
28+
29+
- ``gauss_sinc``
30+
31+
32+
Performs the following tasks before handing over to C++ bindings:
33+
- Checks arguments are of correct type / units
34+
- Converts uvw-array from wavelength (lambda) units to pixel units
35+
36+
Args:
37+
vis (numpy.ndarray): Complex visibilities.
38+
1d array, shape: `(n_vis,)`.
39+
uvw_lambda (numpy.ndarray): UVW-coordinates of visibilities. Units are
40+
multiples of wavelength.
41+
2d array of ``np.float_``, shape: ``(n_vis, 3)``.
42+
Assumed ordering is u,v,w i.e. ``u,v,w = uvw[idx]``
43+
image_size (astropy.units.Quantity): Width of the image in pixels.
44+
e.g. ``1024 * u.pixel``.
45+
NB we assume the pixel ``[image_size//2,image_size//2]``
46+
corresponds to the origin in UV-space.
47+
cell_size (astropy.units.Quantity): Angular-width of a synthesized pixel
48+
in the image to be created, e.g. ``3.5 * u.arcsecond``.
49+
kernel_func_name (str): Choice of kernel function from limited C++ selection.
50+
kernel_trunc_radius (float): Truncation radius of the kernel to be used.
51+
kernel_support (int): Defines the 'radius' of the bounding box within
52+
which convolution takes place. `Box width in pixels = 2*support+1`.
53+
(The central pixel is the one nearest to the UV co-ordinates.)
54+
(This is sometimes known as the 'half-support')
55+
kernel_oversampling (int): (Or None). Controls kernel-generation,
56+
see :func:`fastimgproto.gridder.gridder.convolve_to_grid` for
57+
details.
58+
normalize (bool): Whether or not the returned image and beam
59+
should be normalized such that the beam peaks at a value of
60+
1.0 Jansky. You normally want this to be true, but it may be
61+
interesting to check the raw values for debugging purposes.
62+
63+
Returns:
64+
tuple: (image, beam)
65+
Tuple of ndarrays representing the image map and beam model.
66+
These are 2d arrays of same dtype as ``vis``,
67+
(typically ``np._complex``), shape ``(image_size, image_size)``.
68+
Note numpy style index-order, i.e. access like ``image[y,x]``.
69+
70+
"""
71+
if kernel_func_name not in (CppKernelFuncs.gauss_sinc,):
72+
raise ValueError(
73+
"kernel function of type {} not recognised".format(
74+
kernel_func_name))
75+
76+
image_size = image_size.to(u.pix)
77+
# Size of a UV-grid pixel, in multiples of wavelength (lambda):
78+
grid_pixel_width_lambda = 1.0 / (cell_size.to(u.rad) * image_size)
79+
uvw_in_pixels = (uvw_lambda / grid_pixel_width_lambda).value
80+
uv_in_pixels = uvw_in_pixels[:, :2]
81+
82+
# subroutine = _cpp_image_visibilities
83+
subroutine = _python_image_visibilities
84+
(image, beam) = _python_image_visibilities(
85+
vis=vis,
86+
uv_pixels=uv_in_pixels,
87+
image_size=int(image_size.value),
88+
kernel_func_name=kernel_func_name,
89+
kernel_trunc_radius=kernel_trunc_radius,
90+
kernel_support=kernel_support,
91+
kernel_oversampling=kernel_oversampling,
92+
normalize=normalize,
93+
)
94+
95+
return image, beam
96+
97+
98+
def _cpp_image_visibilities(vis,
99+
uv_pixels,
100+
image_size,
101+
kernel_func_name,
102+
kernel_trunc_radius,
103+
kernel_support,
104+
kernel_oversampling,
105+
normalize=True
106+
):
107+
pass
108+
# C++ Bindings here
109+
110+
111+
def _python_image_visibilities(vis,
112+
uv_pixels,
113+
image_size,
114+
kernel_func_name,
115+
kernel_trunc_radius,
116+
kernel_support,
117+
kernel_oversampling,
118+
normalize=True
119+
):
120+
"""
121+
Equivalent Python code for validation of _cpp_image_visibilities
122+
123+
Args:
124+
vis (numpy.ndarray): Complex visibilities.
125+
1d array, shape: `(n_vis,)`.
126+
uv_pixels (numpy.ndarray): UV-coordinates of visibilities. Units are
127+
pixel-widths relative to the grid being sampled onto.
128+
2d array of ``np.float_``, shape: ``(n_vis, 2)``.
129+
Assumed ordering is u,v i.e. ``u,v = uv[idx]``
130+
image_size (int): Width of the image in pixels.
131+
NB we assume the pixel ``[image_size//2,image_size//2]``
132+
corresponds to the origin in UV-space.
133+
kernel_func_name (str): Choice of kernel function from limited C++ selection.
134+
kernel_trunc_radius (float): Truncation radius of the kernel to be used.
135+
kernel_support (int): Defines the 'radius' of the bounding box within
136+
which convolution takes place. `Box width in pixels = 2*support+1`.
137+
(The central pixel is the one nearest to the UV co-ordinates.)
138+
(This is sometimes known as the 'half-support')
139+
kernel_oversampling (int): (Or None). Controls kernel-generation,
140+
see :func:`fastimgproto.gridder.gridder.convolve_to_grid` for
141+
details.
142+
normalize (bool): Whether or not the returned image and beam
143+
should be normalized such that the beam peaks at a value of
144+
1.0 Jansky. You normally want this to be true, but it may be
145+
interesting to check the raw values for debugging purposes.
146+
147+
Returns:
148+
tuple: (image, beam)
149+
Tuple of ndarrays representing the image map and beam model.
150+
These are 2d arrays of same dtype as ``vis``,
151+
(typically ``np._complex``), shape ``(image_size, image_size)``.
152+
Note numpy style index-order, i.e. access like ``image[y,x]``.
153+
154+
"""
155+
assert kernel_func_name == CppKernelFuncs.gauss_sinc
156+
kernel_func = GaussianSinc(trunc=kernel_trunc_radius)
157+
158+
vis_grid, sample_grid = convolve_to_grid(kernel_func,
159+
support=kernel_support,
160+
image_size=image_size,
161+
uv=uv_pixels,
162+
vis=vis,
163+
oversampling=kernel_oversampling
164+
)
165+
image = fft_to_image_plane(vis_grid)
166+
beam = fft_to_image_plane(sample_grid)
167+
if normalize:
168+
beam_max = np.max(np.real(beam))
169+
beam /= beam_max
170+
image /= beam_max
171+
172+
return (image, beam)

src/fastimgproto/imager.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,57 @@ def image_visibilities(vis, uvw_lambda,
1212
kernel_func, kernel_support,
1313
kernel_oversampling,
1414
normalize=True):
15+
"""
16+
Args:
17+
vis (numpy.ndarray): Complex visibilities.
18+
1d array, shape: `(n_vis,)`.
19+
uvw_lambda (numpy.ndarray): UVW-coordinates of visibilities. Units are
20+
multiples of wavelength.
21+
2d array of ``np.float_``, shape: ``(n_vis, 3)``.
22+
Assumed ordering is u,v,w i.e. ``u,v,w = uvw[idx]``
23+
image_size (astropy.units.Quantity): Width of the image in pixels.
24+
e.g. ``1024 * u.pixel``.
25+
NB we assume the pixel ``[image_size//2,image_size//2]``
26+
corresponds to the origin in UV-space.
27+
cell_size (astropy.units.Quantity): Angular-width of a synthesized pixel
28+
in the image to be created, e.g. ``3.5 * u.arcsecond``.
29+
kernel_func (callable): Callable object,
30+
(e.g. :class:`.conv_funcs.Pillbox`,)
31+
that returns a convolution
32+
co-efficient for a given distance in pixel-widths.
33+
kernel_support (int): Defines the 'radius' of the bounding box within
34+
which convolution takes place. `Box width in pixels = 2*support+1`.
35+
(The central pixel is the one nearest to the UV co-ordinates.)
36+
(This is sometimes known as the 'half-support')
37+
kernel_oversampling (int): (Or None). Controls kernel-generation,
38+
see :func:`fastimgproto.gridder.gridder.convolve_to_grid` for
39+
details.
40+
normalize (bool): Whether or not the returned image and beam
41+
should be normalized such that the beam peaks at a value of
42+
1.0 Jansky. You normally want this to be true, but it may be
43+
interesting to check the raw values for debugging purposes.
44+
45+
Returns:
46+
tuple: (image, beam)
47+
Tuple of ndarrays representing the image map and beam model.
48+
These are 2d arrays of same dtype as ``vis``,
49+
(typically ``np._complex``), shape ``(image_size, image_size)``.
50+
Note numpy style index-order, i.e. access like ``image[y,x]``.
51+
"""
1552

1653
image_size = image_size.to(u.pix)
54+
image_size_int = int(image_size.value)
55+
if image_size_int != image_size.value:
56+
raise ValueError("Please supply an integer-valued image size")
57+
1758
# Size of a UV-grid pixel, in multiples of wavelength (lambda):
1859
grid_pixel_width_lambda = 1.0 / (cell_size.to(u.rad) * image_size)
1960
uvw_in_pixels = (uvw_lambda / grid_pixel_width_lambda).value
2061

2162
uv_in_pixels = uvw_in_pixels[:, :2]
2263
vis_grid, sample_grid = convolve_to_grid(kernel_func,
2364
support=kernel_support,
24-
image_size=int(image_size.value),
65+
image_size=image_size_int,
2566
uv=uv_in_pixels,
2667
vis=vis,
2768
oversampling=kernel_oversampling

tests/test_cpp_bindings/__init__.py

Whitespace-only changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import numpy as np
2+
import astropy.units as u
3+
from fastimgproto.fixtures.data import simple_vis_npz_filepath
4+
from fastimgproto.imager import image_visibilities
5+
from fastimgproto.bindings.imager import cpp_image_visibilities, CppKernelFuncs
6+
from fastimgproto.gridder.conv_funcs import GaussianSinc
7+
8+
9+
def test_cpp_imager_results_parity():
10+
with open(simple_vis_npz_filepath, 'rb') as f:
11+
npz_data_dict = np.load(f)
12+
uvw_lambda = npz_data_dict['uvw_lambda']
13+
vis = npz_data_dict['vis']
14+
15+
trunc = 3.0
16+
support = 3
17+
18+
kernel_func = GaussianSinc(trunc=trunc)
19+
kernel_func_name = CppKernelFuncs.gauss_sinc
20+
21+
image_size = 1024 * u.pix
22+
cell_size = 3. * u.arcsec
23+
py_img, py_beam = image_visibilities(
24+
vis=vis,
25+
uvw_lambda=uvw_lambda,
26+
image_size= image_size,
27+
cell_size=cell_size,
28+
kernel_func = kernel_func,
29+
kernel_support=support,
30+
kernel_oversampling=None,
31+
normalize=True
32+
)
33+
34+
cpp_img, cpp_beam = cpp_image_visibilities(
35+
vis=vis,
36+
uvw_lambda=uvw_lambda,
37+
image_size=image_size,
38+
cell_size=cell_size,
39+
kernel_func_name=kernel_func_name,
40+
kernel_trunc_radius=trunc,
41+
kernel_support=support,
42+
kernel_oversampling=None,
43+
normalize=True
44+
)
45+
46+
assert (np.abs(cpp_img - py_img)< 1E-5).all()

tests/test_gridder/test_oversampled_gridding.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def test_fractional_coord_to_oversampled_index_math():
2727
assert (calculate_oversampled_kernel_indices(
2828
subpixel_coord=subpix_offset, oversampling=oversampling) == 3).all()
2929

30-
# OK, now demonstrate values with oversampling of 0.5, which has easy
30+
# OK, now demonstrate values with oversampling of 5, which has easy
3131
# numbers to calculate since 1/5 = 0.2
3232
oversampling = 5
3333
io_pairs = np.array([

0 commit comments

Comments
 (0)