diff --git a/geomagio/adjusted/transform/LeastSq.py b/geomagio/adjusted/transform/LeastSq.py index 45c6cb8bc8260b6b4d4b0939ab8c7a33e829c479..f33eace1564b7bac087a7e27de70e37c932f2d2a 100644 --- a/geomagio/adjusted/transform/LeastSq.py +++ b/geomagio/adjusted/transform/LeastSq.py @@ -8,7 +8,7 @@ from .Transform import Transform class LeastSq(Transform): """Intance of Transform. Applies least squares to generate matrices""" - def get_stacked_values(self, absolutes, ordinates): + def get_stacked_values(self, absolutes, ordinates, weights=None): # LHS, or dependent variables # [A[0,0], A[1,0], A[2,0], A[0,1], A[1,1], A[2,1], ...] abs_stacked = self.get_stacked_absolutes(absolutes) @@ -54,6 +54,11 @@ class LeastSq(Transform): return ord_stacked + def valid(self, rank): + if rank < self.ndims: + return False + return True + def get_weighted_values( self, values: Tuple[List[float], List[float], List[float]], @@ -83,17 +88,19 @@ class LeastSq(Transform): weights: List[float], ) -> np.array: """Calculates affine with no constraints using least squares.""" - abs_stacked, ord_stacked = self.get_stacked_values(absolutes, ordinates) + abs_stacked, ord_stacked = self.get_stacked_values( + absolutes, ordinates, weights + ) ord_stacked = self.get_weighted_values(ord_stacked, weights) abs_stacked = self.get_weighted_values(abs_stacked, weights) # regression matrix M that minimizes L2 norm matrix, res, rank, sigma = spl.lstsq(ord_stacked.T, abs_stacked.T) - if rank < 3: - print("Poorly conditioned or singular matrix, returning NaNs") - return np.nan * np.ones((4, 4)) - return self.format_matrix(matrix) + if self.valid(rank): + return self.get_matrix(matrix, absolutes, ordinates, weights) + print("Poorly conditioned or singular matrix, returning NaNs") + return np.nan * np.ones((4, 4)) - def format_matrix(self, matrix): + def get_matrix(self, matrix, absolutes=None, ordinates=None, weights=None): return np.array( [ [matrix[0], matrix[1], matrix[2], matrix[3]], diff --git a/geomagio/adjusted/transform/QRFactorization.py b/geomagio/adjusted/transform/QRFactorization.py index fa818738249bc7fddb53c5e72d8e0b3d74827bba..b633ffb6602f02170f758878899799a0293751e7 100644 --- a/geomagio/adjusted/transform/QRFactorization.py +++ b/geomagio/adjusted/transform/QRFactorization.py @@ -2,15 +2,17 @@ import numpy as np import scipy.linalg as spl from typing import List, Optional, Tuple +from .LeastSq import LeastSq from .SVD import SVD -class QRFactorization(SVD): +class QRFactorization(LeastSq): """Calculates affine using singular value decomposition with QR factorization""" ndims = 2 + svd = SVD(ndims=ndims) - def get_weighted_values_lstsq( + def get_weighted_values( self, values: Tuple[List[float], List[float], List[float]], weights: Optional[List[float]], @@ -19,57 +21,34 @@ class QRFactorization(SVD): if weights is None: return values weights = np.sqrt(weights) - return np.array( - [ - values[0] * weights, - values[1] * weights, - ] - ) - - def calculate( - self, - ordinates: Tuple[List[float], List[float], List[float]], - absolutes: Tuple[List[float], List[float], List[float]], - weights: List[float], - ) -> np.array: + return np.array([values[i] * weights for i in range(self.ndims)]) - if weights is None: - weights = np.ones_like(ordinates[0]) - - weighted_absolutes = self.get_weighted_values(values=absolutes, weights=weights) - weighted_ordinates = self.get_weighted_values(values=ordinates, weights=weights) + def get_stacked_values(self, absolutes, ordinates, weights): + weighted_absolutes = self.svd.get_weighted_values( + values=absolutes, weights=weights + ) + weighted_ordinates = self.svd.get_weighted_values( + values=ordinates, weights=weights + ) # LHS, or dependent variables - abs_stacked = self.get_stacked_values( + abs_stacked = self.svd.get_stacked_values( values=absolutes, weighted_values=weighted_absolutes, ) # RHS, or independent variables - ord_stacked = self.get_stacked_values( + ord_stacked = self.svd.get_stacked_values( values=ordinates, weighted_values=weighted_ordinates, ) + return abs_stacked, ord_stacked - abs_stacked = self.get_weighted_values_lstsq( - values=abs_stacked, - weights=weights, - ) - ord_stacked = self.get_weighted_values_lstsq( - values=ord_stacked, - weights=weights, - ) - - # regression matrix M that minimizes L2 norm - M_r, res, rank, sigma = spl.lstsq(ord_stacked.T, abs_stacked.T) - if rank < 2: - print("Poorly conditioned or singular matrix, returning NaNs") - return np.nan * np.ones((4, 4)) - + def get_matrix(self, matrix, absolutes, ordinates, weights): # QR fatorization # NOTE: forcing the diagonal elements of Q to be positive # ensures that the determinant is 1, not -1, and is # therefore a rotation, not a reflection - Q, R = np.linalg.qr(M_r.T) + Q, R = np.linalg.qr(matrix.T) neg = np.diag(Q) < 0 Q[:, neg] = -1 * Q[:, neg] R[neg, :] = -1 * R[neg, :] @@ -81,10 +60,11 @@ class QRFactorization(SVD): # combine shear and rotation QH = np.dot(Q, H) + weighted_absolutes = self.svd.get_weighted_values(absolutes, weights) + weighted_ordinates = self.svd.get_weighted_values(ordinates, weights) + # now get translation using weighted centroids and R - T = np.array([weighted_absolutes[0], weighted_absolutes[1]]) - np.dot( - QH, [weighted_ordinates[0], weighted_ordinates[1]] - ) + T = self.svd.get_translation_matrix(QH, weighted_absolutes, weighted_ordinates) return [ [QH[0, 0], QH[0, 1], 0.0, T[0]], @@ -93,7 +73,8 @@ class QRFactorization(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/Rescale3D.py b/geomagio/adjusted/transform/Rescale3D.py index c55ca8f1e45003296c60b85a59c106cb553f2261..c74a9bb7258d67378b8e7bcc740da021f9066466 100644 --- a/geomagio/adjusted/transform/Rescale3D.py +++ b/geomagio/adjusted/transform/Rescale3D.py @@ -21,7 +21,7 @@ class Rescale3D(LeastSq): ord_stacked[2, 2::3] = ordinates[2] return ord_stacked - def format_matrix(self, matrix): + def get_matrix(self, matrix, absolutes=None, ordinates=None, weights=None): return [ [matrix[0], 0.0, 0.0, 0.0], [0.0, matrix[1], 0.0, 0.0], diff --git a/geomagio/adjusted/transform/RotationTranslationXY.py b/geomagio/adjusted/transform/RotationTranslationXY.py index ade36e19541b323895d44521b7dca161fcdf65b5..f140a38450e7905646b97643020956754bea4668 100644 --- a/geomagio/adjusted/transform/RotationTranslationXY.py +++ b/geomagio/adjusted/transform/RotationTranslationXY.py @@ -14,7 +14,7 @@ class RotationTranslationXY(SVD): 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): + def get_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]], diff --git a/geomagio/adjusted/transform/SVD.py b/geomagio/adjusted/transform/SVD.py index d1b8505dc7392022b1a31783d12a5028c9cecf43..f9ff490560f8a8766b73fb4c1b9ff85fef16ecf1 100644 --- a/geomagio/adjusted/transform/SVD.py +++ b/geomagio/adjusted/transform/SVD.py @@ -7,8 +7,6 @@ from .Transform import Transform class SVD(Transform): """Instance of Transform. Applies singular value decomposition to generate matrices""" - 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) @@ -67,6 +65,11 @@ class SVD(Transform): R, [weighted_ordinates[i] for i in range(self.ndims)] ) + def valid(self, singular_values): + if np.sum(singular_values) < self.ndims: + return False + return True + def calculate( self, ordinates: Tuple[List[float], List[float], List[float]], @@ -80,21 +83,20 @@ class SVD(Transform): # 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) + if self.valid(S): + 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.get_matrix(R, T, weighted_absolutes, weighted_ordinates) + print("Poorly conditioned or singular matrix, returning NaNs") + return np.nan * np.ones((4, 4)) - def format_matrix( + def get_matrix( self, R, T, - weighted_absolutes, - weighted_ordinates, + weighted_absolutes=None, + weighted_ordinates=None, ): return [ [R[0, 0], R[0, 1], R[0, 2], T[0]], diff --git a/geomagio/adjusted/transform/ShearYZ.py b/geomagio/adjusted/transform/ShearYZ.py index dc2e43c213bdb30c976655a7503b79d98157d14a..7873c7b2934c1e167a5fa8c1c806f5b25c459967 100644 --- a/geomagio/adjusted/transform/ShearYZ.py +++ b/geomagio/adjusted/transform/ShearYZ.py @@ -23,7 +23,7 @@ class ShearYZ(LeastSq): ord_stacked[2, 2::3] = 1.0 return ord_stacked - def format_matrix(self, matrix): + def get_matrix(self, matrix, absolutes=None, ordinates=None, weights=None): return [ [1.0, 0.0, 0.0, 0.0], [matrix[0], 1.0, 0.0, 0.0], diff --git a/geomagio/adjusted/transform/Transform.py b/geomagio/adjusted/transform/Transform.py index 5fdbad7c6422b2db44768c95a408ccd7815e6ae2..a365d9f9f040be5392ae346033be80b834590340 100644 --- a/geomagio/adjusted/transform/Transform.py +++ b/geomagio/adjusted/transform/Transform.py @@ -16,6 +16,7 @@ class Transform(BaseModel): acausal: bool = False memory: Optional[float] = None + ndims = 3 def get_weights(self, times: UTCDateTime, time: int = None) -> List[float]: """ diff --git a/geomagio/adjusted/transform/TranslateOrigins.py b/geomagio/adjusted/transform/TranslateOrigins.py index 69c21915fb007c0bcea79e7c0c6dcb958752a40a..fbb08b8f9f573f5eee5e182d7143c1078e71bf43 100644 --- a/geomagio/adjusted/transform/TranslateOrigins.py +++ b/geomagio/adjusted/transform/TranslateOrigins.py @@ -8,7 +8,7 @@ from .LeastSq import LeastSq class TranslateOrigins(LeastSq): """Calculates affine using using least squares, constrained to tanslate origins""" - def get_stacked_values(self, absolutes, ordinates): + def get_stacked_values(self, absolutes, ordinates, weights=None): # LHS, or dependent variables abs_stacked = self.get_stacked_absolutes(absolutes) # subtract ords from abs to force simple translation @@ -40,7 +40,7 @@ class TranslateOrigins(LeastSq): ord_stacked[2, 2::3] = 1.0 return ord_stacked - def format_matrix(self, matrix): + def get_matrix(self, matrix, absolutes=None, ordinates=None, weights=None): return [ [1.0, 0.0, 0.0, matrix[0]], [0.0, 1.0, 0.0, matrix[1]], diff --git a/geomagio/adjusted/transform/ZRotationHScale.py b/geomagio/adjusted/transform/ZRotationHScale.py index a5536bce8202b0a25f40cc6622bef37ec0c181cf..4e56a6e835871b2457c08b5d94e7827c0e48ef91 100644 --- a/geomagio/adjusted/transform/ZRotationHScale.py +++ b/geomagio/adjusted/transform/ZRotationHScale.py @@ -26,7 +26,7 @@ class ZRotationHscale(LeastSq): ord_stacked[5, 2::3] = 1.0 return ord_stacked - def format_matrix(self, matrix): + def get_matrix(self, matrix, absolutes=None, ordinates=None, weights=None): return [ [matrix[0], matrix[1], 0.0, matrix[2]], [-matrix[1], matrix[0], 0.0, matrix[3]], diff --git a/geomagio/adjusted/transform/ZRotationHScaleZBaseline.py b/geomagio/adjusted/transform/ZRotationHScaleZBaseline.py index 569786afcbf5be6f58704d2f9ab50a80158b2c13..13393ac38937db544986bdd8bc2483733adf6a04 100644 --- a/geomagio/adjusted/transform/ZRotationHScaleZBaseline.py +++ b/geomagio/adjusted/transform/ZRotationHScaleZBaseline.py @@ -9,7 +9,7 @@ class ZRotationHscaleZbaseline(LeastSq): """Calculates affine using least squares, constrained to rotate about the Z axis, apply a uniform horizontal scaling, and apply a baseline shift for the Z axis.""" - def get_stacked_values(self, absolutes, ordinates): + def get_stacked_values(self, absolutes, ordinates, weights=None): # LHS, or dependent variables abs_stacked = self.get_stacked_absolutes(absolutes) # subtract z_o from z_a to force simple z translation @@ -34,7 +34,7 @@ class ZRotationHscaleZbaseline(LeastSq): ord_stacked[2, 2::3] = 1.0 return ord_stacked - def format_matrix(self, matrix): + def get_matrix(self, matrix, absolutes=None, ordinates=None, weights=None): return [ [matrix[0], matrix[1], 0.0, 0.0], [-matrix[1], matrix[0], 0.0, 0.0], diff --git a/geomagio/adjusted/transform/ZRotationShear.py b/geomagio/adjusted/transform/ZRotationShear.py index 693cbb4152b5081cdc8806349dd58e6e02c6c286..d1872c0e60e0f94c60afb549478668ad9cc84dbb 100644 --- a/geomagio/adjusted/transform/ZRotationShear.py +++ b/geomagio/adjusted/transform/ZRotationShear.py @@ -22,7 +22,7 @@ class ZRotationShear(LeastSq): ord_stacked[7, 2::3] = 1.0 return ord_stacked - def format_matrix(self, matrix): + def get_matrix(self, matrix, absolutes=None, ordinates=None, weights=None): return [ [matrix[0], matrix[1], 0.0, matrix[2]], [matrix[3], matrix[4], 0.0, matrix[5]],