OpenMD 3.2
Molecular Dynamics in the Open
Loading...
Searching...
No Matches
SFG.cpp
1/*
2 * Copyright (c) 2004-present, The University of Notre Dame. All rights
3 * reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * 3. Neither the name of the copyright holder nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 * POSSIBILITY OF SUCH DAMAGE.
30 */
31
32#include "applications/dynamicProps/SFG.hpp"
33
34#include <algorithm>
35#include <cmath>
36#include <complex>
37#include <fstream>
38
41#include "io/DumpReader.hpp"
42#include "math/Eigenvalue.hpp"
44#include "utils/Revision.hpp"
45#include "utils/simError.h"
46
47#if defined(HAVE_FFTW3_H)
48#include <fftw3.h>
49#endif
50
51// LAPACK dsyevd (divide-and-conquer symmetric eigensolver)
52#if defined(HAVE_LAPACK)
53extern "C" {
54 void dsyevd_(const char* jobz, const char* uplo, const int* n,
55 double* a, const int* lda, double* w,
56 double* work, const int* lwork,
57 int* iwork, const int* liwork, int* info);
58}
59#endif
60
61// CBLAS for optimised dgemm (optional, used alongside LAPACK)
62#if defined(HAVE_CBLAS)
63#include <cblas.h>
64#endif
65
66#if defined(_OPENMP)
67#include <omp.h>
68#endif
69
70namespace OpenMD {
71
72 // ℏ in cm⁻¹·ps (= ℏ / (hc × 100) × 10¹²)
73 // Value from MultiSpec constants.hpp; derivation:
74 // ℏ = 1.054571817e-34 J·s
75 // 1 cm⁻¹ = 1.9864458e-23 J
76 // 1 ps = 1e-12 s
77 // → ℏ/(1 cm⁻¹ × 1 ps) = 1.054571817e-34 / (1.9864458e-23 × 1e-12) = 5.3088...
78 static constexpr double HBAR_CM_PS = 5.3088373;
79
80 // =========================================================================
81 // Constructor
82 // =========================================================================
83 SFG::SFG(SimInfo* info, const std::string& filename,
84 const std::string& sele1, const std::string& sele2,
85 const std::string& polarization, int privilegedAxis,
86 RealType t_apod, RealType t_zerofill,
87 RealType fc) :
88 SystemACF<RealType>(info, filename, sele1, sele2),
89 polarization_(polarization),
90 privilegedAxis_(privilegedAxis),
91 s1_((privilegedAxis + 1) % 3),
92 s2_((privilegedAxis + 2) % 3),
93 t_apod_ps_(t_apod * 1e-3),
94 t_zerofill_ps_(t_zerofill * 1e-3),
95 fc_(fc) {
96
97 setCorrFuncType("SFG");
98 setOutputName(getPrefix(dumpFilename_) + ".sfg");
99 setLabelString("Im chi2(omega)");
100
101 // -----------------------------------------------------------------------
102 // Spectroscopic maps.
103 //
104 // w10_ : ω₁₀(E) = a0 + a1·E + a2·E² [cm⁻¹; E in a.u.]
105 // muPrime_ : μ′(E) = m0 + m1·E + m2·E² [a.u.]
106 // Dipole derivative along OH bond direction.
107 // x10_ : x₁₀(ω) = x0 + x1·ω₁₀ [a.u.]
108 // Coordinate matrix element. |μ₁₀| = μ′ × x₁₀ [a.u.]
109 // Converted to Debye·amu⁻⁰·⁵ by × 4.8032 × 7.2906.
110 // p10_ : p₁₀(ω) = p0 + p1·ω₁₀ [a.u.]
111 // Momentum matrix element.
112 // wintra_ : ω^intra_jk = [k0 + k1·(Eⱼ+Eₖ)]·xⱼ·xₖ
113 // + kinetic_coeff · pⱼ·pₖ [cm⁻¹]
114 // alphaMap_: (α_par, α_perp) [Ų amu⁻⁰·⁵]
115 //
116 // Sources: Auer & Skinner, J. Chem. Phys. 128, 224511 (2008) — SPC/E
117 // Gruenbaum et al., JCTC 9, 3109 (2013) — TIP4P
118 // Takayama et al., J. Chem. Phys. 158, 136101 (2023) — TIP4P-Ice
119 // -----------------------------------------------------------------------
120
121 // SPC/E (Auer & Skinner 2008, Table I)
122 // μ′/μ′_g = 0.7112 + 75.59·E → μ′(E) = 0.1874*(0.7112 + 75.59·E)
123 // = 0.1333 + 14.17·E + 0·E²
124 w10_["H_SPCE"] = std::make_tuple(3761.6, -5060.4, -86225.0);
125 muPrime_["H_SPCE"] = std::make_tuple(0.1333, 14.17, 0.0);
126 x10_["H_SPCE"] = std::make_pair(0.1934, -1.75e-5);
127 p10_["H_SPCE"] = std::make_pair(1.611, 5.893e-4);
128 wintra_["H_SPCE"] = std::make_tuple(-1789.0, 23852.0, -1.966);
129 alphaMap_["H_SPCE"] = std::make_pair(0.185, 0.033);
130 tdcLoc_["H_SPCE"] = 0.58; // Å from O along OH bond
131
132 // TIP4P (Gruenbaum et al. 2013, Table 1)
133 w10_["H_TIP4P"] = std::make_tuple(3760.2, -3541.7, -152677.0);
134 muPrime_["H_TIP4P"] = std::make_tuple(0.1646, 11.39, 63.41);
135 x10_["H_TIP4P"] = std::make_pair(0.19285, -1.7261e-5);
136 p10_["H_TIP4P"] = std::make_pair(1.6466, 5.7692e-4);
137 wintra_["H_TIP4P"] = std::make_tuple(-1361.0, 27165.0, -1.887);
138 alphaMap_["H_TIP4P"] = std::make_pair(0.185, 0.033);
139 tdcLoc_["H_TIP4P"] = 0.67; // Å from O along OH bond
140
141 // TIP4P-Ice (transferability from TIP4P)
142 w10_["H_TIP4P-Ice"] = w10_["H_TIP4P"];
143 muPrime_["H_TIP4P-Ice"] = muPrime_["H_TIP4P"];
144 x10_["H_TIP4P-Ice"] = x10_["H_TIP4P"];
145 p10_["H_TIP4P-Ice"] = p10_["H_TIP4P"];
146 wintra_["H_TIP4P-Ice"] = wintra_["H_TIP4P"];
147 alphaMap_["H_TIP4P-Ice"] = alphaMap_["H_TIP4P"];
148 tdcLoc_["H_TIP4P-Ice"] = tdcLoc_["H_TIP4P"];
149
150 // -----------------------------------------------------------------------
151 // HOH bend overtone maps (Ni & Skinner JCP 143, 014502 (2015)).
152 //
153 // ω_HOH_10(E_b) = b0_10 + b1_10 · E_b [cm⁻¹]
154 // ω_HOH_21(E_b) = b0_21 + b1_21 · E_b [cm⁻¹]
155 // ω_2δ on Hamiltonian diagonal = ω_HOH_10 + ω_HOH_21
156 //
157 // Field-free limit: ω_2δ ≈ 1581 + 1551 = 3133 cm⁻¹, right in the
158 // H-bonded OH stretch region as expected for Fermi resonance.
159 //
160 // The map was originally fit for TIP4P; we apply it to all model
161 // variants for which we have a stretch map.
162 // -----------------------------------------------------------------------
163 wb01_["TIP4P"] = std::make_pair(1581.46, 2938.51);
164 wb12_["TIP4P"] = std::make_pair(1551.32, 3147.80);
165 wb01_["TIP4P-Ice"] = wb01_["TIP4P"];
166 wb12_["TIP4P-Ice"] = wb12_["TIP4P"];
167 // Apply same map to SPC/E as well — Auer/Skinner 2008 didn't publish
168 // a separate bend map for SPC/E, so we use the Ni 2015 TIP4P map.
169 // The bend frequency depends weakly on the water model (compared to
170 // the stretch), so this transfer is a reasonable approximation.
171 wb01_["SPCE"] = wb01_["TIP4P"];
172 wb12_["SPCE"] = wb12_["TIP4P"];
173
174 // -----------------------------------------------------------------------
175 // Applied external electric field.
176 // EF_ stored in kcal mol⁻¹ Å⁻¹ e⁻¹ (same units as getElectricField()).
177 // SimParams gives fields in V/Å; chrgToKcal converts to kcal/mol/Å/e.
178 // -----------------------------------------------------------------------
179 EF_ = V3Zero;
180 const RealType chrgToKcal = 23.060548;
181 std::vector<RealType> ef;
182 bool efSpec = false;
183 if (info_->getSimParams()->haveElectricField()) {
184 efSpec = true; ef = info_->getSimParams()->getElectricField();
185 }
186 if (info_->getSimParams()->haveUniformField()) {
187 efSpec = true; ef = info_->getSimParams()->getUniformField();
188 }
189 if (efSpec) {
190 if (ef.size() != 3) {
191 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
192 "SFG: uniformField must have 3 components (%zu given).\n",
193 ef.size());
194 painCave.isFatal = 1; simError();
195 }
196 EF_.x() = ef[0] * chrgToKcal;
197 EF_.y() = ef[1] * chrgToKcal;
198 EF_.z() = ef[2] * chrgToKcal;
199 }
200
201 // -----------------------------------------------------------------------
202 // Electric field storage setup (mirroring OHFrequencyMap).
203 // -----------------------------------------------------------------------
204 dumpHasElectricFields_ = info_->getSimParams()->getOutputElectricField();
205
206 if (!dumpHasElectricFields_) {
207 int asl = info_->getAtomStorageLayout() | DataStorage::dslElectricField;
208 int rbsl = info_->getRigidBodyStorageLayout() | DataStorage::dslElectricField;
209 int cgsl = info_->getCutoffGroupStorageLayout();
210 info_->setAtomStorageLayout(asl);
211 info_->setRigidBodyStorageLayout(rbsl);
212 info_->setCutoffGroupStorageLayout(cgsl);
213 info_->setSnapshotManager(new SimSnapshotManager(info_, asl, rbsl, cgsl));
214 forceMan_ = new ForceManager(info_);
215 forceMan_->setDoElectricField(true);
216 forceMan_->initialize();
217 }
218 info_->getSimParams()->setOutputElectricField(true);
219 }
220
221 SFG::~SFG() { delete forceMan_; }
222
223 // =========================================================================
224 // computeProperty1
225 //
226 // Called by preCorrelate() via computeFrame() for every frame istep.
227 // We build and cache the FrameData for that frame into allFrames_[istep].
228 // preCorrelate() has already loaded the snapshot into currentSnapshot_.
229 // If electric fields are not in the dump, we recalculate them here.
230 // =========================================================================
231 void SFG::computeProperty1(int frame) {
232 // Grow allFrames_ to cover this frame index (preCorrelate calls frames
233 // 0..nFrames_-1 in order, so a simple push_back would also work, but
234 // resize+assign is safer if frames ever come out of order).
235 if (static_cast<int>(allFrames_.size()) <= frame)
236 allFrames_.resize(frame + 1);
237
238 if (!dumpHasElectricFields_) {
239 forceMan_->setDoElectricField(true);
240 forceMan_->calcForces();
241 }
242
243 std::vector<Vector3d> ohPos;
244 std::vector<int> molIndex;
245 std::vector<RealType> intraJ;
246 allFrames_[frame] = extractFrame(ohPos, molIndex, intraJ);
247 buildHamiltonian(allFrames_[frame], ohPos, molIndex, intraJ);
248
249 // Accumulate raw diagonal frequencies for wAvg_ computation.
250 // buildHamiltonian leaves H diagonal as unshifted ω₁₀ (stretch) and
251 // ω_2δ (bend overtone) values at this point. We only average stretch
252 // frequencies — including bend overtones (at ~3130 cm⁻¹) would skew
253 // wAvg_ and shift the spectrum's frequency axis incorrectly.
254 const FrameData& fd = allFrames_[frame];
255 for (int i = 0; i < fd.nStretch; ++i) {
256 wAvgSum_ += fd.H(i, i);
257 wAvgCount_ += 1;
258 }
259 }
260
261 // =========================================================================
262 // correlation
263 //
264 // Overrides TimeCorrFunc::correlation(). Called by the base doCorrelate()
265 // after preCorrelate() has finished, so by the time we get here:
266 // - allFrames_[0..nFrames_-1] are fully populated
267 // - nTimeBins_ = correlation window length in frames
268 // - nStart_ = frames to skip at start
269 // - nSep_ = gap frames between windows
270 // - nStride_ = nTimeBins_ + nSep_
271 // - navg_ = number of windows that fit
272 // - dtMean_ = mean frame interval [fs]
273 //
274 // Implements the MultiSpec adiabatic windowed propagation:
275 // for each window iwin (origin frame = nStart_ + iwin * nStride_):
276 // F = I (identity, N×N complex)
277 // for tt = 0 .. nTimeBins_-1:
278 // if tt > 0: F ← exp(i H(t-1) dt/ℏ) · F
279 // C(tt) += αᵀ(t) · F · μ(0)
280 // =========================================================================
281 void SFG::correlation() {
282 const std::complex<double> czero(0.0, 0.0);
283
284 int ncor = static_cast<int>(nTimeBins_);
285 RealType dt_ps = dtMean_ * 1.0e-3; // fs → ps
286 dt_ps_ = dt_ps;
287
288 // Finalize wAvg_ from all per-frame diagonal entries accumulated during
289 // preCorrelate(). This is the true mean ω₁₀ across the trajectory and
290 // the selected chromophores, regardless of water model.
291 wAvg_ = (wAvgCount_ > 0)
292 ? wAvgSum_ / static_cast<RealType>(wAvgCount_)
293 : 0.0;
294
295 // Apply the shift to every stored Hamiltonian diagonal in one pass.
296 // The off-diagonals (TDC couplings) are unaffected.
297 for (auto& fd : allFrames_) {
298 for (int i = 0; i < fd.N; ++i)
299 fd.H(i, i) -= wAvg_;
300 }
301
302 tcf_ssp_.assign(ncor, czero);
303 tcf_ppp_.assign(ncor, czero);
304 tcf_sps_.assign(ncor, czero);
305
306 progressBar_->clear();
307
308#if defined(_OPENMP)
309 // -----------------------------------------------------------------
310 // OpenMP parallel window loop.
311 //
312 // Each thread accumulates into its own local TCF vectors, which are
313 // merged into the shared tcf_ssp_/ppp_/sps_ at the end. All per-
314 // window state (F, refFreqs, fd0, fdt) is already thread-local.
315 // The propagateF scratch (Hbuf, Vbuf, etc.) is thread_local static.
316 // allFrames_ is read-only and shared.
317 // -----------------------------------------------------------------
318 int completedWindows = 0;
319
320#pragma omp parallel
321 {
322 // Per-thread TCF accumulators
323 std::vector<std::complex<double>> loc_ssp(ncor, czero);
324 std::vector<std::complex<double>> loc_ppp(ncor, czero);
325 std::vector<std::complex<double>> loc_sps(ncor, czero);
326
327#pragma omp for schedule(dynamic)
328 for (int iwin = 0; iwin < navg_; ++iwin) {
329 int istart = nStart_ + iwin * nStride_;
330 if (istart >= nFrames_) continue;
331
332 const FrameData& fd0raw = allFrames_[istart];
333 int N = fd0raw.N;
334 if (N == 0) continue;
335
336 const std::vector<int>& refIDs = fd0raw.globalIDs;
337
338 std::vector<RealType> refFreqs(N);
339 for (int i = 0; i < N; ++i)
340 refFreqs[i] = fd0raw.H(i, i);
341
342 // F = identity
343 std::vector<std::complex<double>> F(N * N, czero);
344 for (int i = 0; i < N; ++i) F[i*N+i] = std::complex<double>(1.0, 0.0);
345
346 FrameData fd0;
347 remapFrame(fd0raw, refIDs, refFreqs, fd0,
348 fd0raw.nStretch, fd0raw.nBend);
349
350 for (int tt = 0; tt < ncor; ++tt) {
351 int iframe = istart + tt;
352 if (iframe >= nFrames_) break;
353
354 FrameData fdt;
355 if (!remapFrame(allFrames_[iframe], refIDs, refFreqs, fdt,
356 fd0raw.nStretch, fd0raw.nBend))
357 break;
358
359 for (int i = 0; i < N; ++i) {
360 auto it = allFrames_[iframe].idToIndex.find(refIDs[i]);
361 if (it != allFrames_[iframe].idToIndex.end())
362 refFreqs[i] = allFrames_[iframe].H(it->second, it->second);
363 }
364
365 if (tt > 0)
366 propagateF(F, fdt.H, dt_ps, N);
367
368 RealType exptc = (T1_ps_ > 0.0)
369 ? std::exp(-static_cast<RealType>(tt) * dt_ps / (2.0 * T1_ps_))
370 : 1.0;
371
372 accumulateTCF(tt, F, fd0, fdt, N, exptc,
373 loc_ssp, loc_ppp, loc_sps);
374 }
375
376 // Progress update (atomic to avoid garbled output)
377#pragma omp atomic
378 completedWindows++;
379
380 if (omp_get_thread_num() == 0) {
381 progressBar_->setStatus(completedWindows, navg_);
382 progressBar_->update();
383 }
384 }
385
386 // Merge per-thread accumulators into shared TCF vectors
387#pragma omp critical
388 {
389 for (int tt = 0; tt < ncor; ++tt) {
390 tcf_ssp_[tt] += loc_ssp[tt];
391 tcf_ppp_[tt] += loc_ppp[tt];
392 tcf_sps_[tt] += loc_sps[tt];
393 }
394 }
395 } // end omp parallel
396
397#else
398 // -----------------------------------------------------------------
399 // Serial fallback (no OpenMP).
400 // -----------------------------------------------------------------
401 for (int iwin = 0; iwin < navg_; ++iwin) {
402 int istart = nStart_ + iwin * nStride_;
403 if (istart >= nFrames_) break;
404
405 progressBar_->setStatus(iwin + 1, navg_);
406 progressBar_->update();
407
408 const FrameData& fd0raw = allFrames_[istart];
409 int N = fd0raw.N;
410 if (N == 0) continue;
411
412 const std::vector<int>& refIDs = fd0raw.globalIDs;
413
414 std::vector<RealType> refFreqs(N);
415 for (int i = 0; i < N; ++i)
416 refFreqs[i] = fd0raw.H(i, i);
417
418 // F = identity
419 std::vector<std::complex<double>> F(N * N, czero);
420 for (int i = 0; i < N; ++i) F[i*N+i] = std::complex<double>(1.0, 0.0);
421
422 FrameData fd0;
423 remapFrame(fd0raw, refIDs, refFreqs, fd0,
424 fd0raw.nStretch, fd0raw.nBend);
425
426 for (int tt = 0; tt < ncor; ++tt) {
427 int iframe = istart + tt;
428 if (iframe >= nFrames_) break;
429
430 FrameData fdt;
431 if (!remapFrame(allFrames_[iframe], refIDs, refFreqs, fdt,
432 fd0raw.nStretch, fd0raw.nBend))
433 break;
434
435 for (int i = 0; i < N; ++i) {
436 auto it = allFrames_[iframe].idToIndex.find(refIDs[i]);
437 if (it != allFrames_[iframe].idToIndex.end())
438 refFreqs[i] = allFrames_[iframe].H(it->second, it->second);
439 }
440
441 if (tt > 0)
442 propagateF(F, fdt.H, dt_ps, N);
443
444 RealType exptc = (T1_ps_ > 0.0)
445 ? std::exp(-static_cast<RealType>(tt) * dt_ps / (2.0 * T1_ps_))
446 : 1.0;
447
448 accumulateTCF(tt, F, fd0, fdt, N, exptc,
449 tcf_ssp_, tcf_ppp_, tcf_sps_);
450 }
451 }
452#endif
453
454 // Normalize by number of windows
455 if (navg_ > 0) {
456 RealType inv = 1.0 / static_cast<RealType>(navg_);
457 for (int tt = 0; tt < ncor; ++tt) {
458 tcf_ssp_[tt] *= inv;
459 tcf_ppp_[tt] *= inv;
460 tcf_sps_[tt] *= inv;
461 }
462 }
463 }
464
465
466
467 // =========================================================================
468 // extractFrame
469 //
470 // Reads the current snapshot and builds a FrameData in the local (site)
471 // basis. The Hamiltonian matrix is NOT filled here; call buildHamiltonian
472 // afterward. ohPos and molIndex are returned for Hamiltonian construction.
473 // =========================================================================
474 SFG::FrameData SFG::extractFrame(std::vector<Vector3d>& ohPos,
475 std::vector<int>& molIndex,
476 std::vector<RealType>& intraJ) {
477 const RealType kcalToAU = 0.0008432975573;
478
479 FrameData fd;
480 ohPos.clear();
481 molIndex.clear();
482 intraJ.clear();
483
484 std::vector<RealType> freqs;
485
486 // Per-chromophore electric field, x₁₀, p₁₀ needed for
487 // intramolecular coupling: ω^intra = [k0+k1(Ei+Ej)] xi xj + kp pi pj
488 std::vector<RealType> eFields; // projected E in a.u.
489 std::vector<RealType> x10vals; // coordinate matrix element [a.u.]
490 std::vector<RealType> p10vals; // momentum matrix element [a.u.]
491 std::vector<RealType> mxvals; // μ′·x₁₀ in a.u. (for TDC)
492 std::vector<RealType> intraJ_k0; // per-site k0 from wintra_ map
493 std::vector<RealType> intraJ_kp; // per-site kp from wintra_ map
494
495 // Per-molecule data collected during the OH loop, used afterward to
496 // append bend overtone chromophores. Indexed by molID.
497 struct MolData {
498 Vector3d Opos;
499 Vector3d ohUnit1, ohUnit2; // unit OH vectors (in-plane basis)
500 Vector3d Efield1, Efield2; // field at H1, H2 in a.u.
501 RealType rOH1 = 0.0, rOH2 = 0.0;
502 int ohCount = 0; // 1 or 2 stretches contributed
503 int molOgID = -1; // global O atom ID (used as bend ID)
504 std::string modelName; // water-model type name (e.g. "TIP4P")
505 };
506 std::map<int, MolData> molMap; // molID -> MolData
507
508 int ii;
509 Molecule* mol;
510
511 for (mol = seleMan1_.beginSelectedMolecule(ii); mol != nullptr;
512 mol = seleMan1_.nextSelectedMolecule(ii)) {
513
514 int molID = mol->getGlobalIndex();
515
516 // Locate the oxygen and the (up to two) mapped hydrogens of this water.
517 Vector3d Opos;
518 int OgID = -1;
519 bool foundO = false;
520 std::vector<Atom*> hAtoms;
521 Molecule::AtomIterator ai;
522 for (Atom* atom = mol->beginAtom(ai); atom != nullptr;
523 atom = mol->nextAtom(ai)) {
524 const std::string& aname = atom->getAtomType()->getName();
525 if (aname[0] == 'O' && !foundO) {
526 Opos = atom->getPos();
527 OgID = atom->getGlobalIndex();
528 foundO = true;
529 } else if (w10_.find(aname) != w10_.end()) {
530 hAtoms.push_back(atom);
531 }
532 }
533 if (!foundO) continue;
534 if (hAtoms.empty()) continue;
535
536 // Build a stretch chromophore for EACH OH bond of this molecule.
537 // Iterating over molecules (selected by COM z) and including BOTH OH
538 // bonds removes the orientational bias that arises when selecting
539 // individual hydrogens in a thin z-slab (which preferentially admits
540 // up-pointing OH bonds, biasing <mu_z>).
541 for (Atom* hAtom : hAtoms) {
542 std::string name = hAtom->getAtomType()->getName();
543
544 auto wIt = w10_.find(name);
545 auto mpIt = muPrime_.find(name);
546 if (wIt == w10_.end() || mpIt == muPrime_.end()) continue;
547
548 Vector3d Hpos = hAtom->getPos();
549 Vector3d rOH_mic = Hpos - Opos;
550 currentSnapshot_->wrapVector(rOH_mic); // minimum-image OH vector
551 RealType rOH_len = rOH_mic.length();
552 Vector3d ohUnit = rOH_mic;
553 ohUnit.normalize();
554
555 // Electric field [kcal mol⁻¹ Å⁻¹ e⁻¹] → AU
556 Vector3d sE = hAtom->getElectricField() + EF_;
557 sE *= kcalToAU;
558 RealType E = dot(ohUnit, sE);
559
560 // Local frequency ω₁₀ [cm⁻¹]
561 auto [a0, a1, a2] = wIt->second;
562 RealType freq = a0 + a1*E + a2*E*E;
563
564 auto [m0, m1, m2] = mpIt->second;
565 RealType muPrime = m0 + m1*E + m2*E*E; // [a.u.]
566
567 auto [x0, x1] = x10_.at(name);
568 RealType x10 = x0 + x1*freq; // [a.u.]
569
570 // Momentum matrix element for intramolecular coupling
571 auto [pv0, pv1] = p10_.at(name);
572 RealType p10 = pv0 + pv1*freq; // [a.u.]
573
574 // Transition dipole magnitude [Debye]
575 const RealType eaBohr_to_Debye = 2.5417464;
576 RealType muMag = muPrime * x10 * eaBohr_to_Debye;
577
578 // TDC dipole location: a fitted distance along the OH bond from O.
579 // Auer & Skinner 2008: 0.58 Å (SPC/E); Gruenbaum 2013: 0.67 Å (TIP4P).
580 RealType tdcDist = 0.5 * rOH_len; // fallback: midpoint
581 auto tdcIt = tdcLoc_.find(name);
582 if (tdcIt != tdcLoc_.end())
583 tdcDist = tdcIt->second;
584 Vector3d dipolePos = Opos + tdcDist * ohUnit;
585
586 // Bond polarizability tensor.
587 auto [apar, aperp] = alphaMap_.at(name);
588 RealType x10_gas = x10_.at(name).first;
589
590 fd.N++;
591 fd.globalIDs.push_back(hAtom->getGlobalIndex());
592 fd.mu.push_back(muMag * ohUnit);
593 fd.alpha.push_back(bondPolarizability(ohUnit, apar, aperp,
594 x10, x10_gas));
595
596 // Auxiliary data for Hamiltonian construction
597 ohPos.push_back(dipolePos);
598 molIndex.push_back(molID);
599 freqs.push_back(freq);
600 eFields.push_back(E);
601 x10vals.push_back(x10);
602 p10vals.push_back(p10);
603 mxvals.push_back(muPrime * x10); // a.u., for TDC coupling
604
605 auto [k0, k1, kp] = wintra_.at(name);
606 intraJ.push_back(k0 + k1*E);
607 intraJ_k0.push_back(k0);
608 intraJ_kp.push_back(kp);
609
610 // -----------------------------------------------------------------
611 // Stash this stretch's per-molecule info for the bend pass
612 // -----------------------------------------------------------------
613 MolData& md = molMap[molID];
614 md.Opos = Opos;
615 md.molOgID = OgID;
616 md.modelName = mol->getType();
617 if (md.ohCount == 0) {
618 md.ohUnit1 = ohUnit;
619 md.Efield1 = sE;
620 md.rOH1 = rOH_len;
621 } else if (md.ohCount == 1) {
622 md.ohUnit2 = ohUnit;
623 md.Efield2 = sE;
624 md.rOH2 = rOH_len;
625 }
626 md.ohCount++;
627 }
628 }
629
630 int nStretch = fd.N;
631 fd.nStretch = nStretch;
632
633 // -----------------------------------------------------------------------
634 // Bend overtone chromophores.
635 //
636 // Add one bend chromophore per molecule that contributed BOTH OH
637 // stretches (we need both bond vectors to define the bend coordinate).
638 // The bend electric field is projected on the in-plane perpendicular
639 // directions to each OH bond:
640 //
641 // E_b = ê_⊥1 · E(H1) / r_OH1 + ê_⊥2 · E(H2) / r_OH2
642 //
643 // where ê_⊥1 lies in the molecular plane perpendicular to OH1, in the
644 // direction that opens the HOH angle (and similarly for ê_⊥2). This
645 // matches MultiSpec's calcEf() construction:
646 // v_p = (e1 × e2) / |e1 × e2| (out-of-plane unit vector)
647 // ê_⊥1 = v_p × e1 (in-plane, perpendicular to e1)
648 // ê_⊥2 = e2 × v_p (in-plane, perpendicular to e2)
649 //
650 // Bend overtones have zero transition dipole and polarizability (their
651 // intensity is borrowed from the stretches via Fermi mixing in the
652 // exciton eigenvectors).
653 // -----------------------------------------------------------------------
654 int nBend = 0;
655 if (fc_ != 0.0) {
656 for (auto& [molID, md] : molMap) {
657 if (md.ohCount != 2) continue; // need both OH for bend coordinate
658 auto bIt = wb01_.find(md.modelName);
659 if (bIt == wb01_.end()) continue;
660 auto cIt = wb12_.find(md.modelName);
661 if (cIt == wb12_.end()) continue;
662
663 // In-plane perpendiculars to each OH (opens HOH angle direction)
664 Vector3d vp = cross(md.ohUnit1, md.ohUnit2);
665 RealType vpLen = vp.length();
666 if (vpLen < 1.0e-9) continue; // degenerate (parallel OH bonds)
667 vp /= vpLen;
668
669 Vector3d ePerp1 = cross(vp, md.ohUnit1); // ⊥ OH1, in-plane
670 Vector3d ePerp2 = cross(md.ohUnit2, vp); // ⊥ OH2, in-plane
671
672 // Bend field projection in a.u.
673 RealType Eb = dot(ePerp1, md.Efield1) / md.rOH1
674 + dot(ePerp2, md.Efield2) / md.rOH2;
675
676 // Diagonal bend overtone frequency: ω_2δ = ω_10 + ω_21
677 auto [b0_10, b1_10] = bIt->second;
678 auto [b0_21, b1_21] = cIt->second;
679 RealType w_2d = (b0_10 + b1_10*Eb) + (b0_21 + b1_21*Eb);
680
681 // Append bend chromophore. Use the molecule's O global ID as a
682 // unique identifier (distinct from any H atom's ID).
683 fd.N++;
684 fd.globalIDs.push_back(md.molOgID);
685 fd.mu.push_back(V3Zero); // zero transition dipole
686 fd.alpha.push_back(Mat3x3d(0.0)); // zero polarizability
687
688 // For the buildHamiltonian loop, intraJ packing must continue
689 // for the bend block too (6 entries per chromophore). Bends
690 // have no intramolecular OH-OH coupling and no inter TDC, so
691 // we fill placeholders. buildHamiltonian recognizes bend
692 // chromophores by index >= fd.nStretch and applies Fermi
693 // coupling instead of the stretch formulas.
694 ohPos.push_back(md.Opos); // dummy position; bend has no TDC
695 molIndex.push_back(molID);
696 freqs.push_back(w_2d);
697 eFields.push_back(Eb);
698 x10vals.push_back(0.0);
699 p10vals.push_back(0.0);
700 mxvals.push_back(0.0); // mx=0 -> bend cannot TDC-couple
701 intraJ.push_back(0.0);
702 intraJ_k0.push_back(0.0);
703 intraJ_kp.push_back(0.0);
704
705 nBend++;
706 }
707 }
708
709 fd.nBend = nBend;
710 useFermi_ = (nBend > 0);
711
712 // Build H once with the final size; set diagonal to ω₁₀ frequencies
713 // for stretches and ω_2δ for bend overtones.
714 int N = fd.N;
715 fd.H = DynamicRectMatrix<RealType>(N, N, 0.0);
716 for (int i = 0; i < N; ++i) {
717 fd.H(i, i) = freqs[i];
718 fd.idToIndex[fd.globalIDs[i]] = i;
719 }
720
721 // Stash all per-site values for buildHamiltonian via packed intraJ.
722 // Each chromophore contributes 6 consecutive entries:
723 // [0] Ki = k0 + k1·E (intra potential prefactor)
724 // [1] x₁₀ (a.u.)
725 // [2] p₁₀ (a.u.)
726 // [3] k0 (per atom type, for intra)
727 // [4] kp (per atom type, for intra)
728 // [5] mx = μ′·x₁₀ (a.u., for inter TDC)
729 // For bend chromophores all of these are 0.0, which makes the
730 // intra-stretch coupling formula evaluate to 0 and the inter TDC
731 // formula also evaluate to 0 (mx = 0). Fermi coupling between
732 // each bend and its two parent stretches is added separately in
733 // buildHamiltonian using the molIndex array.
734 {
735 std::vector<RealType> packed;
736 packed.reserve(6 * N);
737 for (int i = 0; i < N; ++i) {
738 packed.push_back(intraJ[i]);
739 packed.push_back(x10vals[i]);
740 packed.push_back(p10vals[i]);
741 packed.push_back(intraJ_k0[i]);
742 packed.push_back(intraJ_kp[i]);
743 packed.push_back(mxvals[i]);
744 }
745 intraJ = std::move(packed);
746 }
747
748 return fd;
749 }
750
751 // =========================================================================
752 // buildHamiltonian
753 //
754 // Fills fd.H off-diagonal elements only.
755 // Diagonal (ω₁₀) was already set by extractFrame.
756 // same molecule → J from Gruenbaum eq 3.17:
757 // ω^intra_jk = [k0 + k1·(Eⱼ+Eₖ)]·xⱼ·xₖ + kp·pⱼ·pₖ
758 // where k0, kp, and the per-chromophore potential prefactors,
759 // x₁₀, p₁₀ are packed in intraJ (per atom type from wintra_).
760 // different mols → J = TDC coupling computed in MultiSpec convention:
761 // J = AU_TO_WN · geom · (μ′·x₁₀)ᵢ · (μ′·x₁₀)ⱼ / r_bohr³
762 //
763 // intraJ packing (from extractFrame): 6 entries per chromophore:
764 // [6*i+0] = Ki = k0 + k1·Eᵢ (potential coupling prefactor)
765 // [6*i+1] = x₁₀,ᵢ (coordinate matrix element, a.u.)
766 // [6*i+2] = p₁₀,ᵢ (momentum matrix element, a.u.)
767 // [6*i+3] = k0 (from wintra_ map, per atom type)
768 // [6*i+4] = kp (from wintra_ map, per atom type)
769 // [6*i+5] = μ′·x₁₀ (a.u., for inter TDC)
770 // =========================================================================
771 void SFG::buildHamiltonian(FrameData& fd,
772 const std::vector<Vector3d>& ohPos,
773 const std::vector<int>& molIndex,
774 const std::vector<RealType>& intraJ) {
775 int N = fd.N;
776 if (N == 0) return;
777
778 // H diagonal already holds ω₁₀ (stretch) and ω_2δ (bend) values
779 // (set during extractFrame). Fill off-diagonal elements only.
780 //
781 // Coupling logic:
782 // same molecule + both stretches -> Gruenbaum intramolecular formula
783 // same molecule + stretch ⟷ bend -> Fermi coupling (constant fc_)
784 // different molecules + stretches -> TDC coupling
785 // any pair involving bends across molecules -> zero (mx=0 makes
786 // TDC vanish)
787 const int nStr = fd.nStretch;
788
789 for (int i = 0; i < N; ++i) {
790 bool i_is_bend = (i >= nStr);
791 for (int j = i+1; j < N; ++j) {
792 bool j_is_bend = (j >= nStr);
793 RealType J = 0.0;
794 if (molIndex[i] == molIndex[j]) {
795 if (!i_is_bend && !j_is_bend) {
796 // Stretch-stretch on same molecule: Gruenbaum eq 3.17
797 // ω^intra_jk = [k0 + k1·(Eⱼ+Eₖ)]·xⱼ·xₖ + kp·pⱼ·pₖ
798 RealType Ki = intraJ[6*i + 0];
799 RealType xi = intraJ[6*i + 1];
800 RealType pi = intraJ[6*i + 2];
801 RealType k0_val = intraJ[6*i + 3];
802 RealType kp_val = intraJ[6*i + 4];
803 RealType Kj = intraJ[6*j + 0];
804 RealType xj = intraJ[6*j + 1];
805 RealType pj = intraJ[6*j + 2];
806
807 RealType potentialTerm = (Ki + Kj - k0_val) * xi * xj;
808 RealType kineticTerm = kp_val * pi * pj;
809 J = potentialTerm + kineticTerm;
810 } else if (i_is_bend != j_is_bend) {
811 // Stretch ⟷ bend overtone Fermi coupling on same molecule.
812 // Constant fc_ (default 25 cm⁻¹, Ni & Skinner 2015 / Kananenka 2018).
813 J = fc_;
814 }
815 // else: both bends → impossible (one bend per molecule), J=0
816 } else {
817 // Different molecules: TDC coupling (vanishes naturally for
818 // bend chromophores since mx=0 was packed for them).
819 Vector3d r_ij = ohPos[j] - ohPos[i];
820 currentSnapshot_->wrapVector(r_ij);
821
822 Vector3d e_i = fd.mu[i];
823 RealType m_i = e_i.length();
824 if (m_i > 1.0e-12) e_i /= m_i;
825
826 Vector3d e_j = fd.mu[j];
827 RealType m_j = e_j.length();
828 if (m_j > 1.0e-12) e_j /= m_j;
829
830 RealType mx_i = intraJ[6*i + 5];
831 RealType mx_j = intraJ[6*j + 5];
832
833 J = tdcCoupling(r_ij, e_i, e_j, mx_i, mx_j);
834 }
835 fd.H(i, j) = J;
836 fd.H(j, i) = J;
837 }
838 }
839 }
840
841 // =========================================================================
842 // propagateF
843 //
844 // Adiabatic propagation step (MultiSpec moveF()):
845 // F <- exp(i H dt/hbar) . F
846 //
847 // exp(i H dt/hbar) = V . diag(exp(i w_k dt/hbar)) . V^T
848 // (V is real orthogonal since H is real symmetric)
849 //
850 // Steps:
851 // 1. Diagonalize H -> eigenvalues W, eigenvectors V
852 // Prefers dsyevd (LAPACK divide-and-conquer) when available;
853 // falls back to JAMA otherwise.
854 // 2. Phase factors: u_k = exp(i w_k dt / hbar)
855 // 3. tmp = V^T . F (real x complex, via cblas_dgemm if available)
856 // 4. tmp_k* *= u_k (row-wise phase scaling)
857 // 5. F = V . tmp (real x complex, via cblas_dgemm if available)
858 //
859 // Work arrays are thread_local static and resized only when N grows,
860 // so per-call heap allocation is zero in the steady state.
861 // =========================================================================
862 void SFG::propagateF(std::vector<std::complex<double>>& F,
864 double dt_ps, int N) {
865
866 using cdbl = std::complex<double>;
867
868 // -----------------------------------------------------------------------
869 // Thread-local scratch: grown as needed, never shrunk.
870 // Vbuf: eigenvectors, ROW-MAJOR: V(i,k) = Vbuf[i*N + k].
871 // Column k is the k-th eigenvector.
872 // This matches MultiSpec's convention (LAPACK_ROW_MAJOR).
873 // -----------------------------------------------------------------------
874 thread_local std::vector<double> Hbuf;
875 thread_local std::vector<double> Wbuf;
876 thread_local std::vector<double> Vbuf;
877 thread_local std::vector<double> work_d;
878 thread_local std::vector<int> iwork_d;
879 thread_local std::vector<double> Fr, Fi, Tr, Ti;
880 thread_local int lastN = 0; // tracks when workspace query is needed
881
882 const int N2 = N * N;
883 if (static_cast<int>(Hbuf.size()) < N2) {
884 Hbuf.resize(N2); Wbuf.resize(N); Vbuf.resize(N2);
885 Fr.resize(N2); Fi.resize(N2);
886 Tr.resize(N2); Ti.resize(N2);
887 }
888
889 // -----------------------------------------------------------------------
890 // Step 1: Diagonalize H -> Wbuf (eigenvalues),
891 // Vbuf (eigenvectors row-major)
892 // -----------------------------------------------------------------------
893#if defined(HAVE_LAPACK)
894 // Copy H's contiguous row-major storage into Hbuf.
895 // H is symmetric so row-major == column-major for dsyevd_.
896 // dsyevd_ will overwrite Hbuf with eigenvectors (column-major).
897 {
898 const RealType* src = H.getArrayPointer();
899 for (int i = 0; i < N2; ++i)
900 Hbuf[i] = static_cast<double>(src[i]);
901 }
902
903 // Workspace query — only needed when N changes.
904 if (N != lastN) {
905 int lwork_q = -1, liwork_q = -1, info_q = 0;
906 double wq = 0.0;
907 int wiq = 0;
908 std::vector<double> Htmp(N2, 0.0);
909 std::vector<double> Wtmp(N, 0.0);
910 dsyevd_("V", "U", &N, Htmp.data(), &N, Wtmp.data(),
911 &wq, &lwork_q, &wiq, &liwork_q, &info_q);
912 int lw = static_cast<int>(wq);
913 int liw = wiq;
914 if (static_cast<int>(work_d.size()) < lw) work_d.resize(lw);
915 if (static_cast<int>(iwork_d.size()) < liw) iwork_d.resize(liw);
916 lastN = N;
917 }
918
919 {
920 int lw = static_cast<int>(work_d.size());
921 int liw = static_cast<int>(iwork_d.size());
922 int info = 0;
923 dsyevd_("V", "U", &N, Hbuf.data(), &N, Wbuf.data(),
924 work_d.data(), &lw, iwork_d.data(), &liw, &info);
925 if (info == 0) {
926 // dsyevd_ (Fortran) returns eigenvectors in column-major:
927 // Hbuf[i + k*N] = V(i,k).
928 // Transpose into row-major Vbuf: Vbuf[i*N + k] = V(i,k).
929 for (int i = 0; i < N; ++i)
930 for (int k = 0; k < N; ++k)
931 Vbuf[i*N + k] = Hbuf[i + k*N];
932 goto after_diag;
933 }
934 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
935 "SFG::propagateF: dsyevd returned info=%d; using JAMA.\n", info);
936 painCave.isFatal = 0;
937 painCave.severity = OPENMD_WARNING;
938 simError();
939 }
940#endif // HAVE_LAPACK
941
942 // JAMA fallback
943 {
945 DynamicVector<RealType> evals_j(N);
946 DynamicRectMatrix<RealType> evecs_j(N, N);
947 eig.getRealEigenvalues(evals_j);
948 eig.getV(evecs_j);
949 for (int k = 0; k < N; ++k) {
950 Wbuf[k] = static_cast<double>(evals_j[k]);
951 for (int i = 0; i < N; ++i)
952 Vbuf[i*N + k] = static_cast<double>(evecs_j(i, k));
953 }
954 }
955
956#if defined(HAVE_LAPACK)
957 after_diag:
958#endif
959
960 // -----------------------------------------------------------------------
961 // Step 2: Phase factors u_k = exp(i w_k dt / hbar)
962 // -----------------------------------------------------------------------
963 std::vector<cdbl> phase(N);
964 for (int k = 0; k < N; ++k) {
965 double arg = Wbuf[k] * dt_ps / HBAR_CM_PS;
966 phase[k] = std::exp(cdbl(0.0, arg));
967 }
968
969 // -----------------------------------------------------------------------
970 // Steps 3-5: F <- V · diag(phase) · V^T · F
971 //
972 // All arrays are ROW-MAJOR: M(i,j) = buf[i*N + j].
973 // Vbuf is row-major: V(i,k) = Vbuf[i*N + k].
974 // F is row-major: F(i,j) = F[i*N + j].
975 //
976 // Split F into real/imag parts.
977 //
978 // Step 3: T = V^T · F → T(k,j) = Σ_i V(i,k)·F(i,j)
979 // Step 4: T(k,j) *= phase[k]
980 // Step 5: F = V · T → F(i,j) = Σ_k V(i,k)·T(k,j)
981 //
982 // For CBLAS with CblasRowMajor:
983 // Step 3: T = V^T * F → dgemm(RowMajor, Trans, NoTrans, ...)
984 // Step 5: F = V * T → dgemm(RowMajor, NoTrans, NoTrans, ...)
985 // -----------------------------------------------------------------------
986
987 for (int idx = 0; idx < N2; ++idx) {
988 Fr[idx] = F[idx].real();
989 Fi[idx] = F[idx].imag();
990 }
991
992#if defined(HAVE_CBLAS)
993# if defined(__APPLE__)
994# pragma clang diagnostic push
995# pragma clang diagnostic ignored "-Wdeprecated-declarations"
996# endif
997 // Step 3: T = V^T * F (row-major throughout)
998 cblas_dgemm(CblasRowMajor, CblasTrans, CblasNoTrans,
999 N, N, N, 1.0, Vbuf.data(), N, Fr.data(), N, 0.0, Tr.data(), N);
1000 cblas_dgemm(CblasRowMajor, CblasTrans, CblasNoTrans,
1001 N, N, N, 1.0, Vbuf.data(), N, Fi.data(), N, 0.0, Ti.data(), N);
1002# if defined(__APPLE__)
1003# pragma clang diagnostic pop
1004# endif
1005#else
1006 // Manual fallback: T(k,j) = Σ_i V(i,k) * F(i,j)
1007 std::fill(Tr.begin(), Tr.begin()+N2, 0.0);
1008 std::fill(Ti.begin(), Ti.begin()+N2, 0.0);
1009 for (int k = 0; k < N; ++k)
1010 for (int i = 0; i < N; ++i) {
1011 double vik = Vbuf[i*N + k]; // V(i,k) row-major
1012 for (int j = 0; j < N; ++j) {
1013 Tr[k*N+j] += vik * Fr[i*N+j];
1014 Ti[k*N+j] += vik * Fi[i*N+j];
1015 }
1016 }
1017#endif
1018
1019 // Step 4: Apply phase row-wise: T(k,:) *= phase[k]
1020 for (int k = 0; k < N; ++k) {
1021 double pr = phase[k].real(), pi = phase[k].imag();
1022 for (int j = 0; j < N; ++j) {
1023 double tr = Tr[k*N+j], ti = Ti[k*N+j];
1024 Tr[k*N+j] = pr*tr - pi*ti;
1025 Ti[k*N+j] = pr*ti + pi*tr;
1026 }
1027 }
1028
1029#if defined(HAVE_CBLAS)
1030# if defined(__APPLE__)
1031# pragma clang diagnostic push
1032# pragma clang diagnostic ignored "-Wdeprecated-declarations"
1033# endif
1034 // Step 5: F = V * T (row-major throughout)
1035 cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
1036 N, N, N, 1.0, Vbuf.data(), N, Tr.data(), N, 0.0, Fr.data(), N);
1037 cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
1038 N, N, N, 1.0, Vbuf.data(), N, Ti.data(), N, 0.0, Fi.data(), N);
1039# if defined(__APPLE__)
1040# pragma clang diagnostic pop
1041# endif
1042#else
1043 // Manual fallback: F(i,j) = Σ_k V(i,k) * T(k,j)
1044 std::fill(Fr.begin(), Fr.begin()+N2, 0.0);
1045 std::fill(Fi.begin(), Fi.begin()+N2, 0.0);
1046 for (int i = 0; i < N; ++i)
1047 for (int k = 0; k < N; ++k) {
1048 double vik = Vbuf[i*N + k]; // V(i,k) row-major
1049 for (int j = 0; j < N; ++j) {
1050 Fr[i*N+j] += vik * Tr[k*N+j];
1051 Fi[i*N+j] += vik * Ti[k*N+j];
1052 }
1053 }
1054#endif
1055
1056 for (int idx = 0; idx < N2; ++idx)
1057 F[idx] = cdbl(Fr[idx], Fi[idx]);
1058 }
1059 // =========================================================================
1060 // accumulateTCF
1061 //
1062 // Computes the SFG TCF contributions at lag tt for all polarizations:
1063 //
1064 // For pqr (e.g. yyz, ssp):
1065 // C_pqr(tt) += Σᵢ α_pq,i(t) × [F · μ_r(0)]ᵢ × exp_tc
1066 //
1067 // where [F · μ_r(0)]ᵢ = Σⱼ F_ij · μ_r,j(0) (propagated reference dipole)
1068 //
1069 // Mirroring MultiSpec calcSFG():
1070 // work = F · mu0 (N-vector, complex)
1071 // C_yyz(t) += conj(α_yy(t)) · work_z ← note MultiSpec uses conj(α)
1072 //
1073 // Actually MultiSpec:
1074 // tpt[ii] = conj(plz_yy[ii]) ← conjugate of α at time t
1075 // result = tpt · (F · mu0) ← dot product
1076 // So: C_yyz = Σᵢ α_yy,i*(t) · Σⱼ F_ij · μ_z,j(0)
1077 //
1078 // For real α (as in our bond-polarizability model), conj(α) = α.
1079 // But we keep the conjugate for generality.
1080 // =========================================================================
1081 void SFG::accumulateTCF(int tt,
1082 const std::vector<std::complex<double>>& F,
1083 const FrameData& fd0,
1084 const FrameData& fdt,
1085 int N, double exptc,
1086 std::vector<std::complex<double>>& tgt_ssp,
1087 std::vector<std::complex<double>>& tgt_ppp,
1088 std::vector<std::complex<double>>& tgt_sps) {
1089
1090 using cdbl = std::complex<double>;
1091 const int n = privilegedAxis_; // interface normal axis index
1092 const int s1 = s1_; // first surface-parallel axis
1093 const int s2 = s2_; // second surface-parallel axis
1094
1095 // Fmu[r][i] = sum_j F[i*N+j] * mu_r,j(0) for r in {n, s1, s2}
1096 auto matvec = [&](int r) -> std::vector<cdbl> {
1097 std::vector<cdbl> res(N, cdbl(0.0, 0.0));
1098 for (int i = 0; i < N; ++i)
1099 for (int j = 0; j < N; ++j)
1100 res[i] += F[i*N+j] * static_cast<double>(fd0.mu[j][r]);
1101 return res;
1102 };
1103
1104 std::vector<cdbl> Fmu_n = matvec(n);
1105 std::vector<cdbl> Fmu_s1 = matvec(s1);
1106 std::vector<cdbl> Fmu_s2 = matvec(s2);
1107
1108 // conj(alpha_pq(t)) dotted against propagated dipole component.
1109 auto dot_alpha = [&](int p, int q, const std::vector<cdbl>& Fmu) -> cdbl {
1110 cdbl s(0.0, 0.0);
1111 for (int i = 0; i < N; ++i)
1112 s += std::conj(cdbl(fdt.alpha[i](p, q))) * Fmu[i];
1113 return s;
1114 };
1115
1116 cdbl decay(exptc, 0.0);
1117
1118 // SSP: average chi2_{s1,s1,n} and chi2_{s2,s2,n}
1119 tgt_ssp[tt] += 0.5 * (dot_alpha(s1, s1, Fmu_n)
1120 + dot_alpha(s2, s2, Fmu_n)) * decay;
1121
1122 // SPS: average s1 and s2 channels
1123 tgt_sps[tt] += 0.5 * (dot_alpha(s1, n, Fmu_s1)
1124 + dot_alpha(s2, n, Fmu_s2)) * decay;
1125
1126 // PPP: near-normal approximation (Buch et al. 2007)
1127 // MultiSpec: pppt += (-0.5*cxxz - 0.5*cyyz + czzz) * exptc
1128 tgt_ppp[tt] += (-0.5 * dot_alpha(s1, s1, Fmu_n)
1129 - 0.5 * dot_alpha(s2, s2, Fmu_n)
1130 + dot_alpha(n, n, Fmu_n)) * decay;
1131 }
1132
1133 // =========================================================================
1134 // remapFrame
1135 //
1136 // Build a FrameData of size refIDs.size() by looking up each atom in src.
1137 // Atoms present in src are copied directly.
1138 // Atoms absent from src (left the z-selection) get:
1139 // - zero μ and α (no contribution to TCF numerator)
1140 // - H diagonal from refFreqs (keeps F propagation sensible)
1141 // - zero off-diagonal couplings to/from that row/column
1142 //
1143 // Returns false if more than half the reference atoms are missing,
1144 // signalling the window has drifted too far to be meaningful.
1145 // =========================================================================
1146 bool SFG::remapFrame(const FrameData& src,
1147 const std::vector<int>& refIDs,
1148 const std::vector<RealType>& refFreqs,
1149 FrameData& out,
1150 int refNStretch,
1151 int refNBend) {
1152 int N = static_cast<int>(refIDs.size());
1153 out.N = N;
1154 // Use the reference-frame's stretch/bend partition. refIDs is
1155 // ordered with stretches first, then bends.
1156 out.nStretch = refNStretch;
1157 out.nBend = refNBend;
1158 out.globalIDs = refIDs;
1159
1160 out.mu.assign(N, Vector3d(0.0, 0.0, 0.0));
1161 out.alpha.assign(N, Mat3x3d(0.0));
1162 out.H = DynamicRectMatrix<RealType>(N, N, 0.0);
1163
1164 int nMissing = 0;
1165 for (int i = 0; i < N; ++i) {
1166 auto it = src.idToIndex.find(refIDs[i]);
1167 if (it == src.idToIndex.end()) {
1168 out.H(i, i) = refFreqs[i];
1169 ++nMissing;
1170 } else {
1171 int j = it->second;
1172 out.mu[i] = src.mu[j];
1173 out.alpha[i] = src.alpha[j];
1174 out.H(i, i) = src.H(j, j);
1175 for (int k = 0; k < N; ++k) {
1176 if (k == i) continue;
1177 auto kt = src.idToIndex.find(refIDs[k]);
1178 if (kt != src.idToIndex.end()) {
1179 out.H(i, k) = src.H(j, kt->second);
1180 out.H(k, i) = src.H(kt->second, j);
1181 }
1182 }
1183 }
1184 }
1185 return nMissing <= N / 2;
1186 }
1187
1188 // =========================================================================
1189 // tdcCoupling
1190 //
1191 // Transition-dipole coupling between two OH chromophores, computed in
1192 // MultiSpec's convention (water.cpp::waterTDC + intermC). The signature
1193 // takes UNIT OH vectors (eᵢ, eⱼ), the dipole-location separation r_ij
1194 // (in Å), and the per-site matrix elements (μ′·x₁₀)ᵢ and (μ′·x₁₀)ⱼ
1195 // in atomic units.
1196 //
1197 // J [cm⁻¹] = AU_TO_WN × [eᵢ·eⱼ − 3(eᵢ·r̂)(eⱼ·r̂)] / r_bohr³
1198 // × (μ′·x₁₀)ᵢ × (μ′·x₁₀)ⱼ
1199 //
1200 // The geometric part uses unit OH vectors (no dipole magnitudes). The
1201 // four field-dependent matrix elements then convert the geometric
1202 // coupling into actual cm⁻¹. This keeps maps, transition dipoles, and
1203 // TDC couplings all in a consistent atomic-unit framework — which is
1204 // what Auer/Skinner/Gruenbaum/Kananenka use throughout.
1205 //
1206 // The previous Debye-based formula (5034.12 × μ_D²/r_ų) is numerically
1207 // equivalent in principle, but accumulates conversion factors at each
1208 // site and is harder to verify against published values.
1209 // =========================================================================
1210 RealType SFG::tdcCoupling(const Vector3d& r_ij,
1211 const Vector3d& e_i,
1212 const Vector3d& e_j,
1213 RealType mx_i, RealType mx_j) {
1214 RealType r_A = r_ij.length();
1215 if (r_A < 1.0e-6) return 0.0;
1216 Vector3d rhat = r_ij / r_A;
1217
1218 // Convert separation to bohr
1219 const RealType A_to_bohr = 1.0 / 0.52917721067;
1220 RealType r_bohr = r_A * A_to_bohr;
1221 RealType r3 = r_bohr * r_bohr * r_bohr;
1222
1223 // 1 hartree in cm⁻¹
1224 const RealType AU_TO_WN = 219474.6313705;
1225
1226 RealType geom = dot(e_i, e_j) - 3.0 * dot(e_i, rhat) * dot(e_j, rhat);
1227
1228 return AU_TO_WN * geom * mx_i * mx_j / r3;
1229 }
1230
1231 // =========================================================================
1232 // bondPolarizability
1233 //
1234 // Transition polarizability tensor for one OH bond.
1235 // α_ab = [α_perp·δ_ab + (α_par − α_perp)·ê_a·ê_b] × (x10 / x10_gas)
1236 //
1237 // The published α_par/α_perp values incorporate the gas-phase x10.
1238 // Following MultiSpec's trPol(), we scale by the local x10 (which
1239 // depends on the OH frequency / electric field) divided by x10_gas
1240 // (the constant term of the x10 map) so the Raman tensor reflects
1241 // the field-dependent coordinate matrix element.
1242 // =========================================================================
1243 Mat3x3d SFG::bondPolarizability(const Vector3d& ohUnit,
1244 RealType alpha_par, RealType alpha_perp,
1245 RealType x10, RealType x10_gas) {
1246 RealType scale = (x10_gas > 0.0) ? x10 / x10_gas : 1.0;
1247 RealType da = alpha_par - alpha_perp;
1248 Mat3x3d a(0.0);
1249 for (int p = 0; p < 3; ++p) {
1250 a(p, p) = (alpha_perp + da * ohUnit[p] * ohUnit[p]) * scale;
1251 for (int q = p+1; q < 3; ++q) {
1252 a(p, q) = da * ohUnit[p] * ohUnit[q] * scale;
1253 a(q, p) = a(p, q);
1254 }
1255 }
1256 return a;
1257 }
1258
1259 // =========================================================================
1260 // writeCorrelate
1261 //
1262 // Writes:
1263 // <prefix>.sfg.tcf — real and imaginary parts of all TCFs vs time
1264 // <prefix>.sfg — SFG spectrum Im χ²(ω) with quantum correction
1265 // for the polarization selected on the command line
1266 //
1267 // FFT convention: r2c with normalization by N; frequency axis in cm⁻¹.
1268 // Quantum correction (Bose-Einstein):
1269 // Q(ω) = (ω/kT) / (1 − exp(−ω/kT))
1270 // =========================================================================
1271 void SFG::writeCorrelate() {
1272 Revision r;
1273
1274 // -----------------------------------------------------------------------
1275 // Write time-domain TCFs for diagnostics
1276 // -----------------------------------------------------------------------
1277
1278 std::string tcfName = outputFilename_ + ".tcf";
1279 std::ofstream tcf(tcfName.c_str());
1280 if (!tcf.is_open()) {
1281 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
1282 "SFG::writeCorrelate: cannot open %s\n", tcfName.c_str());
1283 painCave.isFatal = 1; simError();
1284 }
1285 tcf << "# SFG time-domain TCFs\n";
1286 tcf << "# OpenMD " << r.getFullRevision() << "\n";
1287 tcf << "# " << r.getBuildDate() << "\n";
1288 tcf << "# selection: \"" << selectionScript1_ << "\"\n";
1289 tcf << "# navg = " << navg_ << " ncor = " << nTimeBins_ << "\n";
1290 tcf << "#t(ps)\tRe_ssp\tIm_ssp\tRe_ppp\tIm_ppp\tRe_sps\tIm_sps\n";
1291
1292 for (int tt = 0; tt < static_cast<int>(nTimeBins_); ++tt) {
1293 tcf << tt * dt_ps_ << "\t"
1294 << tcf_ssp_[tt].real() << "\t" << tcf_ssp_[tt].imag() << "\t"
1295 << tcf_ppp_[tt].real() << "\t" << tcf_ppp_[tt].imag() << "\t"
1296 << tcf_sps_[tt].real() << "\t" << tcf_sps_[tt].imag() << "\n";
1297 }
1298 tcf.close();
1299
1300#if defined(HAVE_FFTW3_H)
1301
1302 const double ps_to_invcm = 33.3564;
1303
1304 // Select TCF based on polarization argument
1305 const std::vector<std::complex<double>>* pTCF = &tcf_ssp_;
1306 if (polarization_ == "ppp") pTCF = &tcf_ppp_;
1307 else if (polarization_ == "sps") pTCF = &tcf_sps_;
1308
1309 // N_corr: number of real TCF points (determines intrinsic resolution).
1310 // N_fft: FFT size after zero-filling (determines grid spacing).
1311 // The Hermitian extension requires N_fft >= 2*N_corr.
1312 int N_corr = static_cast<int>(nTimeBins_);
1313 if (N_corr < 2) {
1314 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
1315 "SFG::writeCorrelate: too few TCF points (%d) for FFT.\n", N_corr);
1316 painCave.isFatal = 1; simError();
1317 }
1318
1319 // Zero-filling: extend to T_corr + t_zerofill_ps_, rounded up to next
1320 // power of two for FFTW efficiency. Minimum is 2*N_corr for the
1321 // Hermitian extension (C(-t) = C*(t) occupies the upper half).
1322 int N_zf = (t_zerofill_ps_ > 0.0)
1323 ? static_cast<int>(std::ceil((static_cast<double>(N_corr)
1324 + t_zerofill_ps_ / dt_ps_)))
1325 : N_corr;
1326 N_zf = std::max(N_zf, 2 * N_corr);
1327 // Round up to next power of two
1328 int N_fft = 1;
1329 while (N_fft < N_zf) N_fft <<= 1;
1330
1331 fftw_complex* in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex)*N_fft);
1332 fftw_complex* out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex)*N_fft);
1333 if (!in || !out) {
1334 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
1335 "SFG: FFTW malloc failed.\n");
1336 painCave.isFatal = 1; simError();
1337 }
1338
1339 // Pre-apply apodization to the TCF data and store in a work vector.
1340 std::vector<std::complex<double>> apodTCF(N_corr);
1341 for (int i = 0; i < N_corr; ++i) {
1342 double t = static_cast<double>(i) * dt_ps_;
1343 double w = (t_apod_ps_ > 0.0) ? std::exp(-t / t_apod_ps_) : 1.0;
1344 apodTCF[i] = (*pTCF)[i] * w;
1345 }
1346
1347 // Result vector for the two-pass Hermitian FFT
1348 std::vector<std::complex<double>> spectrum(N_fft);
1349
1350 fftw_plan plan = fftw_plan_dft_1d(N_fft, in, out,
1351 FFTW_FORWARD, FFTW_ESTIMATE);
1352
1353 // -----------------------------------------------------------------------
1354 // Two-pass Hermitian FFT (following MultiSpec dofft.cpp).
1355 //
1356 // The one-sided TCF C(t) for t >= 0 is extended to negative times
1357 // using the physical symmetry C(-t) = C*(t). This Hermitian
1358 // extension ensures that the real and imaginary parts of the
1359 // Fourier transform are cleanly separated into dispersive (Re)
1360 // and absorptive (Im) lineshapes with no phase mixing.
1361 //
1362 // -----------------------------------------------------------------------
1363 // The SFG response carries an explicit leading factor of i:
1364 // chi2(w) ∝ i ∫ dt e^{-iwt} C(t).
1365 // Let Ctilde(w) = ∫ dt e^{-iwt} C(t) be the raw transform. Then
1366 // chi2 = i·Ctilde => Re(chi2) = -Im(Ctilde), Im(chi2) = +Re(Ctilde).
1367 // Pass 1 below computes Re(Ctilde) (cosine transform of the Hermitian
1368 // extension); Pass 2 computes Im(Ctilde). We therefore assign:
1369 // Im(chi2) = Re(Ctilde) [pass 1]
1370 // Re(chi2) = -Im(Ctilde) [pass 2]
1371 // so that Im(chi2) is the absorptive lineshape (positive H-bonded band
1372 // for the conventional upper face), matching the time-averaging module.
1373 //
1374 // Pass 1: input = C(t) for t>0, C*(-t) for t<0 → Re(Ctilde) → Im(chi2)
1375 // Pass 2: input = -i·C(t), Hermitian-extended → Im(Ctilde) → -Re(chi2)
1376 // -----------------------------------------------------------------------
1377
1378 // Pass 1: get Re(Ctilde), which becomes Im(chi2)
1379 for (int i = 0; i < N_fft; ++i) { in[i][0] = 0.0; in[i][1] = 0.0; }
1380 for (int i = 0; i < N_corr; ++i) {
1381 in[i][0] = apodTCF[i].real();
1382 in[i][1] = apodTCF[i].imag();
1383 if (i > 0) {
1384 // Hermitian mirror: C(-t) = C*(t) at index N_fft - i
1385 in[N_fft - i][0] = apodTCF[i].real();
1386 in[N_fft - i][1] = -apodTCF[i].imag();
1387 }
1388 }
1389 fftw_execute(plan);
1390 // Hermitian input → real output. This is Re(Ctilde) = Im(chi2).
1391 for (int i = 0; i < N_fft; ++i)
1392 spectrum[i].imag(out[i][0]);
1393
1394 // Pass 2: get Im(Ctilde), which becomes -Re(chi2)
1395 // Multiply TCF by -i: -i·C = Im(C) - i·Re(C)
1396 for (int i = 0; i < N_fft; ++i) { in[i][0] = 0.0; in[i][1] = 0.0; }
1397 for (int i = 0; i < N_corr; ++i) {
1398 double re_ic = apodTCF[i].imag();
1399 double im_ic = -apodTCF[i].real();
1400 in[i][0] = re_ic;
1401 in[i][1] = im_ic;
1402 if (i > 0) {
1403 // Hermitian mirror of -i·C(t)
1404 in[N_fft - i][0] = re_ic;
1405 in[N_fft - i][1] = -im_ic;
1406 }
1407 }
1408 fftw_execute(plan);
1409 // out[i][0] = Re(transform of -i·C) = Im(Ctilde). Re(chi2) = -Im(Ctilde).
1410 for (int i = 0; i < N_fft; ++i)
1411 spectrum[i].real(-out[i][0]);
1412
1413 fftw_destroy_plan(plan);
1414
1415 // Normalization: we normalize by 2*N_corr — the factor of N_corr keeps
1416 // amplitudes independent of zero-filling, and the factor of 2 accounts
1417 // for the Hermitian extension which places C(t) at both positive and
1418 // negative times, doubling the effective signal relative to the one-sided
1419 // transform.
1420 const double norm = 1.0 / (2.0 * static_cast<double>(N_corr));
1421 for (int i = 0; i < N_fft; ++i)
1422 spectrum[i] *= norm;
1423
1424 // Frequency grid
1425 const double dOmega = ps_to_invcm / (static_cast<double>(N_fft) * dt_ps_);
1426 const double dOmega_res = ps_to_invcm / (static_cast<double>(N_corr) * dt_ps_);
1427
1428 // Temperature
1429 RealType T = 300.0;
1430 if (info_->getSimParams()->haveTargetTemp())
1431 T = info_->getSimParams()->getTargetTemp();
1432 const double kT_invcm = 0.6950356 * static_cast<double>(T);
1433
1434 std::ofstream ofs(outputFilename_.c_str());
1435 if (!ofs.is_open()) {
1436 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
1437 "SFG: cannot open %s\n", outputFilename_.c_str());
1438 painCave.isFatal = 1; simError();
1439 }
1440
1441 ofs << "# SFG spectrum (" << polarization_ << ")\n";
1442 ofs << "# OpenMD " << r.getFullRevision() << "\n";
1443 ofs << "# " << r.getBuildDate() << "\n";
1444 ofs << "# selection: \"" << selectionScript1_ << "\"\n";
1445 ofs << "# T = " << T << " K, kT = " << kT_invcm << " cm-1\n";
1446 ofs << "# wAvg = " << wAvg_ << " cm-1\n";
1447 ofs << "# navg = " << navg_ << " nTimeBins = " << nTimeBins_ << "\n";
1448 ofs << "# intrinsic resolution = " << dOmega_res << " cm-1";
1449 if (t_apod_ps_ > 0.0)
1450 ofs << " t_apod = " << t_apod_ps_ << " ps"
1451 << " (added broadening ~"
1452 << ps_to_invcm / (M_PI * t_apod_ps_) << " cm-1)";
1453 ofs << "\n";
1454 ofs << "# dOmega = " << dOmega << " cm-1";
1455 if (t_zerofill_ps_ > 0.0)
1456 ofs << " (zero-filled: N_corr=" << N_corr
1457 << " -> N_fft=" << N_fft << ")";
1458 ofs << "\n";
1459 ofs << "#omega(cm-1)\tRe(chi2)\tIm(chi2)\t|chi2|^2\n";
1460
1461 // Output frequency range: wAvg_ ± (N_corr/2)*dOmega_res from wAvg.
1462 // Convert intrinsic bandwidth to bin count on the zero-filled grid.
1463 const double halfBW_freq = (N_corr / 2) * dOmega_res;
1464 const int halfBW = static_cast<int>(std::round(halfBW_freq / dOmega));
1465
1466 auto writeRow = [&](double omega, int k) {
1467 if (omega <= 0.0) return;
1468 double re = spectrum[k].real();
1469 double im = spectrum[k].imag();
1470 double x = omega / kT_invcm;
1471 double qCorr = (x > 1.0e-6) ?
1472 ((x < 50.0) ? x / (1.0 - std::exp(-x)) : x) : 1.0;
1473 re *= qCorr;
1474 im *= qCorr;
1475 ofs << omega << "\t" << re << "\t" << im << "\t"
1476 << re*re + im*im << "\n";
1477 };
1478
1479 // negative half → ω below wAvg_ (ascending)
1480 for (int k = N_fft - halfBW; k < N_fft; ++k)
1481 writeRow(wAvg_ + (k - N_fft) * dOmega, k);
1482 // positive half → ω at/above wAvg_
1483 for (int k = 0; k <= halfBW; ++k)
1484 writeRow(wAvg_ + k * dOmega, k);
1485
1486 ofs.close();
1487 fftw_free(in);
1488 fftw_free(out);
1489
1490#else
1491 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
1492 "SFG: FFTW3 required for SFG spectra.\n");
1493 painCave.isFatal = 1; simError();
1494#endif
1495 }
1496} // namespace OpenMD
Computes eigenvalues and eigenvectors of a real (non-complex) matrix.
Rectangular matrix class with contiguous flat storage.
Real * getArrayPointer()
Returns a pointer to the contiguous internal storage (row-major).
Dynamically-sized vector class.
One of the heavy-weight classes of OpenMD, SimInfo maintains objects and variables relating to the cu...
Definition SimInfo.hpp:96
void normalize()
Normalizes this vector in place.
Definition Vector.hpp:406
Real length() const
Returns the length of this vector.
Definition Vector.hpp:397
This basic Periodic Table class was originally taken from the data.cpp file in OpenBabel.
Vector3< Real > cross(const Vector3< Real > &v1, const Vector3< Real > &v2)
Returns the cross product of two Vectors.
Definition Vector3.hpp:139
Real dot(const DynamicVector< Real > &v1, const DynamicVector< Real > &v2)
Returns the dot product of two DynamicVectors.
std::string getPrefix(const std::string &str)