This profile is described in Powell 2022 and function form of the potential is given in Eq 4. The first term is equivalent to the external shear that we use in autolens. The other terms correspond to a gradient in the surface mass density and gradient of the external shear.
I think the best approach for the code provided is to commit the first term of the ExternalPotential as this is exactly the ExternalShear in autolens. The deflection angles for the two other terms have a radial dependance so the centre should also be a free parameter (which would often be set to external_potential.centre = mass.centre), unlike the ExternalShear where deflection angles are constant and therefore the position of the source is degenerate with them (centre can be fixed to 0,0).
class ExternalPotential(MassProfile):
def __init__(
self,
centre=(0.0, 0.0),
gamma_1: float = 0.0,
gamma_2: float = 0.0,
tau_1: float = 0.0,
tau_2: float = 0.0,
delta_1: float = 0.0,
delta_2: float = 0.0,
):
super().__init__(
centre=centre, ell_comps=(0.0, 0.0)
)
self.gamma_1 = gamma_1
self.gamma_2 = gamma_2
self.tau_1 = tau_1
self.tau_2 = tau_2
self.delta_1 = delta_1
self.delta_2 = delta_2
@staticmethod
def radial_and_angle_grid_from(
grid: aa.type.Grid2DLike, centre: Tuple[float, float] = (0.0, 0.0), xp=np
) -> Tuple[np.ndarray, np.ndarray]:
y, x = grid.array.T
x_shifted = xp.subtract(x, centre[1])
y_shifted = xp.subtract(y, centre[0])
radial_grid = xp.sqrt(x_shifted**2 + y_shifted**2)
angle_grid = xp.arctan2(y_shifted, x_shifted)
return radial_grid, angle_grid
@staticmethod
def _magnitude_from(c1, c2, xp=np):
return xp.sqrt(c1 * c1 + c2 * c2)
@staticmethod
def _angle_from(c1, c2, harmonic: int, xp=np):
"""
Return the principal angle in degrees for a harmonic of order `harmonic`.
harmonic = 2 -> [0, 180)
harmonic = 1 -> [0, 360)
harmonic = 3 -> [0, 120)
"""
angle = xp.rad2deg(xp.arctan2(c2, c1)) / harmonic
period = 360.0 / harmonic
return angle % period
@classmethod
def from_magnitudes_and_angles(
cls,
centre=(0.0, 0.0),
gamma: float = 0.0,
theta_gamma: float = 0.0,
tau: float = 0.0,
theta_tau: float = 0.0,
delta: float = 0.0,
theta_delta: float = 0.0,
):
"""
Build the profile from paper-style magnitudes and angles.
Angles are in degrees, anticlockwise from +x.
"""
tg = np.deg2rad(theta_gamma)
tt = np.deg2rad(theta_tau)
td = np.deg2rad(theta_delta)
gamma_1 = gamma * np.cos(2.0 * tg)
gamma_2 = gamma * np.sin(2.0 * tg)
tau_1 = tau * np.cos(tt)
tau_2 = tau * np.sin(tt)
delta_1 = delta * np.cos(3.0 * td)
delta_2 = delta * np.sin(3.0 * td)
return cls(
centre=centre,
gamma_1=gamma_1,
gamma_2=gamma_2,
tau_1=tau_1,
tau_2=tau_2,
delta_1=delta_1,
delta_2=delta_2,
)
def gamma_magnitude(self, xp=np):
return self._magnitude_from(self.gamma_1, self.gamma_2, xp=xp)
def gamma_angle(self, xp=np):
return self._angle_from(self.gamma_1, self.gamma_2, harmonic=2, xp=xp)
def tau_magnitude(self, xp=np):
return self._magnitude_from(self.tau_1, self.tau_2, xp=xp)
def tau_angle(self, xp=np):
return self._angle_from(self.tau_1, self.tau_2, harmonic=1, xp=xp)
def delta_magnitude(self, xp=np):
return self._magnitude_from(self.delta_1, self.delta_2, xp=xp)
def delta_angle(self, xp=np):
return self._angle_from(self.delta_1, self.delta_2, harmonic=3, xp=xp)
@aa.grid_dec.to_array
def convergence_2d_from(self, grid, xp=np, **kwargs):
return xp.zeros(shape=grid.shape[0])
@aa.grid_dec.to_array
def potential_2d_from(self, grid, xp=np, **kwargs):
r, theta = self.radial_and_angle_grid_from(
grid=grid,
centre=self.centre,
xp=xp
)
gamma_term = 0.5 * r**2 * (
self.gamma_1 * xp.cos(2.0 * theta) + self.gamma_2 * xp.sin(2.0 * theta)
)
tau_term = 0.25 * r**3 * (
self.tau_1 * xp.cos(theta) + self.tau_2 * xp.sin(theta)
)
delta_term = (1.0 / 6.0) * r**3 * (
self.delta_1 * xp.cos(3.0 * theta) + self.delta_2 * xp.sin(3.0 * theta)
)
return gamma_term + tau_term + delta_term
@aa.grid_dec.to_vector_yx
@aa.grid_dec.transform
def deflections_yx_2d_from(self, grid, xp=np, **kwargs):
r, theta = self.radial_and_angle_grid_from(
grid=grid,
centre=self.centre,
xp=xp
)
alpha_r = (
r * (
self.gamma_1 * xp.cos(2.0 * theta)
+ self.gamma_2 * xp.sin(2.0 * theta)
)
+ 0.75 * r**2 * (
self.tau_1 * xp.cos(theta) + self.tau_2 * xp.sin(theta)
)
+ 0.5 * r**2 * (
self.delta_1 * xp.cos(3.0 * theta)
+ self.delta_2 * xp.sin(3.0 * theta)
)
)
alpha_theta = (
r * (
-self.gamma_1 * xp.sin(2.0 * theta)
+ self.gamma_2 * xp.cos(2.0 * theta)
)
+ 0.25 * r**2 * (
-self.tau_1 * xp.sin(theta) + self.tau_2 * xp.cos(theta)
)
+ 0.5 * r**2 * (
-self.delta_1 * xp.sin(3.0 * theta)
+ self.delta_2 * xp.cos(3.0 * theta)
)
)
alpha_y = xp.sin(theta) * alpha_r + xp.cos(theta) * alpha_theta
alpha_x = xp.cos(theta) * alpha_r - xp.sin(theta) * alpha_theta
return xp.vstack((alpha_y, alpha_x)).T
Overview
This profile is described in Powell 2022 and function form of the potential is given in Eq 4. The first term is equivalent to the external shear that we use in autolens. The other terms correspond to a gradient in the surface mass density and gradient of the external shear.Plan
I think the best approach for the code provided is to commit the first term of the ExternalPotential as this is exactly the ExternalShear in autolens. The deflection angles for the two other terms have a radial dependance so the centre should also be a free parameter (which would often be set to external_potential.centre = mass.centre), unlike the ExternalShear where deflection angles are constant and therefore the position of the source is degenerate with them (centre can be fixed to 0,0).Detailed implementation plan
Add a file external_potential.pyAffected Repositories
profiles/mass/sheetsImplementation Steps
Key Files
Example Code
Original Prompt
Click to expand starting prompt