Skip to content

Commit

Permalink
Merge pull request #110 from EXP-code/UniquePCsigns
Browse files Browse the repository at this point in the history
Disambiguate the sign (phase) of the mSSA PCs
  • Loading branch information
michael-petersen authored Feb 14, 2025
2 parents a232ebd + 55a6190 commit dccf0b8
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 7 deletions.
2 changes: 1 addition & 1 deletion expui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ endif()
set(expui_SOURCES BasisFactory.cc BiorthBasis.cc FieldBasis.cc
CoefContainer.cc CoefStruct.cc FieldGenerator.cc expMSSA.cc
Coefficients.cc KMeans.cc Centering.cc ParticleIterator.cc
Koopman.cc BiorthBess.cc)
Koopman.cc BiorthBess.cc SvdSignChoice.cc)
add_library(expui ${expui_SOURCES})
set_target_properties(expui PROPERTIES OUTPUT_NAME expui)
target_include_directories(expui PUBLIC ${common_INCLUDE})
Expand Down
269 changes: 269 additions & 0 deletions expui/SvdSignChoice.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
#include <Eigen/Dense>

namespace MSSA {

// SVD with corrected signs
//
// Input
// -----
// X is data matrix (constant)
// U is the matrix of left singular vectors
// S is the array of singular values (constant)
// V is the matrix of right singular vectors
//
// Output
// ------
// U, V returned corrected, disambiguated signs
//
// Reference
// ---------
// Bro, R., Acar, E., & Kolda, T. G. (2008). Resolving the sign
// ambiguity in the singular value decomposition. Journal of
// Chemometrics: A Journal of the Chemometrics Society, 22(2),
// 135-140.
//
// URL:
// https://prod-ng.sandia.gov/techlib-noauth/access-control.cgi/2007/076422.pdf
//
void SvdSignChoice
(const Eigen::MatrixXd& X,
Eigen::MatrixXd& U, const Eigen::VectorXd& S, Eigen::MatrixXd& V)
{
// SDV dimensions
int I = U.rows();
int J = V.rows();
int K = S.size();

// Dimensions
// ----------
// X is a [I x J] data matrix
// U is a [I x K] matrix
// S is a [K x 1] vector (diagonal matrix)
// V is a [J x K] matrix

// Sanity checks
if (U.cols() != K)
throw std::invalid_argument("SvdSignChoice: U has wrong dimensions");

if (V.cols() != K)
throw std::invalid_argument("SvdSignChoice: V has wrong dimensions");

if (X.rows() != I || X.cols() != J)
throw std::invalid_argument("SvdSignChoice: X dimensions do not match SVD input");

// Sign determination loop
//
Eigen::VectorXd sL(K), sR(K);
sL.setZero();
sR.setZero();

// Get projections from left and right singular vectors onto data
// matrix
//
#pragma omp parallel for
for (int k=0; k<K; k++) {
// Remove all but target dimension for numerical stability
auto S1 = S; S1(k) = 0.0;
auto Y = X - U * S1.asDiagonal() * V.transpose();

// d_j = U^T_k * Y_j
Eigen::VectorXd dL = Y.transpose() * U.col(k);

// sum of sgn(dL_j)*dL_j^2
sL(k) += dL.dot(dL.cwiseAbs());

// d_i = V^T_k * (Y^T)_i
Eigen::VectorXd dR = Y * V.col(k);

// sum of sgn(dR_i)*dR_i^2
sR(k) += dR.dot(dR.cwiseAbs());
}

auto sgn = [](double val) -> int
{
return (0.0 < val) - (val < 0.0);
};

// Determine and apply the sign correction
//
#pragma omp parallel for
for (int k=0; k<K; k++) {
// If signs are opposite, flip the one with the smaller absolute
// value
//
if (sL(k)*sR(k) < 0.0) {
if (std::abs(sL(k)) < std::abs(sR(k)))
sL(k) = -sL(k);
else
sR(k) = -sR(k);
}

// Apply
U.col(k) *= sgn(sL(k));
V.col(k) *= sgn(sR(k));
}

// Done
}

void SvdSignChoice
(const Eigen::MatrixXd& X,
Eigen::MatrixXd& U, const Eigen::VectorXd& S, const Eigen::MatrixXd& V)
{
// SDV dimensions
int I = U.rows();
int J = V.rows();
int K = S.size();

// Dimensions
// ----------
// X is a [I x J] data matrix
// U is a [I x K] matrix
// S is a [K x 1] vector (diagonal matrix)
// V is a [J x K] matrix

// Sanity checks
if (U.cols() != K)
throw std::invalid_argument("SvdSignChoice: U has wrong dimensions");

if (V.cols() != K)
throw std::invalid_argument("SvdSignChoice: V has wrong dimensions");

if (X.rows() != I || X.cols() != J)
throw std::invalid_argument("SvdSignChoice: X dimensions do not match SVD input");

// Sign determination loop
//
Eigen::VectorXd sL(K), sR(K);
sL.setZero();
sR.setZero();

// Get projections from left and right singular vectors onto data
// matrix
//
#pragma omp parallel for
for (int k=0; k<K; k++) {
// Remove all but target dimension for numerical stability
auto S1 = S; S1(k) = 0.0;
auto Y = X - U * S1.asDiagonal() * V.transpose();

// d_j = U^T_k * Y_j
Eigen::VectorXd dL = Y.transpose() * U.col(k);

// sum of sgn(dL_j)*dL_j^2
sL(k) += dL.dot(dL.cwiseAbs());

// d_i = V^T_k * (Y^T)_i
Eigen::VectorXd dR = Y * V.col(k);

// sum of sgn(dR_i)*dR_i^2
sR(k) += dR.dot(dR.cwiseAbs());
}

auto sgn = [](double val) -> int
{
return (0.0 < val) - (val < 0.0);
};

// Determine and apply the sign correction
//
#pragma omp parallel for
for (int k=0; k<K; k++) {
// If signs are opposite, flip the one with the smaller absolute
// value
//
if (sL(k)*sR(k) < 0.0) {
if (std::abs(sL(k)) < std::abs(sR(k)))
sL(k) = -sL(k);
else
sR(k) = -sR(k);
}

// Apply
U.col(k) *= sgn(sL(k));
}

// Done
}

void SvdSignChoice
(const Eigen::MatrixXd& X,
const Eigen::MatrixXd& U, const Eigen::VectorXd& S, Eigen::MatrixXd& V)
{
// SDV dimensions
int I = U.rows();
int J = V.rows();
int K = S.size();

// Dimensions
// ----------
// X is a [I x J] data matrix
// U is a [I x K] matrix
// S is a [K x 1] vector (diagonal matrix)
// V is a [J x K] matrix

// Sanity checks
if (U.cols() != K)
throw std::invalid_argument("SvdSignChoice: U has wrong dimensions");

if (V.cols() != K)
throw std::invalid_argument("SvdSignChoice: V has wrong dimensions");

if (X.rows() != I || X.cols() != J)
throw std::invalid_argument("SvdSignChoice: X dimensions do not match SVD input");

// Sign determination loop
//
Eigen::VectorXd sL(K), sR(K);
sL.setZero();
sR.setZero();

// Get projections from left and right singular vectors onto data
// matrix
//
#pragma omp parallel for
for (int k=0; k<K; k++) {
// Remove all but target dimension for numerical stability
auto S1 = S; S1(k) = 0.0;
auto Y = X - U * S1.asDiagonal() * V.transpose();

// d_j = U^T_k * Y_j
Eigen::VectorXd dL = Y.transpose() * U.col(k);

// sum of sgn(dL_j)*dL_j^2
sL(k) += dL.dot(dL.cwiseAbs());

// d_i = V^T_k * (Y^T)_i
Eigen::VectorXd dR = Y * V.col(k);

// sum of sgn(dR_i)*dR_i^2
sR(k) += dR.dot(dR.cwiseAbs());
}

auto sgn = [](double val) -> int
{
return (0.0 < val) - (val < 0.0);
};

// Determine and apply the sign correction
//
#pragma omp parallel for
for (int k=0; k<K; k++) {
// If signs are opposite, flip the one with the smaller absolute
// value
//
if (sL(k)*sR(k) < 0.0) {
if (std::abs(sL(k)) < std::abs(sR(k)))
sL(k) = -sL(k);
else
sR(k) = -sR(k);
}

// Apply
V.col(k) *= sgn(sR(k));
}

// Done
}

}
3 changes: 2 additions & 1 deletion expui/expMSSA.H
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ namespace MSSA
//! Primary MSSA analysis
void mssa_analysis();

bool computed, reconstructed, trajectory;
//! MSSA control flags
bool computed, reconstructed, trajectory, useSignChoice;

//! The reconstructed coefficients for each PC
std::map<Key, Eigen::MatrixXd, mSSAkeyCompare> RC;
Expand Down
29 changes: 24 additions & 5 deletions expui/expMSSA.cc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@

namespace MSSA {

// Update sign of left vectors U only
void SvdSignChoice
(const Eigen::MatrixXd& X,
Eigen::MatrixXd& U, const Eigen::VectorXd& S, const Eigen::MatrixXd& V);

// Update sign of right vectors V only
void SvdSignChoice
(const Eigen::MatrixXd& X,
const Eigen::MatrixXd& U, const Eigen::VectorXd& S, Eigen::MatrixXd& V);

Eigen::MatrixXd expMSSA::wCorrKey(const Key& key)
{
if (RC.find(key)==RC.end()) {
Expand Down Expand Up @@ -289,52 +299,59 @@ namespace MSSA {
//
if (params["Jacobi"]) {
// -->Using Jacobi
if (trajectory) { // Trajectory matrix
if (trajectory) { // Trajectory matrix
auto YY = Y/Scale;
Eigen::JacobiSVD<Eigen::MatrixXd>
svd(YY, Eigen::ComputeThinU | Eigen::ComputeThinV);
S = svd.singularValues();
U = svd.matrixV();
if (useSignChoice) SvdSignChoice(YY, svd.matrixU(), S, U);
}
else { // Covariance matrix
Eigen::JacobiSVD<Eigen::MatrixXd>
svd(cov, Eigen::ComputeThinU | Eigen::ComputeThinV);
S = svd.singularValues();
U = svd.matrixU();
if (useSignChoice) SvdSignChoice(cov, U, S, svd.matrixV());
}
} else if (params["BDCSVD"]) {
// -->Using BDC
if (trajectory) { // Trajectory matrix
if (trajectory) { // Trajectory matrix
auto YY = Y/Scale;
Eigen::BDCSVD<Eigen::MatrixXd>
svd(YY, Eigen::ComputeThinU | Eigen::ComputeThinV);
S = svd.singularValues();
U = svd.matrixV();
if (useSignChoice) SvdSignChoice(YY, svd.matrixU(), S, U);
}
else { // Covariance matrix
Eigen::BDCSVD<Eigen::MatrixXd>
svd(cov, Eigen::ComputeThinU | Eigen::ComputeThinV);
S = svd.singularValues();
U = svd.matrixU();
if (useSignChoice) SvdSignChoice(cov, U, S, svd.matrixV());
}
} else {
// -->Use Random approximation algorithm from Halko, Martinsson,
// and Tropp
if (trajectory) { // Trajectory matrix
if (trajectory) { // Trajectory matrix
auto YY = Y/Scale;
RedSVD::RedSVD<Eigen::MatrixXd> svd(YY, srank);
S = svd.singularValues();
U = svd.matrixV();
if (useSignChoice) SvdSignChoice(YY, svd.matrixU(), S, U);
}
else { // Covariance matrix
if (params["RedSym"]) {
RedSVD::RedSymEigen<Eigen::MatrixXd> eigen(cov, srank);
S = eigen.eigenvalues().reverse();
U = eigen.eigenvectors().rowwise().reverse();
if (useSignChoice) SvdSignChoice(cov, U, S, U.transpose());
} else {
RedSVD::RedSVD<Eigen::MatrixXd> svd(cov, srank);
S = svd.singularValues();
U = svd.matrixU();
if (useSignChoice) SvdSignChoice(cov, U, S, U.transpose());
}
}
}
Expand Down Expand Up @@ -1247,6 +1264,7 @@ namespace MSSA {
"Traj",
"RedSym",
"rank",
"Sign",
"allchan",
"distance",
"flip",
Expand Down Expand Up @@ -1807,7 +1825,7 @@ namespace MSSA {
}


expMSSA::expMSSA(const mssaConfig& config, int nW, int nPC, const std::string flags) : numW(nW), npc(nPC), trajectory(true)
expMSSA::expMSSA(const mssaConfig& config, int nW, int nPC, const std::string flags) : numW(nW), npc(nPC), trajectory(true), useSignChoice(true)
{
// Parse the YAML string
//
Expand All @@ -1826,7 +1844,8 @@ namespace MSSA {

// Set the SVD strategy for mSSA
//
if (params["Traj"]) trajectory = params["Traj"].as<bool>();
if (params["Traj"]) trajectory = params["Traj"].as<bool>();
if (params["Sign"]) useSignChoice = params["Sign"].as<bool>();

// std::cout << "Trajectory is " << std::boolalpha << trajectory
// << std::endl;
Expand Down

0 comments on commit dccf0b8

Please sign in to comment.