11#include " MorphologicalDeviationScore.h"
22#include < Logging.h>
3-
43namespace shapeworks {
54
65MorphologicalDeviationScore::MorphologicalDeviationScore () = default ;
@@ -32,44 +31,31 @@ bool MorphologicalDeviationScore::FitPPCA(const Eigen::MatrixXd& X) {
3231 X_c, Eigen::ComputeThinU | Eigen::ComputeThinV);
3332 Eigen::VectorXd s = svd.singularValues (); // (r,)
3433 Eigen::MatrixXd V = svd.matrixV (); // (d x r)
35- Eigen::VectorXd eigvals =
36- (s.array ().square ()) / std::max (1 , n - 1 ); // (r,)
37-
38- double total_var = eigvals.sum ();
39- if (total_var <= 0.0 ) {
40- throw std::runtime_error (
41- " Total variance is non-positive. Cannot fit PCA on control shapes" );
42- }
43-
44- Eigen::VectorXd cum_var = eigvals;
45- for (int i = 1 ; i < cum_var.size (); ++i) {
46- cum_var (i) += cum_var (i - 1 );
47- }
48- cum_var /= total_var;
49-
50- int q = 0 ;
51- for (int i = 0 ; i < cum_var.size (); ++i) {
52- if (cum_var (i) > 0.95 ) {
53- q = i + 1 ;
54- break ;
55- }
56- }
57- if (q == 0 ) q = cum_var.size ();
58-
59- eigvals_ = eigvals.head (q); // (q,)
60- components_ = V.leftCols (q); // (d x q)
61-
62- if (q < d) {
63- double rem_sum = eigvals.tail (eigvals.size () - q).sum ();
64- noise_variance_ = rem_sum / (d - q);
65- } else {
66- noise_variance_ = 0.0 ;
67- }
34+ Eigen::VectorXd eigvals = (s.array ().square ()) / (n - 1 );
35+
36+ // save_matrix(X, std::string(DEBUG_FILES_PTH) + "/X.npy");
37+ // save_matrix(X_c, std::string(DEBUG_FILES_PTH) + "/X_c.npy");
38+ // save_matrix(V, std::string(DEBUG_FILES_PTH) + "/V.npy");
39+ // save_vector(eigvals, std::string(DEBUG_FILES_PTH) + "/eigvals.npy");
40+
41+ Eigen::VectorXd cumsum = eigvals;
42+ for (int i = 1 ; i < eigvals.size (); ++i) cumsum[i] += cumsum[i - 1 ];
43+ Eigen::VectorXd cumvar = cumsum / eigvals.sum ();
44+ n_components_ = 0 ;
45+ while (n_components_ < cumvar.size () && cumvar[n_components_] <= retained_variance_ratio_)
46+ ++n_components_;
47+ int q = n_components_;
48+ principal_components_variance_ = eigvals.head (q); // (q,)
49+ components_ = V.leftCols (q); // (d x q)
50+ // save_matrix(components_, std::string(DEBUG_FILES_PTH) + "/components.npy");
51+ // save_vector(principal_components_variance_,
52+ // std::string(DEBUG_FILES_PTH) + "/eigvals.npy");
53+ noise_variance_ = eigvals.tail (eigvals.size () - q).sum () / double (d - q);
6854 return true ;
6955
7056 } catch (std::exception& e) {
7157 SW_ERROR (
72- " Exception in SVD computation for early stopping score function {}" ,
58+ " SVD computation failed for MorphologicalDeviationScore: {}" ,
7359 e.what ());
7460 return false ;
7561 }
@@ -84,7 +70,7 @@ Eigen::MatrixXd MorphologicalDeviationScore::ComputeCovarianceMatrix() {
8470 return noise_variance_ * Eigen::MatrixXd::Identity (d, d);
8571 }
8672
87- Eigen::MatrixXd diag_lambda = eigvals_ .asDiagonal (); // (q x q)
73+ Eigen::MatrixXd diag_lambda = principal_components_variance_ .asDiagonal (); // (q x q)
8874 Eigen::MatrixXd cov =
8975 components_ *
9076 (diag_lambda - noise_variance_ * Eigen::MatrixXd::Identity (q, q)) *
@@ -99,56 +85,52 @@ Eigen::MatrixXd MorphologicalDeviationScore::ComputePrecisionMatrix() {
9985 const int d = components_.rows ();
10086 const int q = components_.cols ();
10187
102- if (q == 0 ) {
103- if (noise_variance_ <= 0.0 )
104- throw std::runtime_error (
105- " Noise variance is zero; precision undefined." );
106- return (1.0 / noise_variance_) * Eigen::MatrixXd::Identity (d, d);
107- }
88+ Eigen::MatrixXd A_inv = (1 / noise_variance_) * Eigen::MatrixXd::Identity (d, d);
10889
109- Eigen::MatrixXd P = components_ * components_.transpose (); // (d x d)
110- Eigen::MatrixXd term_principal = components_ *
111- eigvals_.cwiseInverse ().asDiagonal () *
112- components_.transpose (); // (d x d)
90+ Eigen::MatrixXd U = components_;
91+ Eigen::MatrixXd C_inv = principal_components_variance_.cwiseInverse ().asDiagonal ();
92+ Eigen::MatrixXd V = components_.transpose ();
93+ // Woodbury matrix identity
94+ // (A + U C V)^(-1)
95+ // = A^(-1)
96+ // - A^(-1) * U * (C^(-1) + V * A^(-1) * U)^(-1) * V * A^(-1)
11397
114- if (noise_variance_ <= 0.0 ) {
115- if (q != d)
116- throw std::runtime_error (
117- " Noise variance is zero and q < d; covariance is singular." );
118- return term_principal;
119- }
98+ Eigen::MatrixXd precision = A_inv - A_inv * U * (C_inv + V * A_inv * U).inverse () * V * A_inv;
99+ // save_matrix(precision, std::string(DEBUG_FILES_PTH)+"/precision.npy");
120100
121- Eigen::MatrixXd precision =
122- term_principal + (1.0 / noise_variance_) *
123- (Eigen::MatrixXd::Identity (d, d) - P); // (d x d)
124101 return precision;
125-
126102 } catch (std::exception& e) {
127103 SW_ERROR (
128- " Exception in computation of precision matrix in early stopping score "
129- " function {}" ,
104+ " Failed to compute precision matrix in the early stopping score function: {}" ,
130105 e.what ());
131106 return Eigen::MatrixXd ();
132107 }
133108}
134109
135110// ---------------------------------------------------------------------------
136- Eigen::VectorXd MorphologicalDeviationScore::GetMahalanobisDistance (
111+ Eigen::VectorXd MorphologicalDeviationScore::GetMorphoDevScore (
137112 const Eigen::MatrixXd& X) {
138113 try {
114+
139115 if (!is_fitted_) {
140116 throw std::runtime_error (
141- " PCA model has not been fitted yet on control shapes." );
117+ " PPCA model is not fitted on control shapes." );
142118 }
143119 const int n = X.rows ();
144- Eigen::MatrixXd X_c = X.rowwise () - mean_;
145- Eigen::VectorXd dist (n);
146- for (int i = 0 ; i < n; ++i) {
147- Eigen::RowVectorXd xi = X_c.row (i);
148- dist (i) = std::sqrt (
149- std::max (0.0 , (xi * precision_matrix_ * xi.transpose ())(0 )));
150- }
151- return dist;
120+ // Eigen::MatrixXd X_c = X.rowwise() - mean_;
121+ // Eigen::VectorXd dist(n);
122+ // for (int i = 0; i < n; ++i) {
123+ // Eigen::RowVectorXd xi = X_c.row(i);
124+ // dist(i) = std::sqrt(
125+ // std::max(0.0, (xi * precision_matrix_ * xi.transpose())(0)));
126+ // }
127+
128+ Eigen::MatrixXd X_bar = X.rowwise () - mean_;
129+ Eigen::MatrixXd Y = X_bar * precision_matrix_; // (n_samples × n_features)
130+ Eigen::VectorXd sq_mahal = (X_bar.array () * Y.array ()).rowwise ().sum (); // quadratic forms
131+ Eigen::VectorXd mahalanobis = sq_mahal.array ().sqrt (); // final distances
132+ return mahalanobis;
133+
152134 } catch (std::exception& e) {
153135 SW_ERROR (
154136 " Exception in computing Mahalanobis distance for early stopping score "
@@ -158,15 +140,4 @@ Eigen::VectorXd MorphologicalDeviationScore::GetMahalanobisDistance(
158140 }
159141}
160142
161- // ---------------------------------------------------------------------------
162- // double MorphologicalDeviationScore::GetDeviationScore(const Eigen::MatrixXd&
163- // X_test) {
164- // if (!is_fitted_) {
165- // throw std::runtime_error("PPCA model has not been fitted. Call
166- // SetControlShapes() first.");
167- // }
168- // Eigen::VectorXd dists = GetMahalanobisDistance(X_test);
169- // return dists(0);
170- // }
171-
172143} // namespace shapeworks
0 commit comments