Skip to content

External potential #419

@Sketos

Description

@Sketos

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.py

Affected Repositories

profiles/mass/sheets

Implementation Steps

Key Files

Example Code

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

Original Prompt

Click to expand starting prompt

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions