|
28 | 28 |
|
29 | 29 | #include <Eigen/Dense> |
30 | 30 | #include <omp.h> |
| 31 | +#include <algorithm> |
31 | 32 | #include <cmath> |
| 33 | +#include <limits> |
32 | 34 | #include <fstream> |
33 | 35 | #include <sstream> |
34 | 36 | #include <string> |
@@ -1087,6 +1089,66 @@ TEST(PriorSegmentation, EstimateScale) { |
1087 | 1089 | EXPECT_GE(scale_std, 0.0); |
1088 | 1090 | } |
1089 | 1091 |
|
| 1092 | +TEST(PriorSegmentation, EstimateScaleMatchesExactNearestCenterReference) { |
| 1093 | + constexpr int n_cells = 6; |
| 1094 | + constexpr int mols_per_cell = 4; |
| 1095 | + Eigen::MatrixXd centers(3, n_cells); |
| 1096 | + centers.col(0) << 0.0, 0.0, 0.0; |
| 1097 | + centers.col(1) << 5.0, 0.0, 0.0; |
| 1098 | + centers.col(2) << 0.0, 7.0, 1.0; |
| 1099 | + centers.col(3) << 10.0, 1.0, 2.0; |
| 1100 | + centers.col(4) << 13.0, 5.0, 1.0; |
| 1101 | + centers.col(5) << 20.0, 0.0, 3.0; |
| 1102 | + |
| 1103 | + Eigen::MatrixXd pos(3, n_cells * mols_per_cell); |
| 1104 | + std::vector<int> assignment(n_cells * mols_per_cell); |
| 1105 | + const Eigen::Vector3d offsets[mols_per_cell] = { |
| 1106 | + Eigen::Vector3d(-0.1, -0.1, 0.0), |
| 1107 | + Eigen::Vector3d( 0.1, -0.1, 0.0), |
| 1108 | + Eigen::Vector3d(-0.1, 0.1, 0.0), |
| 1109 | + Eigen::Vector3d( 0.1, 0.1, 0.0) |
| 1110 | + }; |
| 1111 | + for (int c = 0; c < n_cells; ++c) { |
| 1112 | + for (int m = 0; m < mols_per_cell; ++m) { |
| 1113 | + const int idx = c * mols_per_cell + m; |
| 1114 | + pos.col(idx) = centers.col(c) + offsets[m]; |
| 1115 | + assignment[idx] = c + 1; |
| 1116 | + } |
| 1117 | + } |
| 1118 | + |
| 1119 | + std::vector<double> radii(n_cells); |
| 1120 | + for (int i = 0; i < n_cells; ++i) { |
| 1121 | + double min_dist = std::numeric_limits<double>::max(); |
| 1122 | + for (int j = 0; j < n_cells; ++j) { |
| 1123 | + if (i == j) continue; |
| 1124 | + min_dist = std::min(min_dist, (centers.col(i) - centers.col(j)).norm()); |
| 1125 | + } |
| 1126 | + radii[i] = min_dist / 2.0; |
| 1127 | + } |
| 1128 | + std::sort(radii.begin(), radii.end()); |
| 1129 | + const double expected_scale = (radii[n_cells / 2 - 1] + radii[n_cells / 2]) / 2.0; |
| 1130 | + |
| 1131 | + std::vector<double> abs_devs(n_cells); |
| 1132 | + for (int i = 0; i < n_cells; ++i) { |
| 1133 | + abs_devs[i] = std::abs(radii[i] - expected_scale); |
| 1134 | + } |
| 1135 | + std::sort(abs_devs.begin(), abs_devs.end()); |
| 1136 | + const double expected_std = |
| 1137 | + ((abs_devs[n_cells / 2 - 1] + abs_devs[n_cells / 2]) / 2.0) * 1.4826; |
| 1138 | + |
| 1139 | + int old_threads = omp_get_max_threads(); |
| 1140 | + omp_set_num_threads(1); |
| 1141 | + auto [scale_1, scale_std_1] = baysor::estimate_scale_from_assignment(pos, assignment, mols_per_cell); |
| 1142 | + omp_set_num_threads(4); |
| 1143 | + auto [scale_4, scale_std_4] = baysor::estimate_scale_from_assignment(pos, assignment, mols_per_cell); |
| 1144 | + omp_set_num_threads(old_threads); |
| 1145 | + |
| 1146 | + EXPECT_NEAR(scale_1, expected_scale, 1e-10); |
| 1147 | + EXPECT_NEAR(scale_std_1, expected_std, 1e-10); |
| 1148 | + EXPECT_NEAR(scale_4, expected_scale, 1e-10); |
| 1149 | + EXPECT_NEAR(scale_std_4, expected_std, 1e-10); |
| 1150 | +} |
| 1151 | + |
1090 | 1152 | // ============================================================================ |
1091 | 1153 | // Top-level prior segmentation loading |
1092 | 1154 | // ============================================================================ |
@@ -1683,6 +1745,47 @@ TEST(Triangulation, AdjacencyList3DKNN) { |
1683 | 1745 | // Boundary estimation |
1684 | 1746 | // ============================================================================ |
1685 | 1747 |
|
| 1748 | +static std::vector<std::pair<double, double>> canonical_polygon_vertices(const Eigen::MatrixXd& poly) { |
| 1749 | + std::vector<std::pair<double, double>> vertices; |
| 1750 | + vertices.reserve(static_cast<size_t>(poly.cols())); |
| 1751 | + for (int i = 0; i < poly.cols(); ++i) { |
| 1752 | + vertices.push_back({poly(0, i), poly(1, i)}); |
| 1753 | + } |
| 1754 | + if (vertices.empty()) return vertices; |
| 1755 | + |
| 1756 | + auto canonicalize_direction = [](std::vector<std::pair<double, double>> vals) { |
| 1757 | + const auto it = std::min_element(vals.begin(), vals.end()); |
| 1758 | + std::rotate(vals.begin(), it, vals.end()); |
| 1759 | + return vals; |
| 1760 | + }; |
| 1761 | + |
| 1762 | + auto forward = canonicalize_direction(vertices); |
| 1763 | + std::reverse(vertices.begin(), vertices.end()); |
| 1764 | + auto backward = canonicalize_direction(vertices); |
| 1765 | + return std::lexicographical_compare(backward.begin(), backward.end(), forward.begin(), forward.end()) |
| 1766 | + ? backward |
| 1767 | + : forward; |
| 1768 | +} |
| 1769 | + |
| 1770 | +static void expect_polygon_collection_near( |
| 1771 | + const baysor::PolygonCollection& actual, |
| 1772 | + const baysor::PolygonCollection& expected, |
| 1773 | + double tol = 1e-10 |
| 1774 | +) { |
| 1775 | + ASSERT_EQ(actual.size(), expected.size()); |
| 1776 | + for (const auto& [cell, expected_poly] : expected) { |
| 1777 | + auto it = actual.find(cell); |
| 1778 | + ASSERT_NE(it, actual.end()) << cell; |
| 1779 | + auto actual_vertices = canonical_polygon_vertices(it->second); |
| 1780 | + auto expected_vertices = canonical_polygon_vertices(expected_poly); |
| 1781 | + ASSERT_EQ(actual_vertices.size(), expected_vertices.size()) << cell; |
| 1782 | + for (size_t i = 0; i < expected_vertices.size(); ++i) { |
| 1783 | + EXPECT_NEAR(actual_vertices[i].first, expected_vertices[i].first, tol) << cell; |
| 1784 | + EXPECT_NEAR(actual_vertices[i].second, expected_vertices[i].second, tol) << cell; |
| 1785 | + } |
| 1786 | + } |
| 1787 | +} |
| 1788 | + |
1686 | 1789 | TEST(BoundaryEstimation, BoundaryPolygonsAuto3DPerZ) { |
1687 | 1790 | Eigen::MatrixXd pos(3, 8); |
1688 | 1791 | pos.col(0) << 0.0, 0.0, 0.0; |
@@ -1724,6 +1827,44 @@ TEST(BoundaryEstimation, BoundaryPolygonsAuto3DPerZ) { |
1724 | 1827 | EXPECT_TRUE(saw_z1); |
1725 | 1828 | } |
1726 | 1829 |
|
| 1830 | +TEST(BoundaryEstimation, BoundaryPolygonsAutoStableAcrossThreadCounts) { |
| 1831 | + constexpr int n_cells = 4; |
| 1832 | + constexpr int points_per_cell = 4; |
| 1833 | + Eigen::MatrixXd pos(3, n_cells * points_per_cell); |
| 1834 | + std::vector<int> assignment(n_cells * points_per_cell); |
| 1835 | + std::vector<std::string> cell_names = {"cell_1", "cell_2", "cell_3", "cell_4"}; |
| 1836 | + |
| 1837 | + auto add_square = [&](int cell, double x0, double y0, double z) { |
| 1838 | + const int offset = (cell - 1) * points_per_cell; |
| 1839 | + pos.col(offset + 0) << x0, y0, z; |
| 1840 | + pos.col(offset + 1) << x0 + 2.0, y0, z; |
| 1841 | + pos.col(offset + 2) << x0 + 2.0, y0 + 2.0, z; |
| 1842 | + pos.col(offset + 3) << x0, y0 + 2.0, z; |
| 1843 | + for (int i = 0; i < points_per_cell; ++i) assignment[offset + i] = cell; |
| 1844 | + }; |
| 1845 | + |
| 1846 | + add_square(1, 0.0, 0.0, 0.0); |
| 1847 | + add_square(2, 10.0, 0.0, 0.0); |
| 1848 | + add_square(3, 0.0, 10.0, 1.0); |
| 1849 | + add_square(4, 10.0, 10.0, 1.0); |
| 1850 | + |
| 1851 | + int old_threads = omp_get_max_threads(); |
| 1852 | + omp_set_num_threads(1); |
| 1853 | + auto [joined_1, stack_1] = baysor::boundary_polygons_auto( |
| 1854 | + pos, assignment, /*estimate_per_z=*/true, &cell_names, /*verbose=*/false); |
| 1855 | + omp_set_num_threads(4); |
| 1856 | + auto [joined_4, stack_4] = baysor::boundary_polygons_auto( |
| 1857 | + pos, assignment, /*estimate_per_z=*/true, &cell_names, /*verbose=*/false); |
| 1858 | + omp_set_num_threads(old_threads); |
| 1859 | + |
| 1860 | + expect_polygon_collection_near(joined_4, joined_1); |
| 1861 | + ASSERT_EQ(stack_4.size(), stack_1.size()); |
| 1862 | + for (size_t i = 0; i < stack_1.size(); ++i) { |
| 1863 | + EXPECT_EQ(stack_4[i].first, stack_1[i].first); |
| 1864 | + expect_polygon_collection_near(stack_4[i].second, stack_1[i].second); |
| 1865 | + } |
| 1866 | +} |
| 1867 | + |
1727 | 1868 | TEST(BoundaryEstimation, BoundaryPolygonsFromGridKeepsTouchingLabelsSeparate) { |
1728 | 1869 | Eigen::Matrix<uint32_t, Eigen::Dynamic, Eigen::Dynamic> grid(3, 4); |
1729 | 1870 | grid << |
@@ -2488,6 +2629,59 @@ TEST(MoleculeClustering, GraphPartitionToTargetReturnsRequestedCountForLouvainAn |
2488 | 2629 | EXPECT_EQ(lei_summary.final_clusters, 3); |
2489 | 2630 | } |
2490 | 2631 |
|
| 2632 | +TEST(MoleculeClustering, GraphPartitionToTargetStableAcrossThreadCounts) { |
| 2633 | + constexpr int patches = 8; |
| 2634 | + constexpr int patch_size = 6; |
| 2635 | + constexpr int n = patches * patch_size; |
| 2636 | + |
| 2637 | + Eigen::MatrixXf mol_vecs(3, n); |
| 2638 | + std::vector<double> confidence(n, 1.0); |
| 2639 | + for (int p = 0; p < patches; ++p) { |
| 2640 | + Eigen::Vector3f center; |
| 2641 | + switch (p % 4) { |
| 2642 | + case 0: center << 1.0f, 0.1f, 0.0f; break; |
| 2643 | + case 1: center << 0.1f, 1.0f, 0.1f; break; |
| 2644 | + case 2: center << -1.0f, 0.2f, 0.0f; break; |
| 2645 | + default: center << 0.2f, -1.0f, 0.1f; break; |
| 2646 | + } |
| 2647 | + for (int i = 0; i < patch_size; ++i) { |
| 2648 | + const int idx = p * patch_size + i; |
| 2649 | + mol_vecs.col(idx) = center; |
| 2650 | + mol_vecs(0, idx) += 0.01f * static_cast<float>(i); |
| 2651 | + mol_vecs(1, idx) -= 0.005f * static_cast<float>(i); |
| 2652 | + confidence[idx] = 0.8 + 0.01 * static_cast<double>(i); |
| 2653 | + } |
| 2654 | + } |
| 2655 | + |
| 2656 | + auto adj = baysor::build_knn_similarity_graph(mol_vecs, confidence, /*k=*/5); |
| 2657 | + |
| 2658 | + for (auto method : {baysor::ClusterMethod::Louvain, baysor::ClusterMethod::Leiden}) { |
| 2659 | + int old_threads = omp_get_max_threads(); |
| 2660 | + omp_set_num_threads(1); |
| 2661 | + baysor::GraphClusteringSummary summary_1; |
| 2662 | + auto assignment_1 = baysor::graph_partition_to_target( |
| 2663 | + adj, mol_vecs, confidence, method, |
| 2664 | + /*target_clusters=*/4, /*resolution_seed=*/1.0, /*max_passes=*/100, &summary_1 |
| 2665 | + ); |
| 2666 | + omp_set_num_threads(4); |
| 2667 | + baysor::GraphClusteringSummary summary_4; |
| 2668 | + auto assignment_4 = baysor::graph_partition_to_target( |
| 2669 | + adj, mol_vecs, confidence, method, |
| 2670 | + /*target_clusters=*/4, /*resolution_seed=*/1.0, /*max_passes=*/100, &summary_4 |
| 2671 | + ); |
| 2672 | + omp_set_num_threads(old_threads); |
| 2673 | + |
| 2674 | + EXPECT_EQ(assignment_4, assignment_1); |
| 2675 | + EXPECT_EQ(summary_4.micro_clusters, summary_1.micro_clusters); |
| 2676 | + EXPECT_EQ(summary_4.final_clusters, summary_1.final_clusters); |
| 2677 | + EXPECT_DOUBLE_EQ(summary_4.chosen_resolution, summary_1.chosen_resolution); |
| 2678 | + ASSERT_EQ(summary_4.move_fracs.size(), summary_1.move_fracs.size()); |
| 2679 | + for (size_t i = 0; i < summary_1.move_fracs.size(); ++i) { |
| 2680 | + EXPECT_DOUBLE_EQ(summary_4.move_fracs[i], summary_1.move_fracs[i]); |
| 2681 | + } |
| 2682 | + } |
| 2683 | +} |
| 2684 | + |
2491 | 2685 | TEST(MoleculeClustering, DispatcherReturnsEmptyForNoneMethod) { |
2492 | 2686 | Eigen::MatrixXd pos(2, 2); |
2493 | 2687 | pos << |
|
0 commit comments