11#include " MorphologicalDeviationScore.h"
2+
23#include < Logging.h>
34namespace shapeworks {
45
@@ -8,11 +9,9 @@ MorphologicalDeviationScore::MorphologicalDeviationScore() = default;
89bool MorphologicalDeviationScore::SetControlShapes (const Eigen::MatrixXd& X) {
910 try {
1011 bool ppca_status = FitPPCA (X);
11- precision_matrix_ = ComputePrecisionMatrix ();
1212 is_fitted_ = ppca_status;
1313 } catch (std::exception& e) {
14- SW_ERROR (" Exception in setting control shapes for early stopping {}" ,
15- e.what ());
14+ SW_ERROR (" Exception in setting control shapes for early stopping {}" , e.what ());
1615 return false ;
1716 }
1817 return is_fitted_;
@@ -27,108 +26,79 @@ bool MorphologicalDeviationScore::FitPPCA(const Eigen::MatrixXd& X) {
2726 Eigen::MatrixXd X_c = X.rowwise () - mean_; // (n x d)
2827
2928 try {
30- Eigen::JacobiSVD<Eigen::MatrixXd> svd (
31- X_c, Eigen::ComputeThinU | Eigen::ComputeThinV);
29+ Eigen::JacobiSVD<Eigen::MatrixXd> svd (X_c, Eigen::ComputeThinU | Eigen::ComputeThinV);
3230 Eigen::VectorXd s = svd.singularValues (); // (r,)
3331 Eigen::MatrixXd V = svd.matrixV (); // (d x r)
3432 Eigen::VectorXd eigvals = (s.array ().square ()) / (n - 1 );
3533
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-
4134 Eigen::VectorXd cumsum = eigvals;
4235 for (int i = 1 ; i < eigvals.size (); ++i) cumsum[i] += cumsum[i - 1 ];
4336 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);
54- return true ;
37+ int q = 0 ;
38+ while (q < cumvar.size () && cumvar[q] <= retained_variance_ratio_) ++q;
5539
56- } catch (std::exception& e) {
57- SW_ERROR (
58- " SVD computation failed for MorphologicalDeviationScore: {}" ,
59- e.what ());
60- return false ;
61- }
62- }
40+ if (q == 0 ) {
41+ SW_ERROR (" PPCA: no components retained at {}% variance" , retained_variance_ratio_ * 100 );
42+ return false ;
43+ }
6344
64- // ---------------------------------------------------------------------------
65- Eigen::MatrixXd MorphologicalDeviationScore::ComputeCovarianceMatrix () {
66- const int d = components_.rows ();
67- const int q = components_.cols ();
45+ // Determine effective rank: trim structurally zero eigenvalues
46+ const double eps = 1e-10 ;
47+ int rank = eigvals.size ();
48+ while (rank > q && eigvals[rank - 1 ] < eps) {
49+ --rank;
50+ }
6851
69- if (q == 0 ) {
70- return noise_variance_ * Eigen::MatrixXd::Identity (d, d);
71- }
52+ if (rank <= q) {
53+ SW_ERROR (" PPCA: effective rank ({}) <= retained components ({}), cannot estimate noise variance" , rank, q);
54+ return false ;
55+ }
7256
73- Eigen::MatrixXd diag_lambda = principal_components_variance_.asDiagonal (); // (q x q)
74- Eigen::MatrixXd cov =
75- components_ *
76- (diag_lambda - noise_variance_ * Eigen::MatrixXd::Identity (q, q)) *
77- components_.transpose () +
78- noise_variance_ * Eigen::MatrixXd::Identity (d, d); // (d x d)
79- return cov;
80- }
57+ // Noise variance: average of excluded-but-real eigenvalues
58+ // Uses (rank - q) denominator instead of (d - q) to avoid dilution
59+ // by structurally zero dimensions beyond the data rank
60+ noise_variance_ = eigvals.segment (q, rank - q).sum () / double (rank - q);
8161
82- // ---------------------------------------------------------------------------
83- Eigen::MatrixXd MorphologicalDeviationScore::ComputePrecisionMatrix () {
84- try {
85- const int d = components_.rows ();
86- const int q = components_.cols ();
62+ // Store full rank-dimensional basis for projection-based scoring
63+ all_components_ = V.leftCols (rank); // (d x rank)
8764
88- Eigen::MatrixXd A_inv = (1 / noise_variance_) * Eigen::MatrixXd::Identity (d, d);
65+ // Build per-dimension precision weights
66+ precision_weights_.resize (rank);
67+ for (int i = 0 ; i < q; ++i) {
68+ precision_weights_[i] = 1.0 / eigvals[i];
69+ }
70+ for (int i = q; i < rank; ++i) {
71+ precision_weights_[i] = 1.0 / noise_variance_;
72+ }
8973
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)
74+ SW_LOG (" PPCA fit: n={}, d={}, rank={}, q={}, noise_var={:.6f}" , n, d, rank, q, noise_variance_);
9775
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");
76+ return true ;
10077
101- return precision;
10278 } catch (std::exception& e) {
103- SW_ERROR (
104- " Failed to compute precision matrix in the early stopping score function: {}" ,
105- e.what ());
106- return Eigen::MatrixXd ();
79+ SW_ERROR (" SVD computation failed for MorphologicalDeviationScore: {}" , e.what ());
80+ return false ;
10781 }
10882}
10983
11084// ---------------------------------------------------------------------------
111- Eigen::VectorXd MorphologicalDeviationScore::GetMorphoDevScore (
112- const Eigen::MatrixXd& X) {
85+ Eigen::VectorXd MorphologicalDeviationScore::GetMorphoDevScore (const Eigen::MatrixXd& X) {
11386 try {
114-
11587 if (!is_fitted_) {
116- throw std::runtime_error (
117- " PPCA model is not fitted on control shapes." );
88+ throw std::runtime_error (" PPCA model is not fitted on control shapes." );
11889 }
119- const int n = X.rows ();
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
90+
91+ // Project into the rank-dimensional subspace, eliminating
92+ // structurally zero dimensions that cannot be estimated from data
93+ Eigen::MatrixXd X_bar = X.rowwise () - mean_; // (n x d)
94+ Eigen::MatrixXd Z = X_bar * all_components_; // (n x rank)
95+
96+ // Apply per-dimension precision weights: 1/lambda_i for retained PCs,
97+ // 1/noise_variance for excluded-but-real PCs
98+ Eigen::MatrixXd Z_weighted = Z.array ().rowwise () * precision_weights_.transpose ().array (); // (n x rank)
99+
100+ Eigen::VectorXd sq_mahal = (Z.array () * Z_weighted.array ()).rowwise ().sum (); // (n,)
101+ Eigen::VectorXd mahalanobis = sq_mahal.array ().max (0.0 ).sqrt ();
132102 return mahalanobis;
133103
134104 } catch (std::exception& e) {
@@ -140,4 +110,4 @@ Eigen::VectorXd MorphologicalDeviationScore::GetMorphoDevScore(
140110 }
141111}
142112
143- } // namespace shapeworks
113+ } // namespace shapeworks
0 commit comments