def get_fitted_shell(self, azimuth, zenith): r_scaled = reconstruct_shell(self.modes, self.coefficients, azimuth, zenith) x_scaled, y_scaled, z_scaled = coordinate_tools.spherical_to_cartesian( azimuth, zenith, r_scaled) # need to scale things "down" since they were scaled "up" in the fit # scaling_factors = 1. / self.scaling_factors 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, y, z = coords.T return x.reshape(x_scaled.shape) + self.x0, y.reshape( y_scaled.shape) + self.y0, z.reshape(z_scaled.shape) + self.z0
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()
def test_spherical_to_cartesian(): azi, zen, r = coordinate_tools.cartesian_to_spherical(X_C, Y_C, Z_C) x, y, z = coordinate_tools.spherical_to_cartesian(azi, zen, r) np.testing.assert_array_almost_equal(X_C, x) np.testing.assert_array_almost_equal(Y_C, y) np.testing.assert_array_almost_equal(Z_C, z)