Skip to content

Commit d1161ac

Browse files
committed
Add: OpenMP/Eigen acceleration to TENCM backbone flexibility engine
Accelerate tencm.cpp with multi-backend dispatch: - OpenMP: parallel contact search with thread-private lists (O(N²) loop), parallel Jacobian precomputation over bonds, parallel Cα coordinate update in sample(), parallel B-factor computation (embarrassingly parallel) - Eigen: SelfAdjointEigenSolver replaces manual Jacobi iteration for Hessian diagonalization (more robust, LAPACK/MKL backend when available), GEMV for strain energy ½δθᵀHδθ via Eigen Map All paths guarded by compile-time #ifdef — scalar fallback preserved. https://claude.ai/code/session_011Dzj3wXLYSyDbrswNHSaQ7
1 parent 98362e8 commit d1161ac

File tree

1 file changed

+96
-6
lines changed

1 file changed

+96
-6
lines changed

LIB/tencm.cpp

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
// δθ = Σ_{m≥6} σ_m * z_m * v_m, z_m ~ N(0,1)
2121
//
2222
// Perturbed Cα: r_i' = r_i + Σ_k J_k(i) δθ_k
23+
//
24+
// Hardware acceleration:
25+
// - OpenMP: parallel contact search, B-factor computation, Cα update
26+
// - Eigen: Hessian eigendecomposition, strain energy GEMV
27+
2328
#include "tencm.h"
2429
#include "simd_distance.h"
2530

@@ -33,6 +38,14 @@
3338
#include <numbers>
3439
#include <random>
3540

41+
#ifdef _OPENMP
42+
# include <omp.h>
43+
#endif
44+
45+
#ifdef FLEXAIDS_HAS_EIGEN
46+
# include <Eigen/Dense>
47+
#endif
48+
3649
namespace tencm {
3750

3851
// ─── local utility helpers ───────────────────────────────────────────────────
@@ -106,6 +119,36 @@ void TorsionalENM::build_contacts()
106119
const int N = static_cast<int>(ca_.size());
107120
const float rc2 = cutoff_ * cutoff_;
108121

122+
#ifdef _OPENMP
123+
// OpenMP: each thread builds a private contact list, then merge
124+
int n_threads = omp_get_max_threads();
125+
std::vector<std::vector<Contact>> thread_contacts(n_threads);
126+
127+
#pragma omp parallel
128+
{
129+
int tid = omp_get_thread_num();
130+
auto& local = thread_contacts[tid];
131+
132+
#pragma omp for schedule(dynamic, 4)
133+
for (int i = 0; i < N - 1; ++i) {
134+
for (int j = i + 2; j < N; ++j) {
135+
float dx = ca_[i][0] - ca_[j][0];
136+
float dy = ca_[i][1] - ca_[j][1];
137+
float dz = ca_[i][2] - ca_[j][2];
138+
float r2 = dx*dx + dy*dy + dz*dz;
139+
if (r2 <= rc2) {
140+
float r0 = std::sqrt(r2);
141+
float ratio = cutoff_ / r0;
142+
float k = k0_ * (ratio*ratio*ratio*ratio*ratio*ratio);
143+
local.push_back({i, j, k, r0});
144+
}
145+
}
146+
}
147+
}
148+
// Merge thread-private contact lists
149+
for (auto& tc : thread_contacts)
150+
contacts_.insert(contacts_.end(), tc.begin(), tc.end());
151+
#else
109152
for (int i = 0; i < N - 1; ++i) {
110153
for (int j = i + 2; j < N; ++j) { // skip direct bonded neighbour (i+1)
111154
float dx = ca_[i][0] - ca_[j][0];
@@ -121,6 +164,7 @@ void TorsionalENM::build_contacts()
121164
}
122165
}
123166
}
167+
#endif
124168
}
125169

126170
// ─── build_bonds ─────────────────────────────────────────────────────────────
@@ -171,14 +215,19 @@ void TorsionalENM::assemble_hessian()
171215
const int M = static_cast<int>(bonds_.size());
172216
H_.assign(static_cast<std::size_t>(M * M), 0.0);
173217

174-
// Pre-compute Jacobians for all (bond, atom) pairs to avoid re-computation
175-
// J[k * N_ca + i] = Jacobian of atom i w.r.t. bond k
218+
// Pre-compute Jacobians for all (bond, atom) pairs
176219
const int N = static_cast<int>(ca_.size());
177220
std::vector<std::array<float,3>> J(static_cast<std::size_t>(M * N));
221+
222+
#ifdef _OPENMP
223+
// Jacobian precomputation is embarrassingly parallel over bonds
224+
#pragma omp parallel for schedule(static)
225+
#endif
178226
for (int k = 0; k < M; ++k)
179227
for (int i = 0; i < N; ++i)
180228
J[static_cast<std::size_t>(k * N + i)] = jac(k, i);
181229

230+
// Hessian assembly: accumulate contact contributions
182231
for (const auto& c : contacts_) {
183232
const int ci = c.i;
184233
const int cj = c.j;
@@ -254,16 +303,35 @@ void TorsionalENM::diagonalize()
254303
const int M = static_cast<int>(bonds_.size());
255304
if (M == 0) return;
256305

257-
// Work copy of Hessian
306+
#ifdef FLEXAIDS_HAS_EIGEN
307+
// Eigen: use SelfAdjointEigenSolver for robust, optimized eigendecomposition
308+
{
309+
Eigen::Map<const Eigen::MatrixXd> Hmap(H_.data(), M, M);
310+
Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> solver(Hmap);
311+
312+
const auto& eigenvalues = solver.eigenvalues(); // ascending order
313+
const auto& eigenvectors = solver.eigenvectors();
314+
315+
modes_.clear();
316+
modes_.reserve(static_cast<std::size_t>(M));
317+
for (int i = 0; i < M; ++i) {
318+
NormalMode nm;
319+
nm.eigenvalue = eigenvalues(i);
320+
nm.eigenvector.resize(static_cast<std::size_t>(M));
321+
for (int j = 0; j < M; ++j)
322+
nm.eigenvector[static_cast<std::size_t>(j)] = eigenvectors(j, i);
323+
modes_.push_back(std::move(nm));
324+
}
325+
}
326+
#else
327+
// Manual Jacobi iteration fallback
258328
std::vector<double> A = H_;
259-
// Eigenvector matrix initialised to identity
260329
std::vector<double> V(static_cast<std::size_t>(M * M), 0.0);
261330
for (int i = 0; i < M; ++i)
262331
V[static_cast<std::size_t>(i*M+i)] = 1.0;
263332

264333
const int MAX_SWEEPS = 50;
265334
for (int sweep = 0; sweep < MAX_SWEEPS; ++sweep) {
266-
// Find off-diagonal element with largest absolute value
267335
double max_off = 0.0;
268336
int pp = 0, qq = 1;
269337
for (int p = 0; p < M - 1; ++p)
@@ -275,7 +343,6 @@ void TorsionalENM::diagonalize()
275343
jacobi_rotate(A, V, M, pp, qq);
276344
}
277345

278-
// Collect modes
279346
modes_.clear();
280347
modes_.reserve(static_cast<std::size_t>(M));
281348
for (int i = 0; i < M; ++i) {
@@ -291,6 +358,7 @@ void TorsionalENM::diagonalize()
291358
std::sort(modes_.begin(), modes_.end(),
292359
[](const NormalMode& a, const NormalMode& b){
293360
return a.eigenvalue < b.eigenvalue; });
361+
#endif
294362
}
295363

296364
// ─── sample ──────────────────────────────────────────────────────────────────
@@ -324,6 +392,11 @@ Conformer TorsionalENM::sample(float temperature, std::mt19937& rng) const
324392

325393
// Build perturbed Cα coordinates: r_i' = r_i + Σ_k J_k(i) δθ_k
326394
conf.ca.resize(static_cast<std::size_t>(N));
395+
396+
#ifdef _OPENMP
397+
// Each atom's displacement is independent — parallelize over atoms
398+
#pragma omp parallel for schedule(static)
399+
#endif
327400
for (int i = 0; i < N; ++i) {
328401
float disp[3] = {0.0f, 0.0f, 0.0f};
329402
for (int k = 0; k < M; ++k) {
@@ -342,6 +415,18 @@ Conformer TorsionalENM::sample(float temperature, std::mt19937& rng) const
342415
}
343416

344417
// Elastic strain energy: ½ δθᵀ H δθ
418+
#ifdef FLEXAIDS_HAS_EIGEN
419+
{
420+
Eigen::Map<const Eigen::VectorXd> dth(
421+
reinterpret_cast<const double*>(nullptr), 0); // placeholder
422+
// Convert float delta_theta to double for Eigen GEMV
423+
Eigen::VectorXd dth_d(M);
424+
for (int k = 0; k < M; ++k)
425+
dth_d(k) = static_cast<double>(conf.delta_theta[static_cast<std::size_t>(k)]);
426+
Eigen::Map<const Eigen::MatrixXd> Hmap(H_.data(), M, M);
427+
conf.strain_energy = static_cast<float>(0.5 * dth_d.dot(Hmap * dth_d));
428+
}
429+
#else
345430
conf.strain_energy = 0.0f;
346431
for (int k = 0; k < M; ++k) {
347432
double row = 0.0;
@@ -351,6 +436,7 @@ Conformer TorsionalENM::sample(float temperature, std::mt19937& rng) const
351436
conf.strain_energy += static_cast<float>(
352437
0.5 * static_cast<double>(conf.delta_theta[static_cast<std::size_t>(k)]) * row);
353438
}
439+
#endif
354440

355441
return conf;
356442
}
@@ -396,6 +482,10 @@ std::vector<float> TorsionalENM::bfactors(float temperature) const
396482

397483
std::vector<float> bf(static_cast<std::size_t>(N), 0.0f);
398484

485+
#ifdef _OPENMP
486+
// B-factor for each atom is completely independent — parallelize
487+
#pragma omp parallel for schedule(static)
488+
#endif
399489
for (int i = 0; i < N; ++i) {
400490
double msf = 0.0; // mean-square fluctuation
401491
for (int m = SKIP; m < M && m < SKIP + N_MODES; ++m) {

0 commit comments

Comments
 (0)