votca 2026-dev
Loading...
Searching...
No Matches
ipodcoupling.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2026 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20// Standard includes
21#include <filesystem>
22#include <fstream>
23#include <limits>
24#include <set>
25
26// Third party includes
27#include <boost/format.hpp>
28#include <boost/lexical_cast.hpp>
29
30// VOTCA includes
32
33// Local VOTCA includes
36#include "votca/xtp/logger.h"
40
41// Local private VOTCA includes
42#include "ipodcoupling.h"
43
44namespace votca {
45namespace xtp {
46
49
50 // job tasks -- same, established "tasks" pattern as IQM's own
51 // (ParseSpecificOptions, iqm.cc), matching its own option names
52 // deliberately, for user familiarity, plus one new task specific to
53 // this calculator's own, additional purpose (podcoupling).
54 std::string tasks_string = options.get(".tasks").as<std::string>();
55
56 // We split either on a space or a comma
57 tools::Tokenizer tokenizedTasks(tasks_string, " ,");
58 std::vector<std::string> tasks = tokenizedTasks.ToVector();
59
60 do_dft_input_ = std::find(tasks.begin(), tasks.end(), "input") != tasks.end();
61 do_dft_run_ = std::find(tasks.begin(), tasks.end(), "dft") != tasks.end();
62 do_dft_parse_ = std::find(tasks.begin(), tasks.end(), "parse") != tasks.end();
64 std::find(tasks.begin(), tasks.end(), "podcoupling") != tasks.end();
65
67 options.ifExistsReturnElseReturnDefault<bool>(".store_dft", store_dft_);
69 ".include_linkers", include_linkers_);
70
71 dftpackage_options_ = options.get(".dftpackage");
72 if (options.exists(".podcoupling")) {
73 podcoupling_options_ = options.get(".podcoupling");
75 podcoupling_options_.ifExistsReturnElseReturnDefault<Index>(
76 "levA", numberofstatesA_);
78 podcoupling_options_.ifExistsReturnElseReturnDefault<Index>(
79 "levB", numberofstatesB_);
80 }
81}
82
84 // Reuses IQM::WriteJobFile's own, exact "one job per neighbor-list
85 // pair" logic directly (iqm.cc) -- this calculator's own job
86 // generation is identical at this level: which pairs to evaluate is
87 // entirely a property of the neighbor list itself, independent of
88 // what EvalJob actually does with each pair.
89 std::cout << std::endl
90 << "... ... Writing job file " << jobfile_ << std::flush;
91 std::ofstream ofs;
92 ofs.open(jobfile_, std::ofstream::out);
93 if (!ofs.is_open()) {
94 throw std::runtime_error("\nERROR: bad file handle: " + jobfile_);
95 }
96
97 const QMNBList& nblist = top.NBList();
98
99 Index jobCount = 0;
100 if (nblist.size() == 0) {
101 std::cout << std::endl
102 << "... ... No pairs in neighbor list, skip." << std::flush;
103 return;
104 }
105
106 ofs << "<jobs>" << std::endl;
107 std::string tag = "";
108
109 for (const QMPair* pair : nblist) {
110 if (pair->getType() == QMPair::Excitoncl) {
111 continue;
112 }
113 Index id1 = pair->Seg1()->getId();
114 std::string name1 = pair->Seg1()->getType();
115 Index id2 = pair->Seg2()->getId();
116 std::string name2 = pair->Seg2()->getType();
117 Index id = jobCount;
118 tools::Property Input;
119 tools::Property& pInput = Input.add("input", "");
120 tools::Property& pSegmentA =
121 pInput.add("segment", boost::lexical_cast<std::string>(id1));
122 pSegmentA.setAttribute<std::string>("type", name1);
123 pSegmentA.setAttribute<Index>("id", id1);
124 tools::Property& pSegmentB =
125 pInput.add("segment", boost::lexical_cast<std::string>(id2));
126 pSegmentB.setAttribute<std::string>("type", name2);
127 pSegmentB.setAttribute<Index>("id", id2);
128 Job job(id, tag, Input, Job::AVAILABLE);
129 job.ToStream(ofs);
130 jobCount++;
131 }
132 ofs << "</jobs>" << std::endl;
133 ofs.close();
134 std::cout << std::endl
135 << "... ... In total " << jobCount << " jobs" << std::flush;
136 return;
137}
138
140 const Segment& seg, Index target_segment_id) const {
141 for (const Atom& atom : seg) {
142 if (atom.hasExternalBond() &&
143 atom.getExternalBondPartnerSegmentId() == target_segment_id) {
144 return atom;
145 }
146 }
147 throw std::runtime_error(
148 "IPodCoupling::FindBoundaryAtomTowardSegment: segment " +
149 std::to_string(seg.getId()) +
150 " has no atom with a real, direct external bond toward segment " +
151 std::to_string(target_segment_id) +
152 " -- FindLinkingSegments() reported a bond path that does not "
153 "actually exist at the atom level. This should not be possible in "
154 "practice -- check the underlying mapping/checkpoint data.");
155}
156
158 const Topology& top, const Segment& seg1_positioned,
159 const std::vector<const Segment*>& linkers,
160 const Segment& seg2_positioned) const {
161 std::vector<Segment> results;
162 if (linkers.empty()) {
163 return results;
164 }
165 // Reserved up front, to its own, exact final size -- genuinely
166 // necessary, not just an optimization: previous_positioned, below,
167 // holds a direct pointer into results itself, and a
168 // std::vector::push_back() that triggers a reallocation would
169 // silently invalidate it on the very next iteration.
170 results.reserve(linkers.size());
171
172 // Real, direct, one-covalent-bond-at-a-time walk (design worked
173 // through directly with the user) -- deliberately NOT any single,
174 // whole-chain shift, since each individual linker segment could,
175 // in principle, be wrapped to a genuinely different periodic image
176 // than its own neighbors in the chain; only walking hop by hop,
177 // each one short (a real bond length, far smaller than any
178 // realistic PBC box), guarantees the minimum-image convention is
179 // unambiguous at every single step.
180 const Segment* previous_positioned = &seg1_positioned;
181 for (const Segment* current_raw : linkers) {
182 const Atom& prev_boundary = FindBoundaryAtomTowardSegment(
183 *previous_positioned, current_raw->getId());
184 const Atom& curr_boundary = FindBoundaryAtomTowardSegment(
185 *current_raw, previous_positioned->getId());
186
187 // Same, real bond-length-based expected-partner-position
188 // calculation FragmentSaturator::SaturateExternalBonds already
189 // uses (fragmentsaturator.cc) -- where, physically, the current
190 // segment's own boundary atom is actually expected to be,
191 // according to the previous, already-positioned segment's own
192 // side of this same, real bond.
193 Eigen::Vector3d expected_partner_pos =
194 prev_boundary.getPos() +
197
198 // The real, direct shift to apply to the WHOLE current segment,
199 // so its own boundary atom ends up at (the closest periodic
200 // image of) expected_partner_pos -- confirmed directly, by
201 // reading BCShortestConnection's own actual implementation
202 // (orthorhombicbox.cc: r_j - r_i) before writing this, that
203 // PbShortestConnect(r_i, r_j) returns the shift FROM r_i TO r_j,
204 // so r_i here must be the atom's own, current position
205 // (curr_boundary), and r_j the position it needs to end up at
206 // (expected_partner_pos) -- not the other way around, which would
207 // silently give the exact opposite, negated shift instead.
208 Eigen::Vector3d shift =
209 top.PbShortestConnect(curr_boundary.getPos(), expected_partner_pos);
210
211 Segment current_shifted = *current_raw;
212 current_shifted.Translate(shift);
213 results.push_back(current_shifted);
214 previous_positioned = &results.back();
215 }
216
217 // Validates the very last hop too, into seg2_positioned -- if
218 // FindLinkingSegments() reported a real chain but the very last
219 // linker turns out not to actually be bonded to seg2 at all, this
220 // throws too, for the exact same reason as every other hop (see
221 // FindBoundaryAtomTowardSegment's own header comment). No shift is
222 // applied to seg2_positioned itself here -- it is already,
223 // separately, correctly positioned (typically via
224 // QMPair::Seg2PbCopy(), the existing, established mechanism for
225 // the main pair itself).
226 FindBoundaryAtomTowardSegment(*previous_positioned, seg2_positioned.getId());
227 FindBoundaryAtomTowardSegment(seg2_positioned, previous_positioned->getId());
228
229 return results;
230}
231
233 QMThread& opThread) {
235 Logger& pLog = opThread.getLogger();
236
237 std::string ipodcoupling_work_dir = "OR_FILES";
238 std::string frame_dir =
239 "frame_" + boost::lexical_cast<std::string>(top.getStep());
240
241 QMMapper mapper(pLog);
243
244 // Get the pair's own two segment ids from the job -- same,
245 // established pattern as IQM::EvalJob (iqm.cc), reused directly,
246 // including the optional "qm_geometry" attribute (defaulting to the
247 // ground state, "n", when absent).
248 tools::Property job_input = job.getInput();
249 std::vector<tools::Property*> segment_list = job_input.Select("segment");
250 Index ID_A = segment_list.front()->getAttribute<Index>("id");
251 Index ID_B = segment_list.back()->getAttribute<Index>("id");
252
253 std::string qmgeo_state_A = "n";
254 if (segment_list.front()->exists("qm_geometry")) {
255 qmgeo_state_A =
256 segment_list.front()->getAttribute<std::string>("qm_geometry");
257 }
258 std::string qmgeo_state_B = "n";
259 if (segment_list.back()->exists("qm_geometry")) {
260 qmgeo_state_B =
261 segment_list.back()->getAttribute<std::string>("qm_geometry");
262 }
263 QMState stateA(qmgeo_state_A);
264 QMState stateB(qmgeo_state_B);
265
266 const Segment& seg_A = top.getSegment(ID_A);
267 const Segment& seg_B = top.getSegment(ID_B);
268 const QMNBList& nblist = top.NBList();
269 const QMPair* pair = nblist.FindPair(&seg_A, &seg_B);
270 if (pair == nullptr) {
271 SetJobToFailed(jres, pLog,
272 "No pair " + std::to_string(ID_A) + ":" +
273 std::to_string(ID_B) + " found in the neighbor list.");
274 return jres;
275 }
276
277 // Same, established path-naming pattern as IQM::EvalJob (iqm.cc),
278 // reused directly -- but without orbFileA/orbFileB or eqm_work_dir
279 // at all, since those only ever existed to support the dimer-guess
280 // mechanism, deliberately skipped entirely here (design confirmed
281 // directly with the user: genuinely not useful for this
282 // calculator's own use case, where individual fragment/monomer
283 // calculations are not wanted or not possible at all). Uses
284 // "pairs_ipodcoupling", not IQM's own "pairs_iqm", so the two
285 // calculators' own output files never collide if both are run on
286 // the same morphology.
287 std::string pair_dir =
288 (boost::format("%1%%2%%3%%4%%5%") % "pair" % "_" % ID_A % "_" % ID_B)
289 .str();
290 std::filesystem::path arg_path;
291 std::string orbFileAB =
292 (arg_path / ipodcoupling_work_dir / "pairs_ipodcoupling" / frame_dir /
293 (boost::format("%1%%2%%3%%4%%5%") % "pair_" % ID_A % "_" % ID_B % ".orb")
294 .str())
295 .generic_string();
296 std::string package_append = "workdir_" + Identify();
297 std::string work_dir =
298 (arg_path / ipodcoupling_work_dir / package_append / frame_dir / pair_dir)
299 .generic_string();
300
301 // Real, direct, PBC-correct positioning for the two, real pair
302 // segments -- reuses QMPair::Seg2PbCopy() directly (qmpair.cc),
303 // matching the same, established, PBC-correct path IQM::EvalJob
304 // itself already uses when no linkers are involved.
305 const Segment* seg1 = pair->Seg1();
306 Segment seg2 = pair->Seg2PbCopy();
307
308 // Real, direct, always-visible companion to Md2QmEngine::map's own,
309 // new, real warning (md2qmengine.cc) -- worked through directly
310 // with the user: that one fires once, at xtp_map time, and is easy
311 // to miss (terminal output only, not persisted anywhere else at
312 // all) by the time a user is actually running ipodcoupling itself,
313 // much later, and potentially separately at all. Checked here
314 // again, directly, right at the point it actually matters most --
315 // if neither seg1 nor seg2 has any real, actual MD-level bonded
316 // connectivity at all (Atom::getBondedPartnerIds(), the same, real
317 // underlying data external-bond detection itself, further above in
318 // Md2QmEngine::map, genuinely depends on), H-saturation for this
319 // specific pair can never actually fire at all, no matter what --
320 // real, direct, dangling valences may silently reach the DFT
321 // calculation instead, with no other, direct signal of this at all
322 // otherwise.
323 bool seg1_has_any_bonds = false;
324 for (const Atom& atom : *seg1) {
325 if (atom.getBondedPartnerIds()[0] != -1) {
326 seg1_has_any_bonds = true;
327 break;
328 }
329 }
330 bool seg2_has_any_bonds = false;
331 for (const Atom& atom : seg2) {
332 if (atom.getBondedPartnerIds()[0] != -1) {
333 seg2_has_any_bonds = true;
334 break;
335 }
336 }
337 if (!seg1_has_any_bonds && !seg2_has_any_bonds) {
338 XTP_LOG(Log::error, pLog)
339 << "WARNING: neither segment " << seg1->getId() << " nor segment "
340 << seg_B.getId()
341 << " has any real, actual MD-level bond connectivity at all -- "
342 "H-saturation of cut segment boundaries cannot detect anything "
343 "to saturate at all for this pair, and will silently do nothing. "
344 "This usually means the underlying topology reader used at "
345 "xtp_map time provided no real bond data at all -- check that "
346 "directly."
347 << std::flush;
348 }
349
350 // LINKER SEGMENTS -- real, direct discovery + PBC-correct
351 // positioning, per the design worked through directly with the
352 // user (Topology::FindLinkingSegments/IPodCoupling::
353 // PositionLinkersAlongChain, both already built and confirmed
354 // compiling earlier this session). Declared here, BEFORE segments
355 // itself below, since segments will hold direct pointers into
356 // positioned_linkers -- positioned_linkers itself must genuinely
357 // outlive every use of segments for the rest of this function.
358 std::vector<Segment> positioned_linkers;
359 if (include_linkers_) {
360 std::vector<const Segment*> linkers = top.FindLinkingSegments(*seg1, seg_B);
361 if (!linkers.empty()) {
362 positioned_linkers = PositionLinkersAlongChain(top, *seg1, linkers, seg2);
363 }
364 }
365
366 std::vector<const Segment*> segments = {seg1, &seg2};
367 for (const Segment& linker : positioned_linkers) {
368 segments.push_back(&linker);
369 }
370
371 QMMolecule qmmol = mapper.map(*seg1, stateA);
372 // Real, direct mapped atom count for fragment A -- captured here
373 // directly, rather than re-derived later from seg1->size() (the
374 // MD-level segment's own atom count), since nothing guarantees
375 // these are equal: SegmentMapper::map()'s own size check only
376 // confirms the mapping file's own expected atom count matches the
377 // mapped result, not that every MD-level atom is mapped at all.
378 Index n_fragment_A_atoms = qmmol.size();
379 qmmol.AddContainer(mapper.map(seg2, stateB));
380 // Real, direct mapped atom count for fragment A + fragment B
381 // together, WITHOUT any linker atoms at all -- genuinely different
382 // from n_original_atoms below (which does include linker atoms,
383 // once mapped) -- needed to correctly bound fragment B's own atom
384 // range once linker atoms are also present, since "everything at
385 // or past fragment A's own atoms" is no longer synonymous with
386 // "fragment B" at all once linkers exist too.
387 Index n_fragment_AB_atoms = qmmol.size();
388
389 // Linker segments are mapped at the ground state ("n") always --
390 // deliberately no per-linker-segment-type state map at all (unlike
391 // IQM's own linker_names, which pairs each linker TYPE with its own
392 // QMState) -- this graph-based design needs no such per-type
393 // configuration at all, since inclusion itself is already,
394 // entirely determined by real bond connectivity alone, not
395 // segment type. The ground state is the most physically sensible
396 // default for a genuinely neutral, bridging unit not itself
397 // directly involved in the actual charge transfer.
398 //
399 // linker_atom_ids tracks every atom id (within the ORIGINAL,
400 // pre-saturation qmmol -- i.e. the same "owning atom id" space
401 // fragment_A_atoms/fragment_B_atoms are computed in, below) that
402 // belongs to any linker segment -- per direct agreement with the
403 // user, linker atoms belong to NEITHER fragment_A_atoms NOR
404 // fragment_B_atoms at all.
405 std::set<Index> linker_atom_ids;
406 for (const Segment& linker : positioned_linkers) {
407 Index linker_start_atom_id = qmmol.size();
408 qmmol.AddContainer(mapper.map(linker, QMState("n")));
409 for (Index i = linker_start_atom_id; i < qmmol.size(); i++) {
410 linker_atom_ids.insert(i);
411 }
412 }
413
414 // Which of this supermolecule's own segment ids are already
415 // present -- an external bond whose own partner segment is among
416 // these is already satisfied within the supermolecule itself (both
417 // sides are already present together), and must NOT be saturated
418 // with a new H at all -- this is exactly the check
419 // getExternalBondPartnerSegmentId() was built for, earlier this
420 // session, together with the user.
421 std::set<Index> present_segment_ids;
422 for (const Segment* seg : segments) {
423 present_segment_ids.insert(seg->getId());
424 }
425
426 // FragmentSaturator::SaturateExternalBonds() itself, unconditionally,
427 // saturates every hasExternalBond()==true atom in whatever molecule
428 // it is given -- it has no notion of "this one is already satisfied,
429 // skip it" at all. Reconciled here, on this call's own local copy
430 // only (never the underlying, persisted MD-level data -- see
431 // QMAtom::clearExternalBond()'s own header comment for exactly why
432 // this is safe, worked through directly with the user before
433 // implementing this): any atom whose own external-bond partner
434 // segment already turns out to be present in this specific
435 // supermolecule has its own external bond cleared directly, before
436 // SaturateExternalBonds() ever sees it -- at that point, the bond
437 // genuinely is no longer external to THIS supermolecule at all.
438 Index n_original_atoms = qmmol.size();
439 for (QMAtom& atom : qmmol) {
440 if (!atom.hasExternalBond()) {
441 continue;
442 }
443 if (present_segment_ids.count(atom.getExternalBondPartnerSegmentId()) > 0) {
444 atom.clearExternalBond();
445 }
446 }
447
448 FragmentSaturator::SaturationResult saturation_result =
450 QMMolecule relaxed =
451 FragmentSaturator::RelaxNewAtoms(saturation_result.mol, n_original_atoms);
452
453 // fragment_A_atoms/fragment_B_atoms for PODCoupling's own
454 // constructor -- every ORIGINAL atom's own fragment is already
455 // known directly from its own id (seg1's own atoms were mapped
456 // first, so ids 0..n_fragment_A_atoms-1 are fragment A;
457 // n_fragment_A_atoms..n_fragment_AB_atoms-1 are fragment B; any
458 // linker atoms, mapped after those two, are excluded from both
459 // entirely -- per direct agreement with the user, they belong to
460 // NEITHER fragment at all). Every new, saturating H atom must
461 // inherit the SAME classification as whichever original atom it is
462 // saturating -- not recoverable from its own final position/index
463 // alone (a real gap surfaced and worked through directly with the
464 // user before extending SaturateExternalBonds's own return type to
465 // track this explicitly, saturation_result.new_atom_parent_ids).
466 std::vector<Index> fragment_A_atoms;
467 std::vector<Index> fragment_B_atoms;
468 for (const QMAtom& atom : relaxed) {
469 Index owning_atom_id = atom.getId();
470 if (owning_atom_id >= n_original_atoms) {
471 owning_atom_id = saturation_result.new_atom_parent_ids[owning_atom_id];
472 }
473 if (linker_atom_ids.count(owning_atom_id) > 0) {
474 continue;
475 }
476 if (owning_atom_id < n_fragment_A_atoms) {
477 fragment_A_atoms.push_back(atom.getId());
478 } else if (owning_atom_id < n_fragment_AB_atoms) {
479 fragment_B_atoms.push_back(atom.getId());
480 }
481 }
482
483 Orbitals orbitalsAB;
484 orbitalsAB.QMAtoms() = relaxed;
485
487 std::string qmpackage_work_dir = work_dir;
488
489 Logger dft_logger(Log::current_level);
490 dft_logger.setMultithreading(false);
491 dft_logger.setPreface(Log::info, (boost::format("\nDFT INF ...")).str());
492 dft_logger.setPreface(Log::error, (boost::format("\nDFT ERR ...")).str());
493 dft_logger.setPreface(Log::warning, (boost::format("\nDFT WAR ...")).str());
494 dft_logger.setPreface(Log::debug, (boost::format("\nDFT DBG ...")).str());
495 std::string package = dftpackage_options_.get("name").as<std::string>();
496 std::unique_ptr<QMPackage> qmpackage = QMPackageFactory().Create(package);
497 qmpackage->setLog(&dft_logger);
498 qmpackage->setRunDir(qmpackage_work_dir);
499 qmpackage->Initialize(dftpackage_options_);
500
501 if (do_dft_input_) {
502 std::filesystem::create_directories(qmpackage_work_dir);
503 // Deliberately, always the plain "no guess, start from the DFT
504 // package's own default starting guess" path -- the dimer-guess
505 // mechanism IQM::EvalJob itself optionally uses (combining two,
506 // separately pre-computed monomer orbital files) is skipped
507 // entirely here, on purpose (design confirmed directly with the
508 // user): our own orbitalsAB is not a simple two-monomer
509 // combination at all -- it is the RELAXED, H-SATURATED
510 // supermolecule, and the new H atom(s) have no corresponding
511 // monomer orbitals to guess from in the first place. If the DFT
512 // package's own options actually requested a guess anyway, warn
513 // directly rather than silently ignoring the request.
514 if (qmpackage->GuessRequested()) {
515 XTP_LOG(Log::warning, pLog)
516 << "A DFT guess was requested in the dftpackage options, but "
517 "IPodCoupling does not support this at all (its own "
518 "supermolecule is not a simple monomer combination, given "
519 "the new, saturating H atom(s) have no corresponding "
520 "monomer orbitals to guess from) -- proceeding with the "
521 "DFT package's own default starting guess instead."
522 << std::flush;
523 }
524 qmpackage->WriteInputFile(orbitalsAB);
525 }
526
527 if (do_dft_run_) {
528 XTP_LOG(Log::error, pLog) << "Running DFT" << std::flush;
529 bool run_dft_status = qmpackage->Run();
530 if (!run_dft_status) {
531 SetJobToFailed(jres, pLog, qmpackage->getPackageName() + " run failed");
532 WriteLoggerToFile(work_dir + "/dft.log", dft_logger);
533 return jres;
534 }
535 }
536
537 if (do_dft_parse_) {
538 bool parse_log_status = qmpackage->ParseLogFile(orbitalsAB);
539 if (!parse_log_status) {
540 SetJobToFailed(jres, pLog, "LOG parsing failed");
541 return jres;
542 }
543 bool parse_orbitals_status = qmpackage->ParseMOsFile(orbitalsAB);
544 if (!parse_orbitals_status) {
545 SetJobToFailed(jres, pLog, "Orbitals parsing failed");
546 return jres;
547 }
548 }
549 qmpackage->CleanUp();
550 WriteLoggerToFile(work_dir + "/dft.log", dft_logger);
551 } else {
552 try {
553 orbitalsAB.ReadFromCpt(orbFileAB);
554 } catch (std::runtime_error&) {
555 SetJobToFailed(jres, pLog,
556 "Do input: failed loading orbitals from " + orbFileAB);
557 return jres;
558 }
559 }
560
561 if (store_dft_) {
562 std::filesystem::create_directories(
563 std::filesystem::path(orbFileAB).parent_path());
564 orbitalsAB.WriteToCpt(orbFileAB);
565 }
566
567 // Real PODCoupling calculation, on the real, converged orbitalsAB
568 // (either freshly computed above, via do_dft_parse_, or read back
569 // from a previously-stored orbFileAB) -- final piece of the
570 // six-step design worked through directly with the user at the
571 // very start of this whole calculator.
572 //
573 // Real, direct bug fix: this whole block used to run
574 // unconditionally, with no do_podcoupling_ guard at all -- caught
575 // directly by the user's own real, direct run (tasks="input" only,
576 // to inspect the DFT input file directly): PODCoupling's own
577 // constructor genuinely needs a real, converged orbitalsAB with an
578 // actual basis set name set on it, which simply does not exist yet
579 // at all when only the DFT input file has been written (do_dft_run_/
580 // do_dft_parse_ both false) -- attempting it anyway threw a real,
581 // confusing "basis_sets/.xml" error (an empty basis set name) even
582 // though podcoupling was never requested as a task at all.
583 tools::Property job_summary;
584 tools::Property& job_output = job_summary.add("output", "");
585 if (do_podcoupling_) {
586 try {
587 // Real, direct debug-level output, worked through directly with
588 // the user -- fragment_A_atoms/fragment_B_atoms (PODCoupling's
589 // own terminology for the same thing VOTCA itself calls a
590 // "segment") are otherwise entirely internal: unlike the DFT
591 // input file's own, directly-visible written coordinates, there
592 // is no other, direct way for a user to confirm which real atom
593 // indices actually ended up in which fragment at all, short of
594 // running a real, full, genuinely expensive DFT calculation
595 // first (do_podcoupling_ itself already requires a real,
596 // converged orbitalsAB with a real basis set name set on it --
597 // confirmed directly, earlier this same session, per the
598 // do_podcoupling_ guard fix). Printed at Log::debug specifically
599 // (not error/warning/info) -- confirmed directly, from
600 // application.cc, that this is exactly the level -v/--verbose2
601 // itself raises current_level to; the two milder --verbose/
602 // --verbose1 flags only ever reach warning/info, not this.
603 XTP_LOG(Log::debug, pLog)
604 << "PODCoupling: fragment_A_atoms (" << fragment_A_atoms.size()
605 << " atoms):" << std::flush;
606 for (Index a : fragment_A_atoms) {
607 XTP_LOG(Log::debug, pLog) << " " << a << std::flush;
608 }
609 XTP_LOG(Log::debug, pLog)
610 << "PODCoupling: fragment_B_atoms (" << fragment_B_atoms.size()
611 << " atoms):" << std::flush;
612 for (Index b : fragment_B_atoms) {
613 XTP_LOG(Log::debug, pLog) << " " << b << std::flush;
614 }
615 if (!linker_atom_ids.empty()) {
616 XTP_LOG(Log::debug, pLog)
617 << "PODCoupling: linker_atom_ids, excluded from both fragments ("
618 << linker_atom_ids.size() << " atoms):" << std::flush;
619 for (Index l : linker_atom_ids) {
620 XTP_LOG(Log::debug, pLog) << " " << l << std::flush;
621 }
622 }
623
624 PODCoupling pod(orbitalsAB, &pLog, fragment_A_atoms, fragment_B_atoms);
626
627 // Same, established per-pair output format as DFTcoupling's own
628 // WriteToProperty (dftcoupling.cc), confirmed directly by reading
629 // it before writing this, rather than invented separately --
630 // <coupling levelA="..." levelB="..." j="..."/> for every
631 // (levelA, levelB) pair within the requested range, matching
632 // DFTcoupling's own, backward-compatible core format exactly.
633 // Unlike DFTcoupling's own, more elaborate Addoutput (monomer
634 // energies, raw TB matrices, diagnostics), none of that is added
635 // here at all -- it genuinely does not apply to POD2 at all,
636 // which has no separate, isolated monomer calculation to compare
637 // against in the first place (the whole point of POD2, per this
638 // class's own header comment, podcoupling.h).
639 tools::Property& podcoupling_summary = job_output.add(Identify(), "");
640 Index homoA = pod.getFragmentAHomoIndex();
641 Index lumoA = pod.getFragmentALumoIndex();
642 Index homoB = pod.getFragmentBHomoIndex();
643 Index lumoB = pod.getFragmentBLumoIndex();
644 // homoA/homoB (lumoA/lumoB = homoA/homoB + 1, always -- see
645 // PODCoupling::getFragmentALumoIndex's own header comment,
646 // podcoupling.h) written directly as attributes on this same
647 // node, matching DFTcoupling::Addoutput's own, established
648 // pattern (dftcoupling.cc: dftcoupling.setAttribute("homoA",
649 // orbitalsA.getHomo())) exactly -- genuinely needed for
650 // ReadJobFile to be able to translate "the hole/electron
651 // coupling" into the actual, specific levelA/levelB pair among
652 // the (potentially many, if numberofstatesA_/B_ > 1) <coupling>
653 // elements written below.
654 podcoupling_summary.setAttribute("homoA", homoA);
655 podcoupling_summary.setAttribute("homoB", homoB);
656 for (Index levelA = homoA - numberofstatesA_ + 1;
657 levelA <= lumoA + numberofstatesA_ - 1; ++levelA) {
658 for (Index levelB = homoB - numberofstatesB_ + 1;
659 levelB <= lumoB + numberofstatesB_ - 1; ++levelB) {
660 double J_hartree = pod.getCouplingElement(levelA, levelB);
661 // Written in eV, not Hartree -- matching IQM::WriteToProperty's
662 // own, established convention exactly (confirmed directly by
663 // reading GetDFTCouplingFromProp, iqm.cc, which converts the
664 // read-back "j" value FROM eV back TO Hartree via
665 // tools::conv::ev2hrt -- meaning IQM's own "j" is written in
666 // eV, even though PODCoupling::getCouplingElement's own
667 // documented return unit, podcoupling.h, is Hartree). More
668 // human-readable too -- typical couplings are meV-scale, not
669 // the much smaller numbers raw Hartree would give.
670 double J_ev = J_hartree * tools::conv::hrt2ev;
671 tools::Property& coupling = podcoupling_summary.add("coupling", "");
672 coupling.setAttribute("levelA", levelA);
673 coupling.setAttribute("levelB", levelB);
674 coupling.setAttribute("j", (boost::format("%1$1.6e") % J_ev).str());
675 }
676 }
677 } catch (std::runtime_error& error) {
678 SetJobToFailed(jres, pLog, std::string("PODCoupling: ") + error.what());
679 return jres;
680 }
681 }
682
683 jres.setOutput(job_summary);
685 return jres;
686}
687
689 Index levelA, Index levelB) const {
690 for (const tools::Property* state : podprop.Select("coupling")) {
691 Index state1 = state->getAttribute<Index>("levelA");
692 Index state2 = state->getAttribute<Index>("levelB");
693 if (state1 == levelA && state2 == levelB) {
694 return state->getAttribute<double>("j") * tools::conv::ev2hrt;
695 }
696 }
697 return std::numeric_limits<double>::quiet_NaN();
698}
699
701 // Same, established structure as IQM::ReadJobFile (iqm.cc),
702 // confirmed directly by reading it in full before writing this,
703 // rather than guessed -- but reads back a single, simpler
704 // "podcoupling" node instead of IQM's own separate dftcoupling/
705 // bsecoupling nodes, and, unlike IQM, has no hole_levels_/
706 // electron_levels_-style, user-configurable state map at all: the
707 // HOMO-HOMO coupling is always written back as the hole coupling,
708 // and LUMO-LUMO as the electron coupling -- the most direct,
709 // standard headline result, matching PODCoupling's own
710 // getFragmentAHomoIndex()/getFragmentALumoIndex() (podcoupling.h)
711 // convention exactly (lumo = homo + 1, always).
712 QMNBList& nblist = top.NBList();
713 Index number_of_pairs = nblist.size();
714 Index updated_h = 0;
715 Index updated_e = 0;
716 Index incomplete_jobs = 0;
717 Logger log;
719
720 tools::Property xml;
722
723 for (tools::Property* job : xml.Select("jobs.job")) {
724 if (!job->exists("status")) {
725 throw std::runtime_error(
726 "Jobfile is malformed. <status> tag missing on job.");
727 }
728 if (job->get("status").as<std::string>() != "COMPLETE" ||
729 !job->exists("output")) {
730 incomplete_jobs++;
731 continue;
732 }
733
734 std::vector<Index> id;
735 for (tools::Property* segment : job->Select("input.segment")) {
736 id.push_back(segment->getAttribute<Index>("id"));
737 }
738 if (id.size() != 2) {
739 throw std::runtime_error(
740 "Getting pair ids from jobfile failed, check jobfile.");
741 }
742
743 Segment& segA = top.getSegment(id[0]);
744 Segment& segB = top.getSegment(id[1]);
745 QMPair* qmp = nblist.FindPair(&segA, &segB);
746 if (qmp == nullptr) {
747 XTP_LOG(Log::error, log)
748 << "No pair " << id[0] << ":" << id[1]
749 << " found in the neighbor list. Ignoring" << std::flush;
750 continue;
751 }
752 if (qmp->getType() != QMPair::PairType::Hopping) {
753 XTP_LOG(Log::error, log) << "WARNING Pair " << qmp->getId()
754 << " is not of any of the "
755 "Hopping type. Skipping pair"
756 << std::flush;
757 continue;
758 }
759
760 const tools::Property& pair_property = job->get("output");
761 if (!pair_property.exists(Identify())) {
762 continue;
763 }
764 const tools::Property& podprop = pair_property.get(Identify());
765 Index homoA = podprop.getAttribute<Index>("homoA");
766 Index homoB = podprop.getAttribute<Index>("homoB");
767 Index lumoA = homoA + 1;
768 Index lumoB = homoB + 1;
769
771 double J_hole = GetPODCouplingFromProp(podprop, homoA, homoB);
772 if (!std::isnan(J_hole)) {
773 qmp->setJeff(J_hole, hole);
774 qmp->setJeff2(J_hole * J_hole, hole);
775 updated_h++;
776 }
777
779 double J_electron = GetPODCouplingFromProp(podprop, lumoA, lumoB);
780 if (!std::isnan(J_electron)) {
781 qmp->setJeff(J_electron, electron);
782 qmp->setJeff2(J_electron * J_electron, electron);
783 updated_e++;
784 }
785 }
786 XTP_LOG(Log::error, log) << "Pairs [total:updated(e,h)] " << number_of_pairs
787 << ":(" << updated_e << "," << updated_h
788 << ") Incomplete jobs: " << incomplete_jobs << "\n"
789 << std::flush;
790 std::cout << log;
791}
792
794 const std::string& errormessage) {
795 XTP_LOG(Log::error, pLog) << errormessage << std::flush;
796 std::cout << pLog;
797 jres.setError(errormessage);
799}
800
801void IPodCoupling::WriteLoggerToFile(const std::string& logfile,
802 Logger& logger) {
803 std::ofstream ofs;
804 ofs.open(logfile, std::ofstream::out);
805 if (!ofs.is_open()) {
806 throw std::runtime_error("Bad file handle: " + logfile);
807 }
808 ofs << logger << std::endl;
809 ofs.close();
810}
811
812} // namespace xtp
813} // namespace votca
pair_type * FindPair(element_type e1, element_type e2)
Definition pairlist.h:93
Index size() const
Definition pairlist.h:53
virtual std::unique_ptr< T > Create(const key_t &key, args_t &&...arguments)
class to manage program options with xml serialization functionality
Definition property.h:55
Property & add(const std::string &key, const std::string &value)
add a new property to structure
Definition property.cc:108
Property & get(const std::string &key)
get existing property
Definition property.cc:79
bool exists(const std::string &key) const
check whether property exists
Definition property.cc:122
T as() const
return value as type
Definition property.h:283
T ifExistsReturnElseReturnDefault(const std::string &key, T defaultvalue) const
Definition property.h:321
T getAttribute(const std::string &attribute) const
return attribute as type
Definition property.h:308
std::vector< Property * > Select(const std::string &filter)
select property based on a filter
Definition property.cc:185
void setAttribute(const std::string &attribute, const T &value)
set an attribute
Definition property.h:314
void LoadFromXML(std::string filename)
Definition property.cc:238
break string into words
Definition tokenizer.h:72
void Translate(const Eigen::Vector3d &shift)
const Eigen::Vector3d & getPos() const
Definition atom.h:81
const Eigen::Vector3d & getExternalBondDirection() const
Definition atom.h:114
static QMMolecule RelaxNewAtoms(const QMMolecule &mol, Index n_original_atoms, Index n_steps=500)
static constexpr double kDefaultCHBondLengthAngstrom
static SaturationResult SaturateExternalBonds(const QMMolecule &mol, double bond_length_angstrom=kDefaultCHBondLengthAngstrom)
void WriteJobFile(const Topology &top)
std::string Identify() const
Calculator name.
void ParseSpecificOptions(const tools::Property &user_options)
std::vector< Segment > PositionLinkersAlongChain(const Topology &top, const Segment &seg1_positioned, const std::vector< const Segment * > &linkers, const Segment &seg2_positioned) const
void ReadJobFile(Topology &top)
tools::Property podcoupling_options_
Job::JobResult EvalJob(const Topology &top, Job &job, QMThread &opThread)
const Atom & FindBoundaryAtomTowardSegment(const Segment &seg, Index target_segment_id) const
tools::Property dftpackage_options_
void WriteLoggerToFile(const std::string &logfile, Logger &logger)
void SetJobToFailed(Job::JobResult &jres, Logger &pLog, const std::string &errormessage)
double GetPODCouplingFromProp(const tools::Property &podprop, Index levelA, Index levelB) const
void setError(std::string error)
Definition job.h:67
void setOutput(std::string output)
Definition job.h:50
void setStatus(JobStatus stat)
Definition job.h:49
tools::Property & getInput()
Definition job.h:87
Logger is used for thread-safe output of messages.
Definition logger.h:164
void setPreface(Log::Level level, const std::string &preface)
Definition logger.h:194
void setReportLevel(Log::Level ReportLevel)
Definition logger.h:185
void setMultithreading(bool maverick)
Definition logger.h:186
Container for molecular orbitals and derived one-particle data.
Definition orbitals.h:47
const QMMolecule & QMAtoms() const
Return read-only access to the molecular geometry.
Definition orbitals.h:262
void ReadFromCpt(const std::string &filename)
Read the orbital container from a checkpoint file on disk.
Definition orbitals.cc:1201
void WriteToCpt(const std::string &filename) const
Write the orbital container to a checkpoint file on disk.
Definition orbitals.cc:1105
Index getFragmentAHomoIndex() const
void CalculateCouplings(Index numberofstatesA, Index numberofstatesB)
double getCouplingElement(Index levelA, Index levelB) const
Index getFragmentBLumoIndex() const
Index getFragmentALumoIndex() const
Index getFragmentBHomoIndex() const
typename std::vector< Job >::value_type Job
container for QM atoms
Definition qmatom.h:37
void AddContainer(const AtomContainer< QMAtom > &container)
Definition qmmolecule.h:40
void setJeff2(double Jeff2, QMStateType state)
Definition qmpair.h:121
const PairType & getType() const
Definition qmpair.h:138
Index getId() const
Definition qmpair.h:95
void setJeff(double Jeff, QMStateType state)
Definition qmpair.h:118
Identifier for QMstates. Strings like S1 are converted into enum +zero indexed int.
Definition qmstate.h:135
Logger & getLogger()
Definition qmthread.h:55
void LoadMappingFile(const std::string &mapfile)
AtomContainer map(const Segment &seg, const SegId &segid) const
Container for segments and box and atoms.
Definition topology.h:41
Index getStep() const
Definition topology.h:75
Eigen::Vector3d PbShortestConnect(const Eigen::Vector3d &r1, const Eigen::Vector3d &r2) const
Definition topology.cc:134
std::vector< const Segment * > FindLinkingSegments(const Segment &seg1, const Segment &seg2) const
Definition topology.cc:176
QMNBList & NBList()
Definition topology.h:70
Segment & getSegment(Index id)
Definition topology.h:55
#define XTP_LOG(level, log)
Definition logger.h:40
const double ev2hrt
Definition constants.h:54
const double ang2bohr
Definition constants.h:48
const double hrt2ev
Definition constants.h:53
Charge transport classes.
Definition ERIs.h:28
SegmentMapper< QMMolecule > QMMapper
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
static Level current_level
Definition globals.h:30