Exemplo n.º 1
0
    def shell_coordinates(self, points):
        """Scale query points, projecting them onto the basis used in shell-
        fitting. Return in scaled spherical coordinates

        Parameters
        ----------
        points : 3-tuple
            x, y, z positions

        Returns
        -------
        azimuth, zenith, r
            scaled spherical coordinates of input points
        """
        x, y, z = points

        # scale the query points and convert them to spherical
        x_qs, y_qs, z_qs = coordinate_tools.scaled_projection(
            np.atleast_1d(x - self.x0), np.atleast_1d(y - self.y0),
            np.atleast_1d(z - self.z0), self.scaling_factors,
            self.principal_axes)

        azimuth_qs, zenith_qs, r_qs = coordinate_tools.cartesian_to_spherical(
            x_qs, y_qs, z_qs)
        return azimuth_qs, zenith_qs, r_qs
Exemplo n.º 2
0
def test_rotated_projection():
    x = np.arange(1000)
    y = np.zeros_like(x)
    z = np.zeros_like(x)

    scaling_axes = np.array([[0., 1., 0.], [1., 0., 0.], [0., 0., 1.]])
    scaling_factors = [5., 5., 5.]
    xs, ys, zs = coordinate_tools.scaled_projection(x, y, z, scaling_factors,
                                                    scaling_axes)
    np.testing.assert_array_equal([y, 5 * x, z], [xs, ys, zs])
Exemplo n.º 3
0
 def _scale_fitting_points(self):
     self.standard_deviations, self.principal_axes = coordinate_tools.find_principle_axes(
         self.x_c,
         self.y_c,
         self.z_c,
         sample_fraction=self.sampling_fraction)
     self.scaling_factors = np.max(
         self.standard_deviations) / (self.standard_deviations)
     self.x_cs, self.y_cs, self.z_cs, = coordinate_tools.scaled_projection(
         self.x_c, self.y_c, self.z_c, self.scaling_factors,
         self.principal_axes)
Exemplo n.º 4
0
    def check_inside(self, x=None, y=None, z=None):
        if x is None:
            xcs, ycs, zcs = self.x_cs, self.y_cs, self.z_cs
        else:
            xcs, ycs, zcs = coordinate_tools.scaled_projection(
                x - self.x0, y - self.y0, z - self.z0, self.scaling_factors,
                self.principal_axes)

        azimuth, zenith, rcs = coordinate_tools.cartesian_to_spherical(
            xcs, ycs, zcs)
        r_cs_shell = reconstruct_shell(self.modes, self.coefficients, azimuth,
                                       zenith)
        return rcs < r_cs_shell
Exemplo n.º 5
0
    def fit(self,
            points,
            n_max=3,
            n_iters=2,
            tol=0.3,
            principle_axis_sampling=1.0):
        x, y, z = points
        self.x0, self.y0, self.z0 = x.mean(), y.mean(), z.mean()
        x_c, y_c, z_c = x - self.x0, y - self.y0, z - self.z0

        sig, self.principal_axes = coordinate_tools.find_principle_axes(
            x_c, y_c, z_c, sample_fraction=principle_axis_sampling)
        self.axis_scaling = sig / np.max(sig)

        x_cs, y_cs, z_cs, = coordinate_tools.scaled_projection(
            x_c, y_c, z_c, self.axis_scaling, self.principal_axes)

        self.modes, self.coefficients, _ = sphere_expansion_clean(
            x_cs, y_cs, z_cs, n_max, n_iters, tol)
Exemplo n.º 6
0
    def _distance_error(self, parameterized_distance, vector, starting_point):
        """

        Calculate the error in scaled space between the shell and the point reached traveling a specified distance(s)
        along the input vector from the input starting position.

        This function is to be minimized by a solver. Note that we don't actually have to calculate the distance in
        normal space, since minimizing in the scale space is equivalent.

        Parameters
        ----------
        parameterized_distance : float
            distance along vector to travel, units of nm, unscaled
        vector : ndarray
            Length three, vector in (unscaled) cartesian coordinates along which to travel
        starting_point : ndarray or list
            Length three, point in (unscaled) cartesian space from which to start traveling along 'vector'

        Returns
        -------

        """
        x, y, z = [
            parameterized_distance * np.atleast_2d(vector)[:, ind] +
            np.atleast_2d(starting_point)[:, ind] for ind in range(3)
        ]
        # scale the query points and convert them to spherical
        x_qs, y_qs, z_qs = coordinate_tools.scaled_projection(
            np.atleast_1d(x - self.x0), np.atleast_1d(y - self.y0),
            np.atleast_1d(z - self.z0), self.scaling_factors,
            self.principal_axes)
        azimuth_qs, zenith_qs, r_qs = coordinate_tools.cartesian_to_spherical(
            x_qs, y_qs, z_qs)

        # get scaled shell radius at those angles
        r_shell = reconstruct_shell(self.modes, self.coefficients, azimuth_qs,
                                    zenith_qs)

        # return the (scaled space) difference
        return r_qs - r_shell
Exemplo n.º 7
0
    def approximate_normal(self,
                           x,
                           y,
                           z,
                           d_azimuth=1e-6,
                           d_zenith=1e-6,
                           return_orthogonal_vectors=False):
        """

        Numerically approximate a vector(s) normal to the spherical harmonic shell at the query point(s).

        For input point(s), scale and convert to spherical coordinates, shift by +/- d_azimuth and d_zenith to get
        'phantom' points in the plane tangent to the spherical harmonic expansion on either side of the query point(s).
        Scale back, convert to cartesian, make vectors from the phantom points (which are by definition not parallel)
        and cross them to get a vector perpindicular to the plane.

        Returns
        -------

        Parameters
        ----------
        x : ndarray, float
            cartesian x location of point(s) on the surface to calculate the normal at
        y : ndarray, float
            cartesian y location of point(s) on the surface to calculate the normal at
        z : ndarray, float
            cartesian z location of point(s) on the surface to calculate the normal at
        d_azimuth : float
            azimuth step size for generating vector in plane of the shell [radians]
        d_zenith : float
            zenith step size for generating vector in plane of the shell [radians]

        Returns
        -------
        normal_vector : ndarray
            cartesian unit vector(s) normal to query point(s). size (len(x), 3)
        orth0 : ndarray
            cartesian unit vector(s) in the plane of the spherical harmonic shell at the query point(s), and
            perpendicular to normal_vector
        orth1 : ndarray
            cartesian unit vector(s) orthogonal to normal_vector and orth0

        """
        # scale the query points and convert them to spherical
        x_qs, y_qs, z_qs = coordinate_tools.scaled_projection(
            np.atleast_1d(x - self.x0), np.atleast_1d(y - self.y0),
            np.atleast_1d(z - self.z0), self.scaling_factors,
            self.principal_axes)
        azimuth, zenith, r = coordinate_tools.cartesian_to_spherical(
            x_qs, y_qs, z_qs)

        # get scaled shell radius at +/- points for azimuthal and zenith shifts
        azimuths = np.array(
            [azimuth - d_azimuth, azimuth + d_azimuth, azimuth, azimuth])
        zeniths = np.array(
            [zenith, zenith, zenith - d_zenith, zenith + d_zenith])
        r_scaled = reconstruct_shell(self.modes, self.coefficients, azimuths,
                                     zeniths)

        # convert shifted points to cartesian and scale back. shape = (4, #points)
        x_scaled, y_scaled, z_scaled = coordinate_tools.spherical_to_cartesian(
            azimuths, zeniths, r_scaled)
        # scale things "down" since they were scaled "up" in the fit
        scaled_axes = self.principal_axes / self.scaling_factors[:, None]
        coords = x_scaled.ravel()[:, None] * scaled_axes[0, :] + \
                 y_scaled.ravel()[:, None] * scaled_axes[1, :] + \
                 z_scaled.ravel()[:, None] * scaled_axes[2, :]
        x_p, y_p, z_p = coords.T
        # skip adding x0, y0, z0 back on, since we'll subtract it off in a second
        x_p, y_p, z_p = x_p.reshape(x_scaled.shape), y_p.reshape(
            y_scaled.shape), z_p.reshape(z_scaled.shape)

        # make two vectors in the plane centered at the query point
        v0 = np.array([x_p[1] - x_p[0], y_p[1] - y_p[0], z_p[1] - z_p[0]])
        v1 = np.array([x_p[3] - x_p[2], y_p[3] - y_p[2], z_p[3] - z_p[2]])
        if not np.any(v0) or not np.any(v1):
            raise RuntimeWarning(
                'failed to generate two vectors in the plane - likely precision error in sph -> cart'
            )
        # cross them to get a normal vector NOTE - direction could be negative of true normal
        normal = np.cross(v0, v1, axis=0)
        # return as unit vector(s) along each row
        normal = np.atleast_2d(normal / np.linalg.norm(normal, axis=0)).T
        # make sure normals point outwards, by dotting it with the vector to the point on the shell from the center
        points = np.stack([
            np.atleast_1d(x - self.x0),
            np.atleast_1d(y - self.y0),
            np.atleast_1d(z - self.z0)
        ]).T
        outwards = np.array([
            np.dot(normal[ind], points[ind]) > 0
            for ind in range(normal.shape[0])
        ])
        normal[~outwards, :] *= -1
        if np.isnan(normal).any():
            raise RuntimeError('Failed to calculate normal vector')
        if return_orthogonal_vectors:
            orth0 = np.atleast_2d(v0 / np.linalg.norm(v0, axis=0)).T
            # v0 and v1 are both in a plane perpendicular to normal, but not strictly orthogonal to each other
            orth1 = np.cross(
                normal, orth0, axis=1
            )  # replace v1 with a unit vector orth. to both normal and v0
            return normal.squeeze(), orth0.squeeze(), orth1.squeeze()
        return normal.squeeze()