OpenMD 3.2
Molecular Dynamics in the Open
Loading...
Searching...
No Matches
SFGTimeAvg.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 * SUPPORT OPEN SCIENCE! If you use OpenMD or its source code in your
32 * research, please cite the following paper when you publish your work:
33 *
34 * [1] Drisko et al., J. Open Source Softw. 9, 7004 (2024).
35 */
36
37#include "applications/staticProps/SFGTimeAvg.hpp"
38
39#include <algorithm>
40#include <cmath>
41#include <fstream>
42
45#include "io/DumpReader.hpp"
46#include "math/Eigenvalue.hpp"
48#include "utils/simError.h"
49
50#if defined(HAVE_LAPACK)
51extern "C" {
52 void dsyevd_(const char* jobz, const char* uplo, const int* n,
53 double* a, const int* lda, double* w,
54 double* work, const int* lwork,
55 int* iwork, const int* liwork, int* info);
56}
57#endif
58
59namespace OpenMD {
60
61 SFGTimeAvg::SFGTimeAvg(SimInfo* info, const std::string& filename,
62 const std::string& sele1, int nbins,
63 const std::string& polarization, int privilegedAxis,
64 RealType gamma, RealType fc) :
65 StaticAnalyser(info, filename, nbins),
66 selectionScript1_(sele1), seleMan1_(info_), evaluator1_(info_),
67 polarization_(polarization),
68 privilegedAxis_(privilegedAxis),
69 s1_((privilegedAxis + 1) % 3),
70 s2_((privilegedAxis + 2) % 3),
71 gamma_(gamma), fc_(fc) {
72
73 setOutputName(getPrefix(filename) + ".sfgta");
74
75 dumpHasElectricFields_ = info_->getSimParams()->getOutputElectricField();
76
77 if (!dumpHasElectricFields_) {
78 int atomStorageLayout = info_->getAtomStorageLayout();
79 int rigidBodyStorageLayout = info->getRigidBodyStorageLayout();
80 int cutoffGroupStorageLayout = info->getCutoffGroupStorageLayout();
81
82 atomStorageLayout |= DataStorage::dslElectricField;
83 rigidBodyStorageLayout |= DataStorage::dslElectricField;
84 info_->setAtomStorageLayout(atomStorageLayout);
85 info_->setRigidBodyStorageLayout(rigidBodyStorageLayout);
86 info_->setCutoffGroupStorageLayout(cutoffGroupStorageLayout);
87
88 info_->setSnapshotManager(new SimSnapshotManager(info_, atomStorageLayout,
89 rigidBodyStorageLayout,
90 cutoffGroupStorageLayout));
91 }
92 info_->getSimParams()->setOutputElectricField(true);
93
94 evaluator1_.loadScriptString(sele1);
95 if (!evaluator1_.isDynamic()) {
96 seleMan1_.setSelectionSet(evaluator1_.evaluate());
97 }
98
99 chi2_.resize(nBins_);
100
101 // ---------------------------------------------------------------------
102 // Spectroscopic maps (identical to the dynamical SFG module).
103 // ---------------------------------------------------------------------
104 // SPC/E (Auer & Skinner 2008)
105 w10_["H_SPCE"] = std::make_tuple(3761.6, -5060.4, -86225.0);
106 muPrime_["H_SPCE"] = std::make_tuple(0.1333, 14.17, 0.0);
107 x10_["H_SPCE"] = std::make_pair(0.1934, -1.75e-5);
108 p10_["H_SPCE"] = std::make_pair(1.611, 5.893e-4);
109 wintra_["H_SPCE"] = std::make_tuple(-1789.0, 23852.0, -1.966);
110 alphaMap_["H_SPCE"] = std::make_pair(0.185, 0.033);
111 tdcLoc_["H_SPCE"] = 0.58;
112
113 // TIP4P (Gruenbaum et al. 2013)
114 w10_["H_TIP4P"] = std::make_tuple(3760.2, -3541.7, -152677.0);
115 muPrime_["H_TIP4P"] = std::make_tuple(0.1646, 11.39, 63.41);
116 x10_["H_TIP4P"] = std::make_pair(0.19285, -1.7261e-5);
117 p10_["H_TIP4P"] = std::make_pair(1.6466, 5.7692e-4);
118 wintra_["H_TIP4P"] = std::make_tuple(-1361.0, 27165.0, -1.887);
119 alphaMap_["H_TIP4P"] = std::make_pair(0.185, 0.033);
120 tdcLoc_["H_TIP4P"] = 0.67;
121
122 // TIP4P-Ice (transferability from TIP4P; Takayama 2023)
123 w10_["H_TIP4P-Ice"] = w10_["H_TIP4P"];
124 muPrime_["H_TIP4P-Ice"] = muPrime_["H_TIP4P"];
125 x10_["H_TIP4P-Ice"] = x10_["H_TIP4P"];
126 p10_["H_TIP4P-Ice"] = p10_["H_TIP4P"];
127 wintra_["H_TIP4P-Ice"] = wintra_["H_TIP4P"];
128 alphaMap_["H_TIP4P-Ice"] = alphaMap_["H_TIP4P"];
129 tdcLoc_["H_TIP4P-Ice"] = tdcLoc_["H_TIP4P"];
130
131 // HOH bend overtone maps (Ni & Skinner 2015)
132 wb01_["TIP4P"] = std::make_pair(1581.46, 2938.51);
133 wb12_["TIP4P"] = std::make_pair(1551.32, 3147.80);
134 wb01_["TIP4P-Ice"] = wb01_["TIP4P"];
135 wb12_["TIP4P-Ice"] = wb12_["TIP4P"];
136 wb01_["SPCE"] = wb01_["TIP4P"];
137 wb12_["SPCE"] = wb12_["TIP4P"];
138
139 // Applied external field
140 EF_ = V3Zero;
141 std::vector<RealType> ef;
142 bool efSpec = false;
143 if (info_->getSimParams()->haveElectricField()) {
144 efSpec = true; ef = info_->getSimParams()->getElectricField();
145 }
146 if (info_->getSimParams()->haveUniformField()) {
147 efSpec = true; ef = info_->getSimParams()->getUniformField();
148 }
149 if (efSpec) {
150 if (ef.size() != 3) {
151 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
152 "SFGTimeAvg: uniformField needs 3 parameters, %zu given.\n",
153 ef.size());
154 painCave.isFatal = 1; simError();
155 }
156 EF_.x() = ef[0]; EF_.y() = ef[1]; EF_.z() = ef[2];
157 }
158 }
159
160 // =========================================================================
161 // process: single pass over the dump file
162 // =========================================================================
163 void SFGTimeAvg::process() {
164 ForceManager* forceMan = nullptr;
165
166 DumpReader reader(info_, dumpFilename_);
167 int nFrames = reader.getNFrames();
168 nProcessed_ = 0;
169
170 if (!dumpHasElectricFields_) {
171 forceMan = new ForceManager(info_);
172 forceMan->setDoElectricField(true);
173 forceMan->initialize();
174 }
175
176 std::fill(chi2_.begin(), chi2_.end(),
177 std::complex<double>(0.0, 0.0));
178
179 for (int istep = 0; istep < nFrames; istep += step_) {
180 reader.readFrame(istep);
181 currentSnapshot_ = info_->getSnapshotManager()->getCurrentSnapshot();
182
183 if (!dumpHasElectricFields_) {
184 forceMan->setDoElectricField(true);
185 forceMan->calcForces();
186 }
187
188 if (evaluator1_.isDynamic()) {
189 seleMan1_.setSelectionSet(evaluator1_.evaluate());
190 }
191
192 std::vector<Vector3d> ohPos;
193 std::vector<int> molIndex;
194 std::vector<RealType> intraJ;
195 FrameData fd = extractFrame(ohPos, molIndex, intraJ);
196 if (fd.N == 0) continue;
197
198 buildHamiltonian(fd, ohPos, molIndex, intraJ);
199 accumulateFrame(fd);
200 nProcessed_++;
201 }
202
203 if (forceMan) delete forceMan;
204
205 // Orientation diagnostic summary
206 double avgFree = (diagNFree_ > 0)
207 ? diagMuzFree_ / static_cast<double>(diagNFree_) : 0.0;
208 double avgHB = (diagNHB_ > 0)
209 ? diagMuzHB_ / static_cast<double>(diagNHB_) : 0.0;
210 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
211 "SFGTimeAvg orientation diagnostic:\n"
212 "\t<mu_z> dangling (freq>3650): %.5f [n=%ld]\n"
213 "\t<mu_z> H-bonded (freq<3450): %.5f [n=%ld]\n"
214 "\t(these should have OPPOSITE signs for a real interface)\n",
215 avgFree, diagNFree_, avgHB, diagNHB_);
216 painCave.isFatal = 0; painCave.severity = OPENMD_INFO; simError();
217
218 writeSpectrum();
219 }
220
221 // =========================================================================
222 // extractFrame: build chromophore list, transition dipoles/polarizabilities,
223 // and Hamiltonian diagonal for the current snapshot. (Duplicated from the
224 // dynamical SFG module.)
225 // =========================================================================
226 SFGTimeAvg::FrameData
227 SFGTimeAvg::extractFrame(std::vector<Vector3d>& ohPos,
228 std::vector<int>& molIndex,
229 std::vector<RealType>& intraJ) {
230 const RealType kcalToAU = 0.0008432975573;
231 const RealType chrgToKcal = 23.060548;
232
233 FrameData fd;
234 ohPos.clear(); molIndex.clear(); intraJ.clear();
235
236 std::vector<RealType> freqs;
237 std::vector<RealType> x10vals, p10vals, mxvals, intraJ_k0, intraJ_kp;
238
239 struct MolData {
240 Vector3d Opos;
241 Vector3d ohUnit1, ohUnit2;
242 Vector3d Efield1, Efield2;
243 RealType rOH1 = 0.0, rOH2 = 0.0;
244 int ohCount = 0;
245 std::string modelName;
246 };
247 std::map<int, MolData> molMap;
248
249 int ii;
250 Molecule* mol;
251 for (mol = seleMan1_.beginSelectedMolecule(ii); mol != nullptr;
252 mol = seleMan1_.nextSelectedMolecule(ii)) {
253
254 int molID = mol->getGlobalIndex();
255
256 // Locate the oxygen and the (up to two) hydrogens of this water.
257 Vector3d Opos;
258 bool foundO = false;
259 std::vector<Atom*> hAtoms;
260 Molecule::AtomIterator ai;
261 for (Atom* atom = mol->beginAtom(ai); atom != nullptr;
262 atom = mol->nextAtom(ai)) {
263 const std::string& aname = atom->getAtomType()->getName();
264 if (aname[0] == 'O' && !foundO) {
265 Opos = atom->getPos();
266 foundO = true;
267 } else if (w10_.find(aname) != w10_.end()) {
268 // An H atom type for which we have a stretch map
269 hAtoms.push_back(atom);
270 }
271 }
272 if (!foundO) continue;
273 if (hAtoms.empty()) continue;
274
275 // Build a stretch chromophore for EACH OH bond of this molecule.
276 // Iterating over molecules (selected by COM z) and including both
277 // OH bonds removes the orientational bias that arises when selecting
278 // individual hydrogens within a thin z-slab (which preferentially
279 // admits up-pointing OH bonds).
280 for (Atom* hAtom : hAtoms) {
281 std::string name = hAtom->getAtomType()->getName();
282
283 auto wIt = w10_.find(name);
284 auto mpIt = muPrime_.find(name);
285 if (wIt == w10_.end() || mpIt == muPrime_.end()) continue;
286
287 Vector3d Hpos = hAtom->getPos();
288 Vector3d rOH_mic = Hpos - Opos;
289 currentSnapshot_->wrapVector(rOH_mic);
290 RealType rOH_len = rOH_mic.length();
291 Vector3d ohUnit = rOH_mic; ohUnit.normalize();
292
293 Vector3d sE = hAtom->getElectricField();
294 sE += EF_ * chrgToKcal;
295 sE *= kcalToAU;
296 RealType E = dot(ohUnit, sE);
297
298 auto [a0, a1, a2] = wIt->second;
299 RealType freq = a0 + a1*E + a2*E*E;
300
301 auto [m0, m1, m2] = mpIt->second;
302 RealType muPrime = m0 + m1*E + m2*E*E;
303
304 auto [x0, x1] = x10_.at(name);
305 RealType x10 = x0 + x1*freq;
306
307 auto [pv0, pv1] = p10_.at(name);
308 RealType p10 = pv0 + pv1*freq;
309
310 const RealType eaBohr_to_Debye = 2.5417464;
311 RealType muMag = muPrime * x10 * eaBohr_to_Debye;
312
313 RealType tdcDist = 0.5 * rOH_len;
314 auto tdcIt = tdcLoc_.find(name);
315 if (tdcIt != tdcLoc_.end()) tdcDist = tdcIt->second;
316 Vector3d dipolePos = Opos + tdcDist * ohUnit;
317
318 auto [apar, aperp] = alphaMap_.at(name);
319 RealType x10_gas = x10_.at(name).first;
320
321 fd.N++;
322 fd.mu.push_back(muMag * ohUnit);
323 fd.alpha.push_back(bondPolarizability(ohUnit, apar, aperp,
324 x10, x10_gas));
325
326 // Orientation diagnostic: record mu_z by frequency class
327 {
328 double muz = (muMag * ohUnit)[privilegedAxis_];
329 if (freq > 3650.0) { diagMuzFree_ += muz; diagNFree_++; }
330 else if (freq < 3450.0) { diagMuzHB_ += muz; diagNHB_++; }
331 }
332
333 ohPos.push_back(dipolePos);
334 molIndex.push_back(molID);
335 freqs.push_back(freq);
336 x10vals.push_back(x10);
337 p10vals.push_back(p10);
338 mxvals.push_back(muPrime * x10);
339
340 auto [k0, k1, kp] = wintra_.at(name);
341 intraJ.push_back(k0 + k1*E);
342 intraJ_k0.push_back(k0);
343 intraJ_kp.push_back(kp);
344
345 MolData& md = molMap[molID];
346 md.Opos = Opos;
347 md.modelName = mol->getType();
348 if (md.ohCount == 0) {
349 md.ohUnit1 = ohUnit; md.Efield1 = sE; md.rOH1 = rOH_len;
350 } else if (md.ohCount == 1) {
351 md.ohUnit2 = ohUnit; md.Efield2 = sE; md.rOH2 = rOH_len;
352 }
353 md.ohCount++;
354 }
355 }
356
357 int nStretch = fd.N;
358 fd.nStretch = nStretch;
359
360 // Bend overtone chromophores
361 int nBend = 0;
362 if (fc_ != 0.0) {
363 for (auto& [molID, md] : molMap) {
364 if (md.ohCount != 2) continue;
365 auto bIt = wb01_.find(md.modelName);
366 if (bIt == wb01_.end()) continue;
367 auto cIt = wb12_.find(md.modelName);
368 if (cIt == wb12_.end()) continue;
369
370 Vector3d vp = cross(md.ohUnit1, md.ohUnit2);
371 RealType vpLen = vp.length();
372 if (vpLen < 1.0e-9) continue;
373 vp /= vpLen;
374
375 Vector3d ePerp1 = cross(vp, md.ohUnit1);
376 Vector3d ePerp2 = cross(md.ohUnit2, vp);
377
378 RealType Eb = dot(ePerp1, md.Efield1) / md.rOH1
379 + dot(ePerp2, md.Efield2) / md.rOH2;
380
381 auto [b0_10, b1_10] = bIt->second;
382 auto [b0_21, b1_21] = cIt->second;
383 RealType w_2d = (b0_10 + b1_10*Eb) + (b0_21 + b1_21*Eb);
384
385 fd.N++;
386 fd.mu.push_back(V3Zero);
387 fd.alpha.push_back(Mat3x3d(0.0));
388
389 ohPos.push_back(md.Opos);
390 molIndex.push_back(molID);
391 freqs.push_back(w_2d);
392 x10vals.push_back(0.0);
393 p10vals.push_back(0.0);
394 mxvals.push_back(0.0);
395 intraJ.push_back(0.0);
396 intraJ_k0.push_back(0.0);
397 intraJ_kp.push_back(0.0);
398
399 nBend++;
400 }
401 }
402 fd.nBend = nBend;
403 useFermi_ = (nBend > 0);
404
405 int N = fd.N;
406 fd.H = DynamicRectMatrix<RealType>(N, N, 0.0);
407 for (int i = 0; i < N; ++i)
408 fd.H(i, i) = freqs[i];
409
410 // Pack 6 per-site entries for buildHamiltonian
411 {
412 std::vector<RealType> packed;
413 packed.reserve(6 * N);
414 for (int i = 0; i < N; ++i) {
415 packed.push_back(intraJ[i]);
416 packed.push_back(x10vals[i]);
417 packed.push_back(p10vals[i]);
418 packed.push_back(intraJ_k0[i]);
419 packed.push_back(intraJ_kp[i]);
420 packed.push_back(mxvals[i]);
421 }
422 intraJ = std::move(packed);
423 }
424
425 return fd;
426 }
427
428 // =========================================================================
429 // buildHamiltonian: fill off-diagonals. (Duplicated from dynamical SFG.)
430 // =========================================================================
431 void SFGTimeAvg::buildHamiltonian(FrameData& fd,
432 const std::vector<Vector3d>& ohPos,
433 const std::vector<int>& molIndex,
434 const std::vector<RealType>& intraJ) {
435 int N = fd.N;
436 if (N == 0) return;
437 const int nStr = fd.nStretch;
438
439 for (int i = 0; i < N; ++i) {
440 bool i_is_bend = (i >= nStr);
441 for (int j = i+1; j < N; ++j) {
442 bool j_is_bend = (j >= nStr);
443 RealType J = 0.0;
444 if (molIndex[i] == molIndex[j]) {
445 if (!i_is_bend && !j_is_bend) {
446 RealType Ki = intraJ[6*i + 0];
447 RealType xi = intraJ[6*i + 1];
448 RealType pi = intraJ[6*i + 2];
449 RealType k0_val = intraJ[6*i + 3];
450 RealType kp_val = intraJ[6*i + 4];
451 RealType Kj = intraJ[6*j + 0];
452 RealType xj = intraJ[6*j + 1];
453 RealType pj = intraJ[6*j + 2];
454 J = (Ki + Kj - k0_val) * xi * xj + kp_val * pi * pj;
455 } else if (i_is_bend != j_is_bend) {
456 J = fc_; // Fermi coupling stretch <-> bend overtone
457 }
458 } else {
459 Vector3d r_ij = ohPos[j] - ohPos[i];
460 currentSnapshot_->wrapVector(r_ij);
461 Vector3d e_i = fd.mu[i]; RealType m_i = e_i.length();
462 if (m_i > 1.0e-12) e_i /= m_i;
463 Vector3d e_j = fd.mu[j]; RealType m_j = e_j.length();
464 if (m_j > 1.0e-12) e_j /= m_j;
465 RealType mx_i = intraJ[6*i + 5];
466 RealType mx_j = intraJ[6*j + 5];
467 J = tdcCoupling(r_ij, e_i, e_j, mx_i, mx_j);
468 }
469 fd.H(i, j) = J;
470 fd.H(j, i) = J;
471 }
472 }
473 }
474
475 // =========================================================================
476 // diagonalize: real symmetric eigensolve. evecs row-major: evecs[i*N+a].
477 // =========================================================================
478 void SFGTimeAvg::diagonalize(const DynamicRectMatrix<RealType>& H, int N,
479 std::vector<double>& evals,
480 std::vector<double>& evecs) {
481 evals.assign(N, 0.0);
482 evecs.assign(N*N, 0.0);
483
484#if defined(HAVE_LAPACK)
485 std::vector<double> A(N*N);
486 {
487 const RealType* src = H.getArrayPointer();
488 for (int i = 0; i < N*N; ++i) A[i] = static_cast<double>(src[i]);
489 }
490 // Workspace query
491 int lwork = -1, liwork = -1, info = 0;
492 double wq = 0.0; int wiq = 0;
493 dsyevd_("V", "U", &N, A.data(), &N, evals.data(),
494 &wq, &lwork, &wiq, &liwork, &info);
495 lwork = static_cast<int>(wq);
496 liwork = wiq;
497 std::vector<double> work(std::max(1, lwork));
498 std::vector<int> iwork(std::max(1, liwork));
499 info = 0;
500 dsyevd_("V", "U", &N, A.data(), &N, evals.data(),
501 work.data(), &lwork, iwork.data(), &liwork, &info);
502 if (info == 0) {
503 // A holds eigenvectors column-major: A[i + a*N] = component i of state a.
504 // Store row-major: evecs[i*N + a].
505 for (int i = 0; i < N; ++i)
506 for (int a = 0; a < N; ++a)
507 evecs[i*N + a] = A[i + a*N];
508 return;
509 }
510 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
511 "SFGTimeAvg::diagonalize: dsyevd info=%d; using JAMA.\n", info);
512 painCave.isFatal = 0; painCave.severity = OPENMD_WARNING; simError();
513#endif
514
515 // JAMA fallback
519 eig.getRealEigenvalues(ev);
520 eig.getV(V);
521 for (int a = 0; a < N; ++a) {
522 evals[a] = static_cast<double>(ev[a]);
523 for (int i = 0; i < N; ++i)
524 evecs[i*N + a] = static_cast<double>(V(i, a));
525 }
526 }
527
528 // =========================================================================
529 // accumulateFrame: diagonalize H, project transition moments onto
530 // eigenstates, and add each eigenstate's complex Lorentzian to the spectrum.
531 // =========================================================================
532 void SFGTimeAvg::accumulateFrame(const FrameData& fd) {
533 int N = fd.N;
534 std::vector<double> evals, evecs;
535 diagonalize(fd.H, N, evals, evecs);
536
537 const double dOmega = (maxFreq_ - minFreq_) / static_cast<double>(nBins_);
538 // Only evaluate Lorentzian within +/- window bins of each eigenfrequency
539 const double cutoff = 20.0 * gamma_;
540 const int halfWin = static_cast<int>(std::ceil(cutoff / dOmega));
541
542 for (int a = 0; a < N; ++a) {
543 double wa = evals[a];
544 if (wa < minFreq_ - cutoff || wa > maxFreq_ + cutoff) continue;
545
546 // Eigenstate transition dipole and polarizability components.
547 // mu_r^a = sum_i V(i,a) mu_{r,i}; alpha_pq^a = sum_i V(i,a) alpha_{pq,i}
548 double mu_n = 0.0; // dipole along privileged (normal) axis
549 double a_s1s1 = 0.0, a_s2s2 = 0.0, a_nn = 0.0;
550 double a_s1n = 0.0, a_s2n = 0.0;
551 for (int i = 0; i < N; ++i) {
552 double v = evecs[i*N + a];
553 if (v == 0.0) continue;
554 const Vector3d& mu = fd.mu[i];
555 const Mat3x3d& al = fd.alpha[i];
556 mu_n += v * mu[privilegedAxis_];
557 a_s1s1 += v * al(s1_, s1_);
558 a_s2s2 += v * al(s2_, s2_);
559 a_nn += v * al(privilegedAxis_, privilegedAxis_);
560 a_s1n += v * al(s1_, privilegedAxis_);
561 a_s2n += v * al(s2_, privilegedAxis_);
562 }
563
564 // Polarization combination amplitude A_a = alpha_pq^a * mu_r^a
565 double amp = 0.0;
566 if (polarization_ == "ssp") {
567 // SSP: 0.5 (alpha_s1s1 + alpha_s2s2) mu_n
568 amp = 0.5 * (a_s1s1 + a_s2s2) * mu_n;
569 } else if (polarization_ == "ppp") {
570 amp = (-0.5*a_s1s1 - 0.5*a_s2s2 + a_nn) * mu_n;
571 } else if (polarization_ == "sps") {
572 // SPS: alpha_s1n mu_s1 component; use s1-normal cross term
573 amp = 0.5 * (a_s1n + a_s2n) * mu_n;
574 } else {
575 amp = 0.5 * (a_s1s1 + a_s2s2) * mu_n; // default ssp
576 }
577
578 // Add complex Lorentzian to nearby bins.
579 // The SFG response carries a leading factor of i:
580 // chi2(w) ∝ i ∫ dt e^{-iwt} <...>,
581 // so the absorptive (imaginary) part tracks +A_a. A bare
582 // Lorentzian A/(w - w_a + i*gamma) has Im = -A*gamma/(...),
583 // i.e. the wrong sign; we therefore use the conjugate denominator
584 // A/(w - w_a - i*gamma), whose imaginary part is +A*gamma/(...).
585 int centerBin = static_cast<int>((wa - minFreq_) / dOmega);
586 int lo = std::max(0, centerBin - halfWin);
587 int hi = std::min(static_cast<int>(nBins_) - 1, centerBin + halfWin);
588 for (int b = lo; b <= hi; ++b) {
589 double w = minFreq_ + (b + 0.5) * dOmega;
590 std::complex<double> denom(w - wa, -gamma_);
591 chi2_[b] += amp / denom;
592 }
593 }
594 }
595
596 // =========================================================================
597 // writeSpectrum: normalize, apply Bose-Einstein quantum correction, write.
598 // =========================================================================
599 void SFGTimeAvg::writeSpectrum() {
600 const double dOmega = (maxFreq_ - minFreq_) / static_cast<double>(nBins_);
601
602 // Temperature for quantum correction
603 RealType T = 300.0;
604 if (info_->getSimParams()->haveTargetTemp())
605 T = info_->getSimParams()->getTargetTemp();
606 const double kT_invcm = 0.6950356 * static_cast<double>(T);
607
608 double invN = (nProcessed_ > 0)
609 ? 1.0 / static_cast<double>(nProcessed_) : 1.0;
610
611 std::ofstream ofs(outputFilename_.c_str());
612 if (!ofs.is_open()) {
613 snprintf(painCave.errMsg, MAX_SIM_ERROR_MSG_LENGTH,
614 "SFGTimeAvg: cannot open %s\n", outputFilename_.c_str());
615 painCave.isFatal = 1; simError();
616 }
617
618 ofs << "# SFG spectrum (time-averaging approximation, "
619 << polarization_ << ")\n";
620 ofs << "# selection: (" << selectionScript1_ << ")\n";
621 ofs << "# nFrames = " << nProcessed_ << "\n";
622 ofs << "# T = " << T << " K, kT = " << kT_invcm << " cm-1\n";
623 ofs << "# Lorentzian half-width gamma = " << gamma_ << " cm-1\n";
624 ofs << "# Fermi coupling fc = " << fc_ << " cm-1"
625 << (useFermi_ ? " (bend overtone included)" : "") << "\n";
626 ofs << "#omega(cm-1)\tRe(chi2)\tIm(chi2)\t|chi2|^2\n";
627
628 for (int b = 0; b < static_cast<int>(nBins_); ++b) {
629 double w = minFreq_ + (b + 0.5) * dOmega;
630 std::complex<double> c = chi2_[b] * invN;
631
632 double x = w / kT_invcm;
633 double qCorr = (x > 1.0e-6)
634 ? ((x < 50.0) ? x / (1.0 - std::exp(-x)) : x) : 1.0;
635 double re = c.real() * qCorr;
636 double im = c.imag() * qCorr;
637
638 ofs << w << "\t" << re << "\t" << im << "\t"
639 << re*re + im*im << "\n";
640 }
641 ofs.close();
642 }
643
644 // =========================================================================
645 // bondPolarizability: transition polarizability tensor for one OH bond,
646 // scaled by x10/x10_gas (MultiSpec trPol convention).
647 // =========================================================================
648 Mat3x3d SFGTimeAvg::bondPolarizability(const Vector3d& ohUnit,
649 RealType alpha_par, RealType alpha_perp,
650 RealType x10, RealType x10_gas) {
651 RealType scale = (x10_gas > 0.0) ? x10 / x10_gas : 1.0;
652 RealType da = alpha_par - alpha_perp;
653 Mat3x3d a(0.0);
654 for (int p = 0; p < 3; ++p) {
655 a(p, p) = (alpha_perp + da * ohUnit[p] * ohUnit[p]) * scale;
656 for (int q = p+1; q < 3; ++q) {
657 a(p, q) = da * ohUnit[p] * ohUnit[q] * scale;
658 a(q, p) = a(p, q);
659 }
660 }
661 return a;
662 }
663
664 // =========================================================================
665 // tdcCoupling: transition-dipole coupling in MultiSpec convention.
666 // J [cm-1] = AU_TO_WN * geom * (mu'.x10)_i * (mu'.x10)_j / r_bohr^3
667 // =========================================================================
668 RealType SFGTimeAvg::tdcCoupling(const Vector3d& r_ij,
669 const Vector3d& e_i, const Vector3d& e_j,
670 RealType mx_i, RealType mx_j) {
671 RealType r_A = r_ij.length();
672 if (r_A < 1.0e-6) return 0.0;
673 Vector3d rhat = r_ij / r_A;
674 const RealType A_to_bohr = 1.0 / 0.52917721067;
675 RealType r_bohr = r_A * A_to_bohr;
676 RealType r3 = r_bohr * r_bohr * r_bohr;
677 const RealType AU_TO_WN = 219474.6313705;
678 RealType geom = dot(e_i, e_j) - 3.0 * dot(e_i, rhat) * dot(e_j, rhat);
679 return AU_TO_WN * geom * mx_i * mx_j / r3;
680 }
681
682} // 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)