From fbf4cfbe8b925d7adccaa2ed5c44f52641d48996 Mon Sep 17 00:00:00 2001 From: pcain-usgs <pcain@usgs.gov> Date: Tue, 9 Feb 2021 16:40:28 -0700 Subject: [PATCH] Refactor SVD transforms --- etc/adjusted/synthetic.json | 2 +- .../adjusted/transform/QRFactorization.py | 4 +- .../transform/RotationTranslation3D.py | 57 --------------- .../transform/RotationTranslationXY.py | 46 ++---------- geomagio/adjusted/transform/SVD.py | 71 ++++++++++++++++++- geomagio/adjusted/transform/__init__.py | 3 +- test/adjusted_test/adjusted_test.py | 8 +-- 7 files changed, 84 insertions(+), 107 deletions(-) delete mode 100644 geomagio/adjusted/transform/RotationTranslation3D.py diff --git a/etc/adjusted/synthetic.json b/etc/adjusted/synthetic.json index 015f75cd1..29f466f5c 100644 --- a/etc/adjusted/synthetic.json +++ b/etc/adjusted/synthetic.json @@ -238,7 +238,7 @@ 1 ] ], - "RotationTranslation3D": [ + "SVD": [ [ 0.8100811050998874, -0.3030939677547838, diff --git a/geomagio/adjusted/transform/QRFactorization.py b/geomagio/adjusted/transform/QRFactorization.py index 866744926..fa8187382 100644 --- a/geomagio/adjusted/transform/QRFactorization.py +++ b/geomagio/adjusted/transform/QRFactorization.py @@ -8,6 +8,8 @@ from .SVD import SVD class QRFactorization(SVD): """Calculates affine using singular value decomposition with QR factorization""" + ndims = 2 + def get_weighted_values_lstsq( self, values: Tuple[List[float], List[float], List[float]], @@ -40,14 +42,12 @@ class QRFactorization(SVD): abs_stacked = self.get_stacked_values( values=absolutes, weighted_values=weighted_absolutes, - ndims=2, ) # RHS, or independent variables ord_stacked = self.get_stacked_values( values=ordinates, weighted_values=weighted_ordinates, - ndims=2, ) abs_stacked = self.get_weighted_values_lstsq( diff --git a/geomagio/adjusted/transform/RotationTranslation3D.py b/geomagio/adjusted/transform/RotationTranslation3D.py deleted file mode 100644 index d609b4f53..000000000 --- a/geomagio/adjusted/transform/RotationTranslation3D.py +++ /dev/null @@ -1,57 +0,0 @@ -import numpy as np -import scipy.linalg as spl -from typing import List, Tuple - -from .SVD import SVD - - -class RotationTranslation3D(SVD): - """Calculates affine using using singular value decomposition, - constrained to 3D scaled rotation and translation(no shear)""" - - def calculate( - self, - ordinates: Tuple[List[float], List[float], List[float]], - absolutes: Tuple[List[float], List[float], List[float]], - weights: List[float], - ) -> np.array: - weighted_ordinates = self.get_weighted_values(values=ordinates, weights=weights) - weighted_absolutes = self.get_weighted_values(values=absolutes, weights=weights) - # generate weighted "covariance" matrix - H = np.dot( - self.get_stacked_values( - values=ordinates, - weighted_values=weighted_ordinates, - ndims=3, - ), - np.dot( - np.diag(weights), - self.get_stacked_values( - values=absolutes, - weighted_values=weighted_absolutes, - ndims=3, - ).T, - ), - ) - # Singular value decomposition, then rotation matrix from L&R eigenvectors - # (the determinant guarantees a rotation, and not a reflection) - U, S, Vh = np.linalg.svd(H) - if np.sum(S) < 3: - print("Poorly conditioned or singular matrix, returning NaNs") - return np.nan * np.ones((4, 4)) - - R = np.dot(Vh.T, np.dot(np.diag([1, 1, np.linalg.det(np.dot(Vh.T, U.T))]), U.T)) - - # now get translation using weighted centroids and R - T = np.array( - [weighted_absolutes[0], weighted_absolutes[1], weighted_absolutes[2]] - ) - np.dot( - R, [weighted_ordinates[0], weighted_ordinates[1], weighted_ordinates[2]] - ) - - return [ - [R[0, 0], R[0, 1], R[0, 2], T[0]], - [R[1, 0], R[1, 1], R[1, 2], T[1]], - [R[2, 0], R[2, 1], R[2, 2], T[2]], - [0.0, 0.0, 0.0, 1.0], - ] diff --git a/geomagio/adjusted/transform/RotationTranslationXY.py b/geomagio/adjusted/transform/RotationTranslationXY.py index b0cf427d9..ade36e195 100644 --- a/geomagio/adjusted/transform/RotationTranslationXY.py +++ b/geomagio/adjusted/transform/RotationTranslationXY.py @@ -9,47 +9,12 @@ class RotationTranslationXY(SVD): constrained to rotation and translation in XY(no scale or shear), and only translation in Z""" - def calculate( - self, - ordinates: Tuple[List[float], List[float], List[float]], - absolutes: Tuple[List[float], List[float], List[float]], - weights: List[float], - ) -> np.array: - if weights is None: - weights = np.ones_like(ordinates[0]) - weighted_ordinates = self.get_weighted_values(values=ordinates, weights=weights) - weighted_absolutes = self.get_weighted_values(values=absolutes, weights=weights) - # generate weighted "covariance" matrix - H = np.dot( - self.get_stacked_values( - values=ordinates, - weighted_values=weighted_ordinates, - ndims=2, - ), - np.dot( - np.diag(weights), - self.get_stacked_values( - values=absolutes, - weighted_values=weighted_absolutes, - ndims=2, - ).T, - ), - ) + ndims = 2 - # Singular value decomposition, then rotation matrix from L&R eigenvectors - # (the determinant guarantees a rotation, and not a reflection) - U, S, Vh = np.linalg.svd(H) - if np.sum(S) < 2: - print("Poorly conditioned or singular matrix, returning NaNs") - return np.nan * np.ones((4, 4)) - - R = np.dot(Vh.T, np.dot(np.diag([1, np.linalg.det(np.dot(Vh.T, U.T))]), U.T)) - - # now get translation using weighted centroids and R - T = np.array([weighted_absolutes[0], weighted_absolutes[1]]) - np.dot( - R, [weighted_ordinates[0], weighted_ordinates[1]] - ) + def get_rotation_matrix(self, U, Vh): + return np.dot(Vh.T, np.dot(np.diag([1, np.linalg.det(np.dot(Vh.T, U.T))]), U.T)) + def format_matrix(self, R, T, weighted_absolutes, weighted_ordinates): return [ [R[0, 0], R[0, 1], 0.0, T[0]], [R[1, 0], R[1, 1], 0.0, T[1]], @@ -57,7 +22,8 @@ class RotationTranslationXY(SVD): 0.0, 0.0, 1.0, - np.array(weighted_absolutes[2]) - np.array(weighted_ordinates[2]), + np.array(weighted_absolutes[self.ndims]) + - np.array(weighted_ordinates[self.ndims]), ], [0.0, 0.0, 0.0, 1.0], ] diff --git a/geomagio/adjusted/transform/SVD.py b/geomagio/adjusted/transform/SVD.py index 201dbc417..d1b8505dc 100644 --- a/geomagio/adjusted/transform/SVD.py +++ b/geomagio/adjusted/transform/SVD.py @@ -7,7 +7,28 @@ from .Transform import Transform class SVD(Transform): """Instance of Transform. Applies singular value decomposition to generate matrices""" - def get_stacked_values(self, values, weighted_values, ndims=3) -> np.array: + ndims = 3 + + def get_covariance_matrix(self, absolutes, ordinates, weights): + weighted_ordinates = self.get_weighted_values(values=ordinates, weights=weights) + weighted_absolutes = self.get_weighted_values(values=absolutes, weights=weights) + # generate weighted "covariance" matrix + H = np.dot( + self.get_stacked_values( + values=ordinates, + weighted_values=weighted_ordinates, + ), + np.dot( + np.diag(weights), + self.get_stacked_values( + values=absolutes, + weighted_values=weighted_absolutes, + ).T, + ), + ) + return H + + def get_stacked_values(self, values, weighted_values) -> np.array: """Supports intermediate mathematical steps by differencing and shaping values for SVD Attributes @@ -20,7 +41,7 @@ class SVD(Transform): ------- Stacked and differenced values from their weighted counterparts """ - return np.vstack([[values[i] - weighted_values[i]] for i in range(ndims)]) + return np.vstack([[values[i] - weighted_values[i]] for i in range(self.ndims)]) def get_weighted_values( self, @@ -35,3 +56,49 @@ class SVD(Transform): np.average(values[1], weights=weights), np.average(values[2], weights=weights), ) + + def get_rotation_matrix(self, U, Vh): + return np.dot( + Vh.T, np.dot(np.diag([1, 1, np.linalg.det(np.dot(Vh.T, U.T))]), U.T) + ) + + def get_translation_matrix(self, R, weighted_absolutes, weighted_ordinates): + return np.array([weighted_absolutes[i] for i in range(self.ndims)]) - np.dot( + R, [weighted_ordinates[i] for i in range(self.ndims)] + ) + + def calculate( + self, + ordinates: Tuple[List[float], List[float], List[float]], + absolutes: Tuple[List[float], List[float], List[float]], + weights: List[float], + ) -> np.array: + weighted_ordinates = self.get_weighted_values(values=ordinates, weights=weights) + weighted_absolutes = self.get_weighted_values(values=absolutes, weights=weights) + # generate weighted "covariance" matrix + H = self.get_covariance_matrix(absolutes, ordinates, weights) + # Singular value decomposition, then rotation matrix from L&R eigenvectors + # (the determinant guarantees a rotation, and not a reflection) + U, S, Vh = np.linalg.svd(H) + if np.sum(S) < 3: + print("Poorly conditioned or singular matrix, returning NaNs") + return np.nan * np.ones((4, 4)) + R = self.get_rotation_matrix(U, Vh) + # now get translation using weighted centroids and R + T = self.get_translation_matrix(R, weighted_absolutes, weighted_ordinates) + + return self.format_matrix(R, T, weighted_absolutes, weighted_ordinates) + + def format_matrix( + self, + R, + T, + weighted_absolutes, + weighted_ordinates, + ): + return [ + [R[0, 0], R[0, 1], R[0, 2], T[0]], + [R[1, 0], R[1, 1], R[1, 2], T[1]], + [R[2, 0], R[2, 1], R[2, 2], T[2]], + [0.0, 0.0, 0.0, 1.0], + ] diff --git a/geomagio/adjusted/transform/__init__.py b/geomagio/adjusted/transform/__init__.py index 85156ca47..0fa99ab9a 100644 --- a/geomagio/adjusted/transform/__init__.py +++ b/geomagio/adjusted/transform/__init__.py @@ -1,11 +1,11 @@ from .LeastSq import LeastSq from .QRFactorization import QRFactorization from .Rescale3D import Rescale3D -from .RotationTranslation3D import RotationTranslation3D from .RotationTranslationXY import RotationTranslationXY from .ShearYZ import ShearYZ from .Transform import Transform from .TranslateOrigins import TranslateOrigins +from .SVD import SVD from .ZRotationHScale import ZRotationHscale from .ZRotationHScaleZBaseline import ZRotationHscaleZbaseline from .ZRotationShear import ZRotationShear @@ -19,6 +19,7 @@ __all__ = [ "ShearYZ", "Transform", "TranslateOrigins", + "SVD", "ZRotationHscale", "ZRotationHscaleZbaseline", "ZRotationShear", diff --git a/test/adjusted_test/adjusted_test.py b/test/adjusted_test/adjusted_test.py index 276e626fb..6e4ef6368 100644 --- a/test/adjusted_test/adjusted_test.py +++ b/test/adjusted_test/adjusted_test.py @@ -10,7 +10,7 @@ from geomagio.adjusted.transform import ( LeastSq, QRFactorization, Rescale3D, - RotationTranslation3D, + SVD, RotationTranslationXY, ShearYZ, TranslateOrigins, @@ -79,15 +79,15 @@ def test_Rescale3D_synthetic(): ) -def test_RotationTranslation3D_synthetic(): +def test_SVD_synthetic(): ordinates, absolutes, weights = get_sythetic_variables() assert_array_almost_equal( - RotationTranslation3D().calculate( + SVD().calculate( ordinates=ordinates, absolutes=absolutes, weights=weights, ), - get_expected_synthetic_result("RotationTranslation3D"), + get_expected_synthetic_result("SVD"), decimal=3, ) -- GitLab