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
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+
3649namespace 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