Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proper tilting of input antenna positions when z-component is non-zero #24

Merged
merged 8 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[run]
include = fftvis/*
omit = fftvis/logutils.py
branch = True

[report]
Expand Down
21 changes: 19 additions & 2 deletions fftvis/simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def simulate_vis(
eps: float = None,
use_feed: str = "x",
flat_array_tol: float = 0.0,
live_progress: bool = True,
):
"""
Parameters:
Expand Down Expand Up @@ -80,6 +81,8 @@ def simulate_vis(
Tolerance for checking if the array is flat in units of meters. If the
z-coordinate of all baseline vectors is within this tolerance, the array
is considered flat and the z-coordinate is set to zero. Default is 0.0.
live_progress : bool, default = True
Whether to show progress bar during simulation.

Returns:
-------
Expand Down Expand Up @@ -115,6 +118,7 @@ def simulate_vis(
polarized=polarized,
eps=eps,
flat_array_tol=flat_array_tol,
live_progress=live_progress,
)


Expand Down Expand Up @@ -241,14 +245,24 @@ def simulate(
# Factor of 0.5 accounts for splitting Stokes between polarization channels
Isky = (0.5 * fluxes).astype(complex_dtype)

# Flatten antenna positions
antkey_to_idx = dict(zip(ants.keys(), range(len(ants))))
antvecs = np.array([ants[ant] for ant in ants], dtype=real_dtype)

# Rotate the array to the xy-plane
rotation_matrix = utils.get_plane_to_xy_rotation_matrix(antvecs)
rotation_matrix = rotation_matrix.astype(real_dtype)
rotated_antvecs = np.dot(rotation_matrix.T, antvecs.T)
rotated_ants = {ant: rotated_antvecs[:, antkey_to_idx[ant]] for ant in ants}

# Compute baseline vectors
blx, bly, blz = np.array([ants[bl[1]] - ants[bl[0]] for bl in baselines])[
blx, bly, blz = np.array([rotated_ants[bl[1]] - rotated_ants[bl[0]] for bl in baselines])[
:, :
].T.astype(real_dtype)

# Check if the array is flat within tolerance
is_coplanar = np.all(np.less_equal(np.abs(blz), flat_array_tol))

# Generate visibility array
if expand_vis:
vis = np.zeros(
Expand Down Expand Up @@ -308,6 +322,9 @@ def simulate(
# Compute azimuth and zenith angles
az, za = conversions.enu_to_az_za(enu_e=tx, enu_n=ty, orientation="uvbeam")

# Rotate source coordinates with rotation matrix.
tx, ty, tz = np.dot(rotation_matrix.T, [tx, ty, tz])

for fi in range(nfreqs):
# Compute uv coordinates
u[:], v[:], w[:] = blx * freqs[fi], bly * freqs[fi], blz * freqs[fi]
Expand Down
11 changes: 5 additions & 6 deletions fftvis/tests/test_compare_matvis.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def test_simulate_non_coplanar():

# Set up the antenna positions
antpos = {k: np.array([k * 10, 0, k]) for k in range(nants)}
antpos_flat = {k: np.array([k * 10, 0, 0]) for k in range(nants)}

# Define a Gaussian beam
beam = AnalyticBeam("gaussian", diameter=14.0)
Expand All @@ -151,6 +152,9 @@ def test_simulate_non_coplanar():
mvis = matvis.simulate_vis(
antpos, sky_model, ra, dec, freqs, lsts, beams=[beam], precision=2
)
mvis_flat = matvis.simulate_vis(
antpos_flat, sky_model, ra, dec, freqs, lsts, beams=[beam], precision=2
)

# Use fftvis to simulate visibilities
fvis = simulate.simulate_vis(
Expand All @@ -160,10 +164,5 @@ def test_simulate_non_coplanar():
# Check that the results are the same
assert np.allclose(mvis, fvis, atol=1e-5)

# Use fftvis to simulate visibilities with flat array
fvis_flat = simulate.simulate_vis(
antpos, sky_model, ra, dec, freqs, lsts, beam, precision=2, eps=1e-10, flat_array_tol=np.inf
)

# Check that the results are different
assert not np.allclose(fvis, fvis_flat, atol=1e-5)
assert not np.allclose(mvis_flat, fvis, atol=1e-5)
50 changes: 49 additions & 1 deletion fftvis/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import numpy as np

from scipy import linalg
IDEALIZED_BL_TOL = 1e-8 # bl_error_tol for redcal.get_reds when using antenna positions calculated from reds
speed_of_light = 299792458.0 # m/s

Expand Down Expand Up @@ -65,3 +65,51 @@ def get_pos_reds(antpos, decimals=3, include_autos=True):
reds_list.append(red)

return reds_list


def get_plane_to_xy_rotation_matrix(antvecs):
"""
Compute the rotation matrix that projects the antenna positions onto the xy-plane.
This function is used to rotate the antenna positions so that they lie in the xy-plane.

Parameters:
----------
antvecs: np.array
Array of antenna positions in the form (Nants, 3).

Returns:
-------
rotation_matrix: np.array
Rotation matrix that projects the antenna positions onto the xy-plane of shape (3, 3).
"""
# Fit a plane to the antenna positions
antx, anty, antz = antvecs.T
basis = np.array([antx, anty, np.ones_like(antz)]).T
plane, res, rank, s = linalg.lstsq(basis, antz)

# Project the antenna positions onto the plane
slope_x, slope_y, z_offset = plane

# Plane is already approximately aligned with the xy-axes,
# return identity rotation matrix
if np.isclose(slope_x, 0) and np.isclose(slope_y, 0.0):
return np.eye(3)

# Normalize the normal vector
Comment on lines +70 to +98
Copy link
Collaborator

@steven-murray steven-murray Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it probably works, but I also had gotten a working solution that might be a little more compact (maybe faster, not sure):

from scipy.spatial.transform import Rotation
mean = np.mean(antvecs, axis=0)
svd = np.linalg.svd(antvecs.T - mean[None].T)

# Get the normal vector of the plane
normal = svd[0][:, -1]

cross = np.cross(normal, np.array([0,0,1]))
rot = Rotation.from_rotvec(cross)
return rot.as_matrix()

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or you could just return rot itself, and use rot.apply(antvecs) to get new antenna vectors that are rotated to the plane (they will still have whatever offset mean they started with).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the compactness of the solution, but I'm finding that it's not reducing the z-component as the existing solution. Maybe I'm doing something wrong here?

Screenshot 2024-08-23 at 12 18 20 PM

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think the mean abs of the z-component would actually be expected to be about 12, after rotation, because at least in the compact method, it only applies a rotation around the mean point, which means the output points should all have z-components equivalent to that mean. Here it looks like that mean should be in the ballpark of 12. You can also apply an offset to bring the points back down to the z=0 plane, but I don't think that is necessary.

normal = np.array([slope_x, slope_y, -1])
normal = normal / np.linalg.norm(normal)

# Compute the rotation axis
axis = np.array([slope_y, -slope_x, 0])
axis = axis / np.linalg.norm(axis)

# Compute the rotation angle
theta = np.arccos(-normal[2])

# Compute the rotation matrix using Rodrigues' formula
K = np.array([[0, -axis[2], axis[1]],
[axis[2], 0, -axis[0]],
[-axis[1], axis[0], 0]])
rotation_matrix = np.eye(3) + np.sin(theta) * K + (1 - np.cos(theta)) * np.dot(K, K)

return rotation_matrix
Loading