Skip to content

Commit 9aa2c0e

Browse files
committed
added parallelization to scale estimation, cluster resolution search, boundary polygon est
1 parent c1baba5 commit 9aa2c0e

4 files changed

Lines changed: 233 additions & 12 deletions

File tree

src/data_loading/prior_segmentation.cpp

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "baysor/data_loading/prior_segmentation.h"
2+
#include "baysor/processing/utils/utils.h"
23
#include "baysor/utils/general.h"
34

45
#include <algorithm>
@@ -385,14 +386,31 @@ std::pair<double, double> estimate_scale_from_assignment(
385386
std::to_string(n_centers) + " < 3). Please specify scale manually.");
386387
}
387388

388-
// For each center, find distance to nearest neighbor, then take radius = dist / 2
389+
Eigen::MatrixXd center_mat(n_dims, n_centers);
390+
for (int i = 0; i < n_centers; ++i) {
391+
center_mat.col(i) = centers[i];
392+
}
393+
394+
// For each center, find distance to nearest neighbor, then take radius = dist / 2.
395+
// This is the exact same nearest-neighbor definition as the all-pairs loop,
396+
// but uses the shared exact KD-tree implementation for large priors.
397+
auto knn = knn_parallel(center_mat, center_mat, std::min(8, n_centers), true);
389398
std::vector<double> radii(n_centers);
390399
for (int i = 0; i < n_centers; ++i) {
391400
double min_dist = std::numeric_limits<double>::max();
392-
for (int j = 0; j < n_centers; ++j) {
393-
if (i == j) continue;
394-
double dist = (centers[i] - centers[j]).norm();
395-
if (dist < min_dist) min_dist = dist;
401+
const auto& ids = knn.indices[i];
402+
const auto& dists = knn.distances[i];
403+
for (int j = 0; j < static_cast<int>(ids.size()); ++j) {
404+
if (ids[j] == i) continue;
405+
min_dist = dists[j];
406+
break;
407+
}
408+
if (min_dist == std::numeric_limits<double>::max()) {
409+
for (int j = 0; j < n_centers; ++j) {
410+
if (i == j) continue;
411+
double dist = (centers[i] - centers[j]).norm();
412+
if (dist < min_dist) min_dist = dist;
413+
}
396414
}
397415
radii[i] = min_dist / 2.0;
398416
}

src/processing/bmm_algorithm/molecule_clustering_louvain.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -789,10 +789,11 @@ std::vector<int> graph_partition_to_target(
789789
candidate_resolutions.end()
790790
);
791791

792-
std::vector<PartitionAttempt> attempts;
793-
attempts.reserve(candidate_resolutions.size());
794-
for (double res : candidate_resolutions) {
795-
attempts.push_back(run_graph_partition_once(graph, method, res, max_passes));
792+
std::vector<PartitionAttempt> attempts(candidate_resolutions.size());
793+
#pragma omp parallel for schedule(dynamic, 1)
794+
for (int i = 0; i < static_cast<int>(candidate_resolutions.size()); ++i) {
795+
attempts[i] = run_graph_partition_once(
796+
graph, method, candidate_resolutions[static_cast<size_t>(i)], max_passes);
796797
}
797798

798799
const PartitionAttempt* chosen = nullptr;

src/processing/data_processing/boundary_estimation.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <cmath>
1414
#include <limits>
1515
#include <numeric>
16+
#include <omp.h>
1617
#include <spdlog/spdlog.h>
1718
#include <sstream>
1819
#include <unordered_map>
@@ -391,9 +392,9 @@ PolygonCollection build_polygons_for_cells(
391392
}
392393
const double offset = mean_nn_dist * offset_rel;
393394

394-
PolygonCollection polygons;
395-
polygons.reserve(max_label);
395+
std::vector<Eigen::MatrixXd> polygons_by_cell(max_label);
396396

397+
#pragma omp parallel for schedule(dynamic, 32) if(!omp_in_parallel())
397398
for (int cid = 1; cid <= max_label; ++cid) {
398399
const auto& cell_ids = mids_per_cell[cid - 1];
399400
if (cell_ids.empty()) continue;
@@ -464,10 +465,17 @@ PolygonCollection build_polygons_for_cells(
464465
}
465466

466467
if (poly.cols() == 0) continue;
468+
polygons_by_cell[cid - 1] = std::move(poly);
469+
}
470+
471+
PolygonCollection polygons;
472+
polygons.reserve(max_label);
473+
for (int cid = 1; cid <= max_label; ++cid) {
474+
if (polygons_by_cell[cid - 1].cols() == 0) continue;
467475
const std::string cell_name = (cell_names && cid - 1 < static_cast<int>(cell_names->size()))
468476
? (*cell_names)[cid - 1]
469477
: default_cell_name(cid);
470-
polygons[cell_name] = std::move(poly);
478+
polygons[cell_name] = std::move(polygons_by_cell[cid - 1]);
471479
}
472480

473481
return polygons;

tests/test_main.cpp

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828

2929
#include <Eigen/Dense>
3030
#include <omp.h>
31+
#include <algorithm>
3132
#include <cmath>
33+
#include <limits>
3234
#include <fstream>
3335
#include <sstream>
3436
#include <string>
@@ -1087,6 +1089,66 @@ TEST(PriorSegmentation, EstimateScale) {
10871089
EXPECT_GE(scale_std, 0.0);
10881090
}
10891091

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+
10901152
// ============================================================================
10911153
// Top-level prior segmentation loading
10921154
// ============================================================================
@@ -1683,6 +1745,47 @@ TEST(Triangulation, AdjacencyList3DKNN) {
16831745
// Boundary estimation
16841746
// ============================================================================
16851747

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+
16861789
TEST(BoundaryEstimation, BoundaryPolygonsAuto3DPerZ) {
16871790
Eigen::MatrixXd pos(3, 8);
16881791
pos.col(0) << 0.0, 0.0, 0.0;
@@ -1724,6 +1827,44 @@ TEST(BoundaryEstimation, BoundaryPolygonsAuto3DPerZ) {
17241827
EXPECT_TRUE(saw_z1);
17251828
}
17261829

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+
17271868
TEST(BoundaryEstimation, BoundaryPolygonsFromGridKeepsTouchingLabelsSeparate) {
17281869
Eigen::Matrix<uint32_t, Eigen::Dynamic, Eigen::Dynamic> grid(3, 4);
17291870
grid <<
@@ -2488,6 +2629,59 @@ TEST(MoleculeClustering, GraphPartitionToTargetReturnsRequestedCountForLouvainAn
24882629
EXPECT_EQ(lei_summary.final_clusters, 3);
24892630
}
24902631

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+
24912685
TEST(MoleculeClustering, DispatcherReturnsEmptyForNoneMethod) {
24922686
Eigen::MatrixXd pos(2, 2);
24932687
pos <<

0 commit comments

Comments
 (0)