fftvis 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fftvis/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from . import beams, utils, simulate
fftvis/beams.py ADDED
@@ -0,0 +1,75 @@
1
+ import numpy as np
2
+ from pyuvdata import UVBeam
3
+
4
+
5
+ def _evaluate_beam(
6
+ A_s: np.ndarray,
7
+ beam: UVBeam,
8
+ az: np.ndarray,
9
+ za: np.ndarray,
10
+ polarized: bool,
11
+ freq: float,
12
+ check: bool = False,
13
+ spline_opts: dict = None,
14
+ ):
15
+ """Evaluate the beam on the CPU. Simplified version of the `_evaluate_beam_cpu` function
16
+ in matvis.
17
+
18
+ This function will either interpolate the beam to the given coordinates tx, ty,
19
+ or evaluate the beam there if it is an analytic beam.
20
+
21
+ Parameters
22
+ ----------
23
+ A_s
24
+ Array of shape (nax, nfeed, nsrcs_up) that will be filled with beam
25
+ values.
26
+ beam
27
+ UVBeam object to evaluate.
28
+ tx, ty
29
+ Coordinates to evaluate the beam at, in sin-projection.
30
+ polarized
31
+ Whether to use beam polarization.
32
+ freq
33
+ Frequency to interpolate beam to.
34
+ check
35
+ Whether to check that the beam has no inf/nan values. Set to False if you are
36
+ sure that the beam is valid, as it will be faster.
37
+ spline_opts
38
+ Extra options to pass to the RectBivariateSpline class when interpolating.
39
+ """
40
+ # Primary beam pattern using direct interpolation of UVBeam object
41
+ kw = (
42
+ {
43
+ "reuse_spline": True,
44
+ "check_azza_domain": False,
45
+ "spline_opts": spline_opts,
46
+ }
47
+ if isinstance(beam, UVBeam)
48
+ else {}
49
+ )
50
+ if isinstance(beam, UVBeam) and not beam.future_array_shapes:
51
+ beam.use_future_array_shapes()
52
+
53
+ interp_beam = beam.interp(
54
+ az_array=az,
55
+ za_array=za,
56
+ freq_array=np.atleast_1d(freq),
57
+ **kw,
58
+ )[0]
59
+
60
+ if polarized:
61
+ interp_beam = interp_beam[:, :, 0, :]
62
+ else:
63
+ # Here we have already asserted that the beam is a power beam and
64
+ # has only one polarization, so we just evaluate that one.
65
+ interp_beam = np.sqrt(interp_beam[0, 0, 0, :])
66
+
67
+ A_s[:, :] = interp_beam
68
+
69
+ # Check for invalid beam values
70
+ if check:
71
+ sm = np.sum(A_s)
72
+ if np.isinf(sm) or np.isnan(sm):
73
+ raise ValueError("Beam interpolation resulted in an invalid value")
74
+
75
+ return A_s
fftvis/simulate.py ADDED
@@ -0,0 +1,330 @@
1
+ from __future__ import annotations
2
+
3
+ import finufft
4
+ import numpy as np
5
+ from matvis import conversions
6
+
7
+ from . import utils, beams
8
+
9
+ # Default accuracy for the non-uniform fast fourier transform based on precision
10
+ default_accuracy_dict = {
11
+ 1: 6e-8,
12
+ 2: 1e-13,
13
+ }
14
+
15
+
16
+ def simulate_vis(
17
+ ants: dict,
18
+ fluxes: np.ndarray,
19
+ ra: np.ndarray,
20
+ dec: np.ndarray,
21
+ freqs: np.ndarray,
22
+ lsts: np.ndarray,
23
+ beam,
24
+ baselines: list[tuple] = None,
25
+ precision: int = 2,
26
+ polarized: bool = False,
27
+ latitude: float = -0.5361913261514378,
28
+ eps: float = None,
29
+ use_feed: str = "x",
30
+ ):
31
+ """
32
+ Parameters:
33
+ ----------
34
+ ants : dict
35
+ Dictionary of antenna positions
36
+ fluxes : np.ndarray
37
+ Intensity distribution of sources/pixels on the sky, assuming intensity
38
+ (Stokes I) only. The Stokes I intensity will be split equally between
39
+ the two linear polarization channels, resulting in a factor of 0.5 from
40
+ the value inputted here. This is done even if only one polarization
41
+ channel is simulated.
42
+ ra, dec : array_like
43
+ Arrays of source RA and Dec positions in radians. RA goes from [0, 2 pi]
44
+ and Dec from [-pi, +pi].
45
+ freqs : np.ndarray
46
+ Frequencies to evaluate visibilities in Hz.
47
+ lsts : np.ndarray
48
+ Local sidereal time in radians. Range is [0, 2 pi].
49
+ beam : UVBeam
50
+ Beam object to use for the array. Per-antenna beams are not yet supported.
51
+ baselines : list of tuples, default = None
52
+ If provided, only the baselines within the list will be simulated and array of shape
53
+ (nbls, nfreqs, ntimes) will be returned if polarized is False, and (nbls, nfreqs, ntimes, 2, 2) if polarized is True.
54
+ precision : int, optional
55
+ Which precision level to use for floats and complex numbers
56
+ Allowed values:
57
+ - 1: float32, complex64
58
+ - 2: float64, complex128
59
+ polarized : bool, optional
60
+ Whether to simulate polarized visibilities. If True, the output will have
61
+ shape (nfreqs, ntimes, 2, 2, nants, nants), and if False, the output will
62
+ have shape (nfreqs, ntimes, nants, nants).
63
+ latitude : float, optional
64
+ Latitude of the array in radians. The default is the
65
+ HERA latitude = -30.7215 * pi / 180.
66
+ eps : float, default = None
67
+ Desired accuracy of the non-uniform fast fourier transform. If None, the default accuracy
68
+ for the given precision will be used. For precision 1, the default accuracy is 6e-8, and for
69
+ precision 2, the default accuracy is 1e-12.
70
+
71
+ Returns:
72
+ -------
73
+ vis : np.ndarray
74
+ Array of shape (nfreqs, ntimes, nants, nants) if polarized is False, and
75
+ (nfreqs, ntimes, 2, 2, nants, nants) if polarized is True.
76
+ """
77
+ # Get the accuracy for the given precision if not provided
78
+ if eps is None:
79
+ eps = default_accuracy_dict[precision]
80
+
81
+ # Source coordinate transform, from equatorial to Cartesian
82
+ crd_eq = conversions.point_source_crd_eq(ra, dec)
83
+
84
+ # Make sure antpos has the right format
85
+ ants = {k: np.array(v) for k, v in ants.items()}
86
+
87
+ # Get coordinate transforms as a function of LST
88
+ eq2tops = np.array([conversions.eci_to_enu_matrix(lst, latitude) for lst in lsts])
89
+
90
+ return simulate(
91
+ ants=ants,
92
+ freqs=freqs,
93
+ fluxes=fluxes,
94
+ beam=beam,
95
+ crd_eq=crd_eq,
96
+ eq2tops=eq2tops,
97
+ baselines=baselines,
98
+ precision=precision,
99
+ polarized=polarized,
100
+ eps=eps,
101
+ use_feed=use_feed,
102
+ )
103
+
104
+
105
+ def simulate(
106
+ ants: dict,
107
+ freqs: np.ndarray,
108
+ fluxes: np.ndarray,
109
+ beam,
110
+ crd_eq: np.ndarray,
111
+ eq2tops: np.ndarray,
112
+ baselines: list[tuple] = None,
113
+ precision: int = 2,
114
+ polarized: bool = False,
115
+ eps: float = 1e-13,
116
+ use_feed: str = "x",
117
+ ):
118
+ """
119
+ Parameters:
120
+ ----------
121
+ ants : dict
122
+ Dictionary of antenna positions in the form {ant_index: np.array([x,y,z])}.
123
+ freqs : np.ndarray
124
+ Frequencies to evaluate visibilities at in Hz.
125
+ fluxes : np.ndarray
126
+ Intensity distribution of sources/pixels on the sky, assuming intensity
127
+ (Stokes I) only. The Stokes I intensity will be split equally between
128
+ the two linear polarization channels, resulting in a factor of 0.5 from
129
+ the value inputted here. This is done even if only one polarization
130
+ channel is simulated.
131
+ beam : UVBeam
132
+ Beam object to use for the array. Per-antenna beams are not yet supported.
133
+ crd_eq : np.ndarray
134
+ Cartesian unit vectors of sources in an ECI (Earth Centered
135
+ Inertial) system, which has the Earth's center of mass at
136
+ the origin, and is fixed with respect to the distant stars.
137
+ The components of the ECI vector for each source are:
138
+ (cos(RA) cos(Dec), sin(RA) cos(Dec), sin(Dec)).
139
+ Shape=(3, NSRCS).
140
+ eq2tops : np.ndarray
141
+ Set of 3x3 transformation matrices to rotate the RA and Dec
142
+ cosines in an ECI coordinate system (see `crd_eq`) to
143
+ topocentric ENU (East-North-Up) unit vectors at each
144
+ time/LST/hour angle in the dataset. Shape=(NTIMES, 3, 3).
145
+ baselines : list of tuples, default = None
146
+ If provided, only the baselines within the list will be simulated and array of shape
147
+ (nbls, nfreqs, ntimes) will be returned
148
+ precision : int, optional
149
+ Which precision level to use for floats and complex numbers
150
+ Allowed values:
151
+ - 1: float32, complex64
152
+ - 2: float64, complex128
153
+ eps : float, default = 6e-8
154
+ Desired accuracy of the non-uniform fast fourier transform.
155
+
156
+
157
+ Returns:
158
+ -------
159
+ vis : np.ndarray
160
+ Array of shape (nfreqs, ntimes, nants, nants) if polarized is False, and
161
+ (nfreqs, ntimes, 2, 2, nants, nants) if polarized is True.
162
+ """
163
+ # Get sizes of inputs
164
+ nfreqs = np.size(freqs)
165
+ nants = len(ants)
166
+ ntimes = len(eq2tops)
167
+
168
+ if polarized:
169
+ nax = nfeeds = 2
170
+ else:
171
+ nax = nfeeds = 1
172
+
173
+ if precision == 1:
174
+ real_dtype = np.float32
175
+ complex_dtype = np.complex64
176
+ else:
177
+ real_dtype = np.float64
178
+ complex_dtype = np.complex128
179
+
180
+ # Get the redundant groups - TODO handle this better
181
+ if not baselines:
182
+ reds = utils.get_pos_reds(ants, include_autos=True)
183
+ baselines = [red[0] for red in reds]
184
+ nbls = len(baselines)
185
+ bl_to_red_map = {red[0]: np.array(red) for red in reds}
186
+ expand_vis = True
187
+ else:
188
+ nbls = len(baselines)
189
+ expand_vis = False
190
+
191
+ # Prepare the beam
192
+ beam = conversions.prepare_beam(beam, polarized=polarized, use_feed=use_feed)
193
+
194
+ # Check if the beam is complex
195
+ beam_values, _ = beam.interp(
196
+ az_array=np.array([0]),
197
+ za_array=np.array([0]),
198
+ freq_array=np.array([freqs[0]]),
199
+ )
200
+ is_beam_complex = np.issubdtype(beam_values.dtype, np.complexfloating)
201
+
202
+ # Convert to correct precision
203
+ crd_eq = crd_eq.astype(real_dtype)
204
+ eq2tops = eq2tops.astype(real_dtype)
205
+
206
+ # Factor of 0.5 accounts for splitting Stokes between polarization channels
207
+ Isky = (0.5 * fluxes).astype(complex_dtype)
208
+
209
+ # Compute baseline vectors
210
+ blx, bly = np.array([ants[bl[1]] - ants[bl[0]] for bl in baselines])[
211
+ :, :2
212
+ ].T.astype(real_dtype)
213
+
214
+ # Generate visibility array
215
+ if expand_vis:
216
+ vis = np.zeros(
217
+ (ntimes, nants, nants, nfeeds, nfeeds, nfreqs), dtype=complex_dtype
218
+ )
219
+ else:
220
+ vis = np.zeros((ntimes, nbls, nfeeds, nfeeds, nfreqs), dtype=complex_dtype)
221
+
222
+ # Loop over time samples
223
+ for ti, eq2top in enumerate(eq2tops):
224
+ # Convert to topocentric coordinates
225
+ tx, ty, tz = np.dot(eq2top, crd_eq)
226
+
227
+ # Only simulate above the horizon
228
+ above_horizon = tz > 0
229
+ tx = tx[above_horizon]
230
+ ty = ty[above_horizon]
231
+
232
+ # Number of above horizon points
233
+ nsim_sources = above_horizon.sum()
234
+
235
+ # Form the visibility array
236
+ _vis = np.zeros((nfeeds, nfeeds, nbls, nfreqs), dtype=complex_dtype)
237
+
238
+ if is_beam_complex:
239
+ _vis_negatives = np.zeros(
240
+ (nfeeds, nfeeds, nbls, nfreqs), dtype=complex_dtype
241
+ )
242
+
243
+ # Compute azimuth and zenith angles
244
+ az, za = conversions.enu_to_az_za(enu_e=tx, enu_n=ty, orientation="uvbeam")
245
+
246
+ for fi in range(nfreqs):
247
+ # Compute uv coordinates
248
+ u, v = (
249
+ blx * freqs[fi] / utils.speed_of_light,
250
+ bly * freqs[fi] / utils.speed_of_light,
251
+ )
252
+
253
+ # Compute beams - only single beam is supported
254
+ A_s = np.zeros((nax, nfeeds, nsim_sources), dtype=complex_dtype)
255
+ A_s = beams._evaluate_beam(A_s, beam, az, za, polarized, freqs[fi])
256
+ A_s = A_s.transpose((1, 0, 2))
257
+ beam_product = np.einsum("abs,cbs->acs", A_s.conj(), A_s)
258
+ beam_product = beam_product.reshape(nax * nfeeds, nsim_sources)
259
+
260
+ # Compute sky beam product
261
+ i_sky = beam_product * Isky[above_horizon, fi]
262
+
263
+ # Compute visibilities w/ non-uniform FFT
264
+ _vis_here = finufft.nufft2d3(
265
+ 2 * np.pi * tx,
266
+ 2 * np.pi * ty,
267
+ i_sky,
268
+ u,
269
+ v,
270
+ modeord=0,
271
+ eps=eps,
272
+ )
273
+
274
+ # Expand out the visibility array
275
+ _vis[..., fi] = _vis_here.reshape(nfeeds, nfeeds, nbls)
276
+
277
+ # If beam is complex, we need to compute the reverse negative frequencies
278
+ # TODO: no way to store this in the loop
279
+ if is_beam_complex:
280
+ # Compute
281
+ _vis_here_neg = finufft.nufft2d3(
282
+ 2 * np.pi * tx,
283
+ 2 * np.pi * ty,
284
+ i_sky,
285
+ -u,
286
+ -v,
287
+ modeord=0,
288
+ eps=eps,
289
+ )
290
+ _vis_negatives[..., fi] = _vis_here_neg.reshape(nfeeds, nfeeds, nbls)
291
+
292
+ # Expand out the visibility array in antenna by antenna matrix
293
+ if expand_vis:
294
+ for bi, bls in enumerate(baselines):
295
+ np.add.at(
296
+ vis,
297
+ (ti, bl_to_red_map[bls][:, 0], bl_to_red_map[bls][:, 1]),
298
+ _vis[..., bi, :],
299
+ )
300
+
301
+ # Add the conjugate, avoid auto baselines twice
302
+ if bls[0] != bls[1]:
303
+ if is_beam_complex:
304
+ np.add.at(
305
+ vis,
306
+ (ti, bl_to_red_map[bls][:, 1], bl_to_red_map[bls][:, 0]),
307
+ _vis_negatives[..., bi, :],
308
+ )
309
+ else:
310
+ np.add.at(
311
+ vis,
312
+ (ti, bl_to_red_map[bls][:, 1], bl_to_red_map[bls][:, 0]),
313
+ _vis[..., bi, :].conj(),
314
+ )
315
+
316
+ else:
317
+ vis[ti] = np.swapaxes(_vis, 2, 0)
318
+
319
+ if expand_vis:
320
+ return (
321
+ np.transpose(vis, (5, 0, 3, 4, 1, 2))
322
+ if polarized
323
+ else np.moveaxis(vis[..., 0, 0, :], 3, 0)
324
+ )
325
+ else:
326
+ return (
327
+ np.transpose(vis, (4, 0, 2, 3, 1))
328
+ if polarized
329
+ else np.moveaxis(vis[..., 0, 0, :], 2, 0)
330
+ )
@@ -0,0 +1,165 @@
1
+ """Tests."""
2
+
3
+ import numpy as np
4
+ from astropy import units as un
5
+ from astropy.coordinates import EarthLocation, Latitude, Longitude
6
+ from astropy.time import Time
7
+ from astropy.units import Quantity
8
+ from pathlib import Path
9
+ from pyradiosky import SkyModel
10
+ from pyuvdata import UVBeam
11
+ from pyuvsim import AnalyticBeam, simsetup
12
+ from pyuvsim.telescope import BeamList
13
+
14
+ from matvis import DATA_PATH, conversions
15
+
16
+ nfreq = 1
17
+ ntime = 5 # 20
18
+ nants = 3 # 4
19
+ nsource = 15 # 250
20
+ beam_file = DATA_PATH / "NF_HERA_Dipole_small.fits"
21
+
22
+
23
+ def get_standard_sim_params(
24
+ use_analytic_beam: bool,
25
+ polarized: bool,
26
+ nants=nants,
27
+ nfreq=nfreq,
28
+ ntime=ntime,
29
+ nsource=nsource,
30
+ first_source_antizenith=False,
31
+ ):
32
+ """Create some standard random simulation parameters for use in tests."""
33
+ hera_lat = -30.7215
34
+ hera_lon = 21.4283
35
+ hera_alt = 1073.0
36
+ obstime = Time("2018-08-31T04:02:30.11", format="isot", scale="utc")
37
+
38
+ # HERA location
39
+ location = EarthLocation.from_geodetic(lat=hera_lat, lon=hera_lon, height=hera_alt)
40
+
41
+ np.random.seed(1)
42
+
43
+ # Beam model
44
+ if use_analytic_beam:
45
+ n_freq = nfreq
46
+ beam = AnalyticBeam("gaussian", diameter=14.0)
47
+ else:
48
+ n_freq = min(nfreq, 2)
49
+ # This is a peak-normalized e-field beam file at 100 and 101 MHz,
50
+ # downsampled to roughly 4 square-degree resolution.
51
+ beam = UVBeam()
52
+ beam.read_beamfits(beam_file)
53
+ if not polarized:
54
+ uvsim_beam = beam.copy()
55
+ beam.efield_to_power(calc_cross_pols=False, inplace=True)
56
+ beam.select(polarizations=["xx"], inplace=True)
57
+
58
+ # Now, the beam we have on file doesn't actually properly wrap around in azimuth.
59
+ # This is fine -- the uvbeam.interp() handles the interpolation well. However, when
60
+ # comparing to the GPU interpolation, which first has to interpolate to a regular
61
+ # grid that ends right at 2pi, it's better to compare like for like, so we
62
+ # interpolate to such a grid here.
63
+ beam = beam.interp(
64
+ az_array=np.linspace(0, 2 * np.pi, 181),
65
+ za_array=np.linspace(0, np.pi / 2, 46),
66
+ az_za_grid=True,
67
+ new_object=True,
68
+ )
69
+
70
+ if polarized or use_analytic_beam:
71
+ uvsim_beams = BeamList([beam] * nants)
72
+ else:
73
+ uvsim_beams = BeamList([uvsim_beam] * nants)
74
+
75
+ # beams = [AnalyticBeam('uniform') for i in range(len(ants.keys()))]
76
+ beam_dict = {str(i): i for i in range(nants)}
77
+
78
+ # Random antenna locations
79
+ x = np.random.random(nants) * 400.0 # Up to 400 metres
80
+ y = np.random.random(nants) * 400.0
81
+ z = np.random.random(nants) * 0.0
82
+ ants = {i: (x[i], y[i], z[i]) for i in range(nants)}
83
+
84
+ # Observing parameters in a UVData object
85
+ uvdata = simsetup.initialize_uvdata_from_keywords(
86
+ Nfreqs=n_freq,
87
+ start_freq=100e6,
88
+ channel_width=97.3e3,
89
+ start_time=obstime.jd,
90
+ integration_time=182.0, # Just over 3 mins between time samples
91
+ Ntimes=ntime,
92
+ array_layout=ants,
93
+ polarization_array=np.array(["XX", "YY", "XY", "YX"]),
94
+ telescope_location=(hera_lat, hera_lon, hera_alt),
95
+ telescope_name="test_array",
96
+ phase_type="drift",
97
+ vis_units="Jy",
98
+ complete=True,
99
+ write_files=False,
100
+ )
101
+ lsts = np.unique(uvdata.lst_array)
102
+
103
+ # One fixed source plus random other sources
104
+ sources = [
105
+ [
106
+ 300 if first_source_antizenith else 125.7,
107
+ -30.72,
108
+ 2,
109
+ 0,
110
+ ], # Fix a single source near zenith
111
+ ]
112
+ if nsource > 1: # Add random other sources
113
+ ra = np.random.uniform(low=0.0, high=360.0, size=nsource - 1)
114
+ dec = -30.72 + np.random.random(nsource - 1) * 10.0
115
+ flux = np.random.random(nsource - 1) * 4
116
+ sources.extend([ra[i], dec[i], flux[i], 0] for i in range(nsource - 1))
117
+ sources = np.array(sources)
118
+
119
+ # Source locations and frequencies
120
+ ra_dec = np.deg2rad(sources[:, :2])
121
+ freqs = np.unique(uvdata.freq_array)
122
+
123
+ # Correct source locations so that matvis uses the right frame
124
+ ra_new, dec_new = conversions.equatorial_to_eci_coords(
125
+ ra_dec[:, 0], ra_dec[:, 1], obstime, location, unit="rad", frame="icrs"
126
+ )
127
+
128
+ # Calculate source fluxes for matvis
129
+ flux = ((freqs[:, np.newaxis] / freqs[0]) ** sources[:, 3].T * sources[:, 2].T).T
130
+
131
+ # Stokes for the first frequency only. Stokes for other frequencies
132
+ # are calculated later.
133
+ stokes = np.zeros((4, 1, ra_dec.shape[0]))
134
+ stokes[0, 0] = sources[:, 2]
135
+ reference_frequency = np.full(len(ra_dec), freqs[0])
136
+
137
+ # Set up sky model
138
+ sky_model = SkyModel(
139
+ name=[str(i) for i in range(len(ra_dec))],
140
+ ra=Longitude(ra_dec[:, 0], "rad"),
141
+ dec=Latitude(ra_dec[:, 1], "rad"),
142
+ frame="icrs",
143
+ spectral_type="spectral_index",
144
+ spectral_index=sources[:, 3],
145
+ stokes=stokes * un.Jy,
146
+ reference_frequency=Quantity(reference_frequency, "Hz"),
147
+ )
148
+
149
+ # Calculate stokes at all the frequencies.
150
+ sky_model.at_frequencies(Quantity(freqs, "Hz"), inplace=True)
151
+
152
+ return (
153
+ sky_model,
154
+ ants,
155
+ flux,
156
+ ra_new,
157
+ dec_new,
158
+ freqs,
159
+ lsts,
160
+ [beam],
161
+ uvsim_beams,
162
+ beam_dict,
163
+ hera_lat,
164
+ uvdata,
165
+ )
@@ -0,0 +1,122 @@
1
+ import matvis
2
+ import pytest
3
+ import numpy as np
4
+ from fftvis import simulate
5
+ from pyuvsim.analyticbeam import AnalyticBeam
6
+
7
+
8
+ def test_simulate():
9
+ """ """
10
+ # Simulation parameters
11
+ ntimes = 10
12
+ nfreqs = 5
13
+ nants = 3
14
+ nsrcs = 20
15
+
16
+ # Set random set
17
+ np.random.seed(42)
18
+
19
+ # Define frequency and time range
20
+ freqs = np.linspace(100e6, 200e6, nfreqs)
21
+ lsts = np.linspace(0, np.pi, ntimes)
22
+
23
+ # Set up the antenna positions
24
+ antpos = {k: np.array([k * 10, 0, 0]) for k in range(nants)}
25
+
26
+ # Define a Gaussian beam
27
+ beam = AnalyticBeam("gaussian", diameter=14.0)
28
+
29
+ # Set sky model
30
+ ra = np.linspace(0.0, 2.0 * np.pi, nsrcs)
31
+ dec = np.linspace(-0.5 * np.pi, 0.5 * np.pi, nsrcs)
32
+ sky_model = np.random.uniform(0, 1, size=(nsrcs, 1)) * (freqs[None] / 150e6) ** -2.5
33
+
34
+ # Use matvis as a reference
35
+ mvis = matvis.simulate_vis(
36
+ antpos, sky_model, ra, dec, freqs, lsts, beams=[beam], precision=2
37
+ )
38
+
39
+ # Use fftvis to simulate visibilities
40
+ fvis = simulate.simulate_vis(
41
+ antpos, sky_model, ra, dec, freqs, lsts, beam, precision=2, eps=1e-10
42
+ )
43
+
44
+ # Should have shape (nfreqs, ntimes, nants, nants)
45
+ assert fvis.shape == (nfreqs, ntimes, nants, nants)
46
+
47
+ # Check that the results are the same
48
+ assert np.allclose(mvis, fvis, atol=1e-5)
49
+
50
+ # Test polarized visibilities
51
+ # Use matvis as a reference
52
+ mvis = matvis.simulate_vis(
53
+ antpos,
54
+ sky_model,
55
+ ra,
56
+ dec,
57
+ freqs,
58
+ lsts,
59
+ beams=[beam],
60
+ precision=2,
61
+ polarized=True,
62
+ )
63
+
64
+ # Use fftvis to simulate visibilities
65
+ fvis = simulate.simulate_vis(
66
+ antpos,
67
+ sky_model,
68
+ ra,
69
+ dec,
70
+ freqs,
71
+ lsts,
72
+ beam,
73
+ precision=2,
74
+ eps=1e-10,
75
+ polarized=True,
76
+ )
77
+
78
+ # Should have shape (nfreqs, ntimes, nfeeds, nfeeds, nants, nants)
79
+ assert fvis.shape == (nfreqs, ntimes, 2, 2, nants, nants)
80
+
81
+ # Check that the polarized results are the same
82
+ assert np.allclose(mvis, fvis, atol=1e-5)
83
+
84
+ # Simulate with specified baselines
85
+ sim_baselines = [(0, 1), (0, 2), (1, 2)]
86
+ fvis = simulate.simulate_vis(
87
+ antpos,
88
+ sky_model,
89
+ ra,
90
+ dec,
91
+ freqs,
92
+ lsts,
93
+ beam,
94
+ baselines=sim_baselines,
95
+ precision=2,
96
+ eps=1e-10,
97
+ polarized=True,
98
+ )
99
+
100
+ # Should have shape (nfreqs, ntimes, 2, 2, len(sim_baselines))
101
+ assert fvis.shape == (nfreqs, ntimes, 2, 2, len(sim_baselines))
102
+
103
+ # Check that the polarized results are the same
104
+ for bi, bl in enumerate(sim_baselines):
105
+ assert np.allclose(fvis[:, :, :, :, bi], mvis[:, :, :, :, bl[0], bl[1]])
106
+
107
+ # Test with precision 1
108
+ # Use matvis as a reference
109
+ mvis = matvis.simulate_vis(
110
+ antpos, sky_model, ra, dec, freqs, lsts, beams=[beam], precision=1
111
+ )
112
+
113
+ # Use fftvis to simulate visibilities
114
+ fvis = simulate.simulate_vis(
115
+ antpos, sky_model, ra, dec, freqs, lsts, beam, precision=1, eps=6e-8
116
+ )
117
+
118
+ # Should have shape (nfreqs, ntimes, nants, nants)
119
+ assert fvis.shape == (nfreqs, ntimes, nants, nants)
120
+
121
+ # Check that the results are the same
122
+ assert np.allclose(mvis, fvis, atol=1e-5)
@@ -0,0 +1,129 @@
1
+ """
2
+
3
+ Compare matvis with pyuvsim visibilities.
4
+
5
+ Tests copied from matvis/tests/test_compare_pyuvsim.py
6
+
7
+ """
8
+
9
+ import pytest
10
+
11
+ import numpy as np
12
+ from pyuvsim import simsetup, uvsim
13
+
14
+ from fftvis.simulate import simulate_vis
15
+ from matvis import simulate_vis as simulate_vis_matvis
16
+
17
+ from . import get_standard_sim_params, nants
18
+
19
+
20
+ @pytest.mark.parametrize("use_analytic_beam", (True, False))
21
+ @pytest.mark.parametrize("polarized", (True, False))
22
+ def test_compare_pyuvsim(polarized, use_analytic_beam):
23
+ """Compare matvis and pyuvsim simulated visibilities."""
24
+ print("Polarized=", polarized, "Analytic Beam =", use_analytic_beam)
25
+ (
26
+ sky_model,
27
+ ants,
28
+ flux,
29
+ ra,
30
+ dec,
31
+ freqs,
32
+ lsts,
33
+ cpu_beams,
34
+ uvsim_beams,
35
+ beam_dict,
36
+ hera_lat,
37
+ uvdata,
38
+ ) = get_standard_sim_params(use_analytic_beam, polarized)
39
+ # ---------------------------------------------------------------------------
40
+ # (1) Run matvis
41
+ # ---------------------------------------------------------------------------
42
+ vis_fftvis = simulate_vis(
43
+ ants=ants,
44
+ fluxes=flux,
45
+ ra=ra,
46
+ dec=dec,
47
+ freqs=freqs,
48
+ lsts=lsts,
49
+ beam=cpu_beams[0],
50
+ polarized=polarized,
51
+ precision=2,
52
+ latitude=hera_lat * np.pi / 180.0,
53
+ )
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # (2) Run pyuvsim
57
+ # ---------------------------------------------------------------------------
58
+ # uvd_uvsim = uvsim.run_uvdata_uvsim(
59
+ # uvdata,
60
+ # uvsim_beams,
61
+ # beam_dict=beam_dict,
62
+ # catalog=simsetup.SkyModelData(sky_model),
63
+ # )
64
+ vis_matvis = simulate_vis_matvis(
65
+ ants=ants,
66
+ fluxes=flux,
67
+ ra=ra,
68
+ dec=dec,
69
+ freqs=freqs,
70
+ lsts=lsts,
71
+ beams=cpu_beams,
72
+ polarized=polarized,
73
+ precision=2,
74
+ latitude=hera_lat * np.pi / 180.0,
75
+ )
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Compare
79
+ # ---------------------------------------------------------------------------
80
+ # Loop over baselines and compare
81
+ diff_re = 0.0
82
+ diff_im = 0.0
83
+ rtol = 2e-4 if use_analytic_beam else 0.01
84
+ atol = 5e-4
85
+
86
+ # If it passes this test, but fails the following tests, then its probably an
87
+ # ordering issue.
88
+ for i in range(nants):
89
+ for j in range(i, nants):
90
+ for if1, feed1 in enumerate(("X", "Y") if polarized else ("X",)):
91
+ for if2, feed2 in enumerate(("X", "Y") if polarized else ("X",)):
92
+ # d_uvsim = uvd_uvsim.get_data(
93
+ # (i, j, feed1 + feed2)
94
+ # ).T # pyuvsim visibility
95
+ d_uvsim = (
96
+ vis_matvis[:, :, if1, if2, i, j]
97
+ if polarized
98
+ else vis_fftvis[:, :, i, j]
99
+ )
100
+ d_fftvis = (
101
+ vis_fftvis[:, :, if1, if2, i, j]
102
+ if polarized
103
+ else vis_fftvis[:, :, i, j]
104
+ )
105
+
106
+ # Keep track of maximum difference
107
+ delta = d_uvsim - d_fftvis
108
+ if np.max(np.abs(delta.real)) > diff_re:
109
+ diff_re = np.max(np.abs(delta.real))
110
+ if np.max(np.abs(delta.imag)) > diff_im:
111
+ diff_im = np.abs(np.max(delta.imag))
112
+
113
+ err = f"\nMax diff: {diff_re:10.10e} + 1j*{diff_im:10.10e}\n"
114
+ err += f"Baseline: ({i},{j},{feed1}{feed2})\n"
115
+ err += f"Avg. diff: {delta.mean():10.10e}\n"
116
+ err += f"Max values: \n uvsim={d_uvsim.max():10.10e}"
117
+ err += f"\n matvis={d_fftvis.max():10.10e}"
118
+ assert np.allclose(
119
+ d_uvsim.real,
120
+ d_fftvis.real,
121
+ rtol=rtol if feed1 == feed2 else rtol * 100,
122
+ atol=atol,
123
+ ), err
124
+ assert np.allclose(
125
+ d_uvsim.imag,
126
+ d_fftvis.imag,
127
+ rtol=rtol if feed1 == feed2 else rtol * 100,
128
+ atol=atol,
129
+ ), err
@@ -0,0 +1,6 @@
1
+ import pytest
2
+
3
+
4
+ def test_get_pos_reds():
5
+ """ """
6
+ pass
fftvis/utils.py ADDED
@@ -0,0 +1,57 @@
1
+ import numpy as np
2
+
3
+ IDEALIZED_BL_TOL = 1e-8 # bl_error_tol for redcal.get_reds when using antenna positions calculated from reds
4
+ speed_of_light = 299792458.0 # m/s
5
+
6
+
7
+ def get_pos_reds(antpos, decimals=3, include_autos=True):
8
+ """
9
+ Figure out and return list of lists of redundant baseline pairs. This function is a modified version of the
10
+ get_pos_reds function in redcal. It is used to calculate the redundant baseline groups from antenna positions
11
+ rather than from a list of baselines. This is useful for simulating visibilities with fftvis.
12
+
13
+ Parameters:
14
+ ----------
15
+ antpos: dict
16
+ dictionary of antenna positions in the form {ant_index: np.array([x,y,z])}.
17
+ decimals: int, optional
18
+ Number of decimal places to round to when determining redundant baselines. default is 3.
19
+ include_autos: bool, optional
20
+ if True, include autos in the list of pos_reds. default is False
21
+ Returns:
22
+ -------
23
+ reds: list of lists of redundant tuples of antenna indices (no polarizations),
24
+ sorted by index with the first index of the first baseline the lowest in the group.
25
+ """
26
+ # Create a dictionary of redundant baseline groups
27
+ uv_to_red_key = {}
28
+ reds = {}
29
+
30
+ # Compute baseline lengths and round to specified precision
31
+ baselines = np.round(
32
+ [
33
+ antpos[aj] - antpos[ai]
34
+ for ai in antpos
35
+ for aj in antpos
36
+ if ai < aj or include_autos and ai == aj
37
+ ],
38
+ decimals,
39
+ )
40
+
41
+ ci = 0
42
+ for ai in antpos:
43
+ for aj in antpos:
44
+ if ai < aj or include_autos and ai == aj:
45
+ u, v, _ = baselines[ci]
46
+
47
+ if (u, v) not in uv_to_red_key and (-u, -v) not in uv_to_red_key:
48
+ reds[(ai, aj)] = [(ai, aj)]
49
+ uv_to_red_key[(u, v)] = (ai, aj)
50
+ elif (-u, -v) in uv_to_red_key:
51
+ reds[uv_to_red_key[(-u, -v)]].append((aj, ai))
52
+ elif (u, v) in uv_to_red_key:
53
+ reds[uv_to_red_key[(u, v)]].append((ai, aj))
54
+
55
+ ci += 1
56
+
57
+ return [reds[k] for k in reds]
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.1
2
+ Name: fftvis
3
+ Version: 0.0.1
4
+ Summary: A package for simulating visibilities using FFTs
5
+ Home-page: https://github.com/tyler-a-cox/fftvis
6
+ Author: Tyler Cox
7
+ Author-email: tyler.a.cox@berkeley.edu
8
+ License: MIT
9
+ Platform: any
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.6
16
+ Classifier: Programming Language :: Python :: 3.7
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
22
+ Classifier: Topic :: Scientific/Engineering :: Visualization
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: numpy
26
+ Requires-Dist: matvis
27
+ Requires-Dist: finufft
28
+ Requires-Dist: pyuvdata
29
+ Provides-Extra: dev
30
+ Requires-Dist: mpi4py ; extra == 'dev'
31
+ Requires-Dist: pyuvsim[sim] ; extra == 'dev'
32
+ Requires-Dist: pyradiosky ; extra == 'dev'
33
+ Requires-Dist: pytest ; extra == 'dev'
34
+ Requires-Dist: pre-commit ; extra == 'dev'
35
+ Requires-Dist: pytest-cov ; extra == 'dev'
36
+ Requires-Dist: hera-sim ; extra == 'dev'
37
+ Requires-Dist: pytest-xdist ; extra == 'dev'
38
+
39
+ # fftvis: A Non-Uniform Fast Fourier Transform-based Visibility Simulator
40
+
41
+ ![Tests](https://github.com/tyler-a-cox/fftvis/actions/workflows/ci.yml/badge.svg)
42
+ ![codecov](https://codecov.io/gh/tyler-a-cox/fftvis/branch/main/graph/badge.svg)
43
+ ![Black Formatting](https://img.shields.io/badge/code%20style-black-000000.svg)
44
+
45
+ `fftvis` is a fast Python package designed for simulating interferometric visibilities using the Non-Uniform Fast Fourier Transform (NUFFT). It provides a convenient and efficient way to generate simulated visibilities.
46
+
47
+ ## Features
48
+
49
+ - Utilizes the Flatiron Institute NUFFT (finufft) [algorithm](https://arxiv.org/abs/1808.06736) for fast visibility simulations that agree with similar methods ([`matvis`](https://github.com/HERA-team/matvis)) to nearly machine precision.
50
+ - Designed to be a near drop-in replacement to `matvis` with a ~10x improvement in runtime
51
+
52
+ ## Limitations
53
+ - Currently no support for per-antenna beams
54
+ - Currently no support for polarized sky emission
55
+ - Currently no GPU support
56
+ - Diffuse sky models must be pixelized
57
+
58
+ ## Installation
59
+
60
+ You can install `fftvis` via pip:
61
+
62
+ ```bash
63
+ git clone https://github.com/tyler-a-cox/fftvis
64
+ cd fftvis
65
+ pip install .
66
+ ```
67
+
68
+ ## Contributing
69
+ Contributions to `fftvis` are welcome! If you find any issues, have feature requests, or want to contribute improvements, please open an issue or submit a pull request on the GitHub repository: `fftvis` on GitHub
70
+
71
+ ## License
72
+
73
+ This project is licensed under the MIT License - see the LICENSE file for details.
74
+
75
+ ## Acknowledgments
76
+ This package relies on the `finufft` implementation provided by [finufft](https://github.com/flatironinstitute/finufft) library. Special thanks to the contributors and maintainers of open-source libraries used in this project.
@@ -0,0 +1,12 @@
1
+ fftvis/__init__.py,sha256=8tSRabRYy3YZ8Oiu0yP-KhV2Pti6CNumjqrs4Qll6tw,37
2
+ fftvis/beams.py,sha256=JP6jFyh2ulXCZvFLoiLRQ8GfYMLx0XWNkijqDTyQJU4,2143
3
+ fftvis/simulate.py,sha256=YtyhNCAwq2ysggf2rl2Haur7qRyaU61SqjkVugYM4Oo,11422
4
+ fftvis/utils.py,sha256=4nDW2pm5NCJofsxMB1DlSjyTAhYCUSxfuOmNwD2Bh3g,2159
5
+ fftvis/tests/__init__.py,sha256=Dy1vOmUe-Q4lKQsNGE2vB0YR9n5XJn030IG6V55LCo8,5353
6
+ fftvis/tests/test_compare_matvis.py,sha256=fRc2pvCS_jHV4Mb-5zCia7MMUZ3outlTBkLglDe3joU,3236
7
+ fftvis/tests/test_compare_pyuvsim.py,sha256=N49B_903gjjtoeddGmu5niWNWxNM_heCQQxBITdtW2s,4367
8
+ fftvis/tests/test_utils.py,sha256=gBmBDW-FFqEBOsh_315xALdpLk26RlkEQnmc8pw4hE8,62
9
+ fftvis-0.0.1.dist-info/METADATA,sha256=-dbnCfVKg3oZ2idpm3MYliEYxw_kAkGQ2A2r5nDuOqQ,3177
10
+ fftvis-0.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
11
+ fftvis-0.0.1.dist-info/top_level.txt,sha256=p91SGcGpU_MXL32mier1gBGXt2_nxk5H8hn1MwNki2I,7
12
+ fftvis-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ fftvis