Loading...
Searching...
No Matches
LAMMPSPot.cpp
Go to the documentation of this file.
1/*
2** This file is part of eOn.
3**
4** SPDX-License-Identifier: BSD-3-Clause
5**
6** Copyright (c) 2010--present, eOn Development Team
7** All rights reserved.
8**
9** Repo:
10** https://github.com/TheochemUI/eOn
11*/
13#include "eon/EonLogger.h"
14#include "eon/fpe_handler.h"
16
17#include <cmath>
18#include <cstring>
19#include <filesystem>
20#include <format>
21#include <fstream>
22#include <map>
23#include <string>
24
25#if !defined(EONMPI) && !defined(IS_WINDOWS)
26#include <cerrno>
27#include <cstdlib>
28#include <vector>
29
30#include <csignal>
31#include <poll.h>
32#include <sys/wait.h>
33#include <unistd.h>
34#endif
35
36#ifdef EONMPI
37#define LAMMPS_LIB_MPI
38#endif
39
41 : Potential(p),
42 lammpsThr{p.potential_options.LAMMPSThreads}
43#ifdef EONMPI
44 ,
45 mpiComm{p.potential_options.MPIClientComm}
46#endif
47{
48 // Fail fast if LAMMPS library not available
50#if !defined(EONMPI) && !defined(IS_WINDOWS)
51 // Fork the worker NOW, at construction, before this process ever opens a
52 // LAMMPS instance (and thus before liblammps initialises MPI). Open MPI
53 // does not support using MPI in a process that called MPI_Init before fork,
54 // so the worker must be spawned from a still-MPI-clean parent. Every
55 // LAMMPSPot -- endpoints and per-image alike -- runs its LAMMPS in its own
56 // child process, so the parent never initialises MPI at all.
58#endif
59}
60
62
64#if !defined(EONMPI) && !defined(IS_WINDOWS)
65 stopWorker();
66#endif
67 if (LAMMPSObj != nullptr) {
69 LAMMPSObj = nullptr;
70 }
71}
72
73#if !defined(EONMPI) && !defined(IS_WINDOWS)
74// ---------------------------------------------------------------------------
75// Process-per-image worker plumbing (POSIX only)
76// ---------------------------------------------------------------------------
77namespace {
78// Report a geometry the worker could not evaluate as an impassable wall.
79//
80// Every failure path here used to throw, and nothing between the potential
81// call and main catches it, so the client terminated. That loses the whole
82// job, including searches that had already converged: a copper V/SIA search
83// reached a saddle at 0.082 eV with a force of 0.027 eV/A and a curvature of
84// -0.47, then died on the next evaluation before the result was written.
85//
86// A large finite energy with zeroed forces reads to the optimiser as a wall,
87// so it backs out of the step and the search abandons this configuration and
88// carries on. Callers stop the worker first; ensureWorker respawns it on the
89// next evaluation.
90void rejectGeometry(double *U, double *F, long N) {
91 *U = 1.0e6;
92 // Zero forces would be read as convergence: an optimiser judges a point
93 // converged on force magnitude alone, so a rejected geometry with no force
94 // is accepted as a minimum and the wall energy is recorded as that
95 // minimum's energy. It then reaches the barrier as E_saddle - 1e6, which
96 // eOn reports as a negative barrier and discards -- a real 0.062 eV copper
97 // saddle was lost exactly this way. Return a force far above any
98 // convergence threshold so the point can never be mistaken for a
99 // stationary one, alternating its sign so the frame gains no net force.
100 for (long i = 0; i < 3 * N; ++i) {
101 F[i] = 0.0;
102 }
103 for (long i = 0; i < N; ++i) {
104 F[3 * i] = (i % 2 == 0) ? 1.0 : -1.0;
105 }
106}
107
108// Blocking read/write of exactly n bytes over a pipe. Returns false on EOF or
109// error, so a dead peer is detected rather than silently producing garbage.
110bool readExact(int fd, void *buf, size_t n) {
111 auto *p = static_cast<char *>(buf);
112 while (n > 0) {
113 ssize_t r = read(fd, p, n);
114 if (r <= 0) {
115 if (r < 0 && errno == EINTR)
116 continue;
117 return false;
118 }
119 p += r;
120 n -= static_cast<size_t>(r);
121 }
122 return true;
123}
124bool writeExact(int fd, const void *buf, size_t n) {
125 const auto *p = static_cast<const char *>(buf);
126 while (n > 0) {
127 ssize_t w = write(fd, p, n);
128 if (w < 0) {
129 if (errno == EINTR)
130 continue;
131 return false;
132 }
133 p += w;
134 n -= static_cast<size_t>(w);
135 }
136 return true;
137}
138} // namespace
139
141 if (workerSpawned)
142 return;
143
144 int reqPipe[2]; // parent -> child
145 int resPipe[2]; // child -> parent
146 if (pipe(reqPipe) != 0 || pipe(resPipe) != 0) {
147 throw std::runtime_error("LAMMPSPot: failed to create worker pipes");
148 }
149
150 // Fork BEFORE opening any LAMMPS instance in this process, so MPI is first
151 // initialised inside the child. Each child is its own process with its own
152 // MPI_COMM_WORLD; concurrent children never share a communicator.
153 pid_t pid = fork();
154 if (pid < 0) {
155 throw std::runtime_error("LAMMPSPot: fork for worker failed");
156 }
157
158 if (pid == 0) {
159 // Child: keep reqPipe read end and resPipe write end.
160 close(reqPipe[1]);
161 close(resPipe[0]);
162 reqFd = reqPipe[0];
163 resFd = resPipe[1];
164 // Client main arms feenableexcept unconditionally. The worker inherits
165 // that mask; LAMMPS EAM (PairEAM::compute) performs IEEE divisions that
166 // can raise FE_DIVBYZERO on near-coincident pairs during saddle / product
167 // minimisations. Under trapping that SIGFPEs, and the continue handler
168 // without MXCSR masking re-storms forever in the child (GB of identical
169 // "FPE (continuing)" lines). External pot code expects soft IEEE defaults;
170 // demote trapping for the whole worker process before any LAMMPS call.
172 runWorkerLoop(); // never returns
173 }
174
175 // Parent: keep reqPipe write end and resPipe read end.
176 //
177 // Writing to a worker that has already exited raises SIGPIPE, whose default
178 // action kills the client outright -- before writeExact can return the
179 // error the caller is written to handle. A search that had converged on a
180 // saddle at 0.062 eV died this way with signal 13 as its endpoints were
181 // about to be minimised. Ignoring it turns the same condition into an
182 // EPIPE return, which reaches the geometry-rejection path and respawns.
183 std::signal(SIGPIPE, SIG_IGN);
184 close(reqPipe[0]);
185 close(resPipe[1]);
186 reqFd = reqPipe[1];
187 resFd = resPipe[0];
188 workerPid = pid;
189 workerSpawned = true;
190}
191
193 // Running in the forked child. Evaluate forces with an in-process LAMMPS
194 // (this child's own MPI_COMM_WORLD) and stream results back to the parent.
195 for (;;) {
196 long N = 0;
197 if (!readExact(reqFd, &N, sizeof(N))) {
198 _exit(0); // request pipe closed -> shut down cleanly
199 }
200 if (N < 0) {
201 _exit(0); // explicit shutdown sentinel from stopWorker()
202 }
203 std::vector<int> atomicNrs(static_cast<size_t>(N));
204 std::vector<double> R(static_cast<size_t>(3 * N));
205 double box[9];
206 if (!readExact(reqFd, atomicNrs.data(),
207 sizeof(int) * static_cast<size_t>(N)) ||
208 !readExact(reqFd, box, sizeof(box)) ||
209 !readExact(reqFd, R.data(),
210 sizeof(double) * static_cast<size_t>(3 * N))) {
211 _exit(1);
212 }
213
214 std::vector<double> F(static_cast<size_t>(3 * N), 0.0);
215 double U = 0.0;
216 int status = 0;
217 try {
218 forceLocal(N, R.data(), atomicNrs.data(), F.data(), &U, box);
219 } catch (...) {
220 status = 1;
221 }
222
223 if (!writeExact(resFd, &status, sizeof(status)) ||
224 !writeExact(resFd, &U, sizeof(U)) ||
225 !writeExact(resFd, F.data(),
226 sizeof(double) * static_cast<size_t>(3 * N))) {
227 _exit(1);
228 }
229 }
230}
231
233 if (!workerSpawned)
234 return;
235 if (reqFd >= 0) {
236 // Send an explicit shutdown sentinel, then close. A sentinel (rather than
237 // relying on pipe EOF) guarantees the child exits even when sibling worker
238 // processes hold an inherited copy of this write end.
239 long sentinel = -1;
240 writeExact(reqFd, &sentinel, sizeof(sentinel));
241 close(reqFd);
242 reqFd = -1;
243 }
244 if (resFd >= 0) {
245 close(resFd);
246 resFd = -1;
247 }
248 if (workerPid > 0) {
249 // A wedged worker never acts on the sentinel or the closed pipe, and an
250 // unconditional wait then blocks the client forever at no CPU cost -- the
251 // hang this teardown exists to avoid. Give the child a brief chance to
252 // exit on its own, then insist.
253 int st = 0;
254 bool reaped = false;
255 for (int i = 0; i < 100; ++i) { // up to ~1 s
256 pid_t r = waitpid(workerPid, &st, WNOHANG);
257 if (r == workerPid || r < 0) {
258 reaped = true;
259 break;
260 }
261 usleep(10000);
262 }
263 if (!reaped) {
264 kill(workerPid, SIGKILL);
265 waitpid(workerPid, &st, 0);
266 }
267 workerPid = -1;
268 }
269 workerSpawned = false;
270}
271#endif // !EONMPI && !IS_WINDOWS
272
273void LAMMPSPot::force(long N, const double *R, const int *atomicNrs, double *F,
274 double *U, double *variance, const double *box) {
275 variance = nullptr;
276
277#ifdef EONMPI
278 forceLocal(N, R, atomicNrs, F, U, box);
279#elif defined(IS_WINDOWS)
280 // No fork/pipe on Windows; call forceLocal directly.
281 forceLocal(N, R, atomicNrs, F, U, box);
282#else
283 // Drive the dedicated worker process so this image's LAMMPS runs in its own
284 // process (own MPI_COMM_WORLD). Per-image NEB threads thus evaluate forces
285 // as truly concurrent processes with no shared-communicator contention.
286 std::lock_guard<std::mutex> workerLock(workerMutex);
287 if (workerRespawnsLeft <= 0) {
288 rejectGeometry(U, F, N);
289 return;
290 }
291 ensureWorker();
292
293 if (!writeExact(reqFd, &N, sizeof(N)) ||
294 !writeExact(reqFd, atomicNrs, sizeof(int) * static_cast<size_t>(N)) ||
295 !writeExact(reqFd, box, sizeof(double) * 9) ||
296 !writeExact(reqFd, R, sizeof(double) * static_cast<size_t>(3 * N))) {
297 // A worker stopped by an earlier rejected geometry leaves the request
298 // pipe closed, so the first send after it fails. Respawning happens on
299 // the next evaluation; reject this one rather than end the client.
301 EONC_LOG_WARNING("[LAMMPSPot] send to worker failed; {} respawns left "
302 "(eon-7416)",
304 stopWorker();
305 rejectGeometry(U, F, N);
306 return;
307 }
308
309 // eon-7416: a worker stuck on a pathological geometry (LAMMPS spinning, or
310 // a NaN it never returns) would make the blocking read below hang until the
311 // akmc pass times out. Bound the wait: if the worker is silent past a
312 // generous per-eval deadline, kill and reap it (a stuck worker never reaches
313 // EOF, so a plain waitpid would block too) and fail this evaluation so the
314 // search discards the geometry; ensureWorker respawns on the next call.
315 {
316 struct pollfd pfd;
317 pfd.fd = resFd;
318 pfd.events = POLLIN;
319 pfd.revents = 0;
320 int pr = poll(&pfd, 1, 90000); // 90 s: orders beyond a normal force eval
321 if (pr <= 0) {
322 if (workerPid > 0) {
323 kill(workerPid, SIGKILL);
324 }
327 "[LAMMPSPot] worker force eval timed out; {} respawns left "
328 "(eon-7416)",
330 stopWorker();
331 rejectGeometry(U, F, N);
332 return;
333 }
334 }
335 int status = 0;
336 if (!readExact(resFd, &status, sizeof(status)) ||
337 !readExact(resFd, U, sizeof(double)) ||
338 !readExact(resFd, F, sizeof(double) * static_cast<size_t>(3 * N))) {
341 "[LAMMPSPot] worker died during force eval; {} respawns left "
342 "(eon-7416)",
344 stopWorker();
345 rejectGeometry(U, F, N);
346 return;
347 }
348 if (status != 0) {
351 "[LAMMPSPot] worker reported an evaluation error; {} respawns left "
352 "(eon-7416)",
354 stopWorker();
355 rejectGeometry(U, F, N);
356 return;
357 }
358 // A saddle search that never terminates silently truncates the event
359 // table: the KMC residence time is 1/sum_j k_j over the discovered
360 // mechanisms, so a dropped search removes a term and biases the clock
361 // (Pedersen and Jónsson, Math. Comput. Simul. 80, 1487 (2010),
362 // doi:10.1016/j.matcom.2009.02.010, Fig. 1; Alexander and Schuh,
363 // Modelling Simul. Mater. Sci. Eng. 24, 065014 (2016),
364 // doi:10.1088/0965-0393/24/6/065014, on catalog completeness).
365 // An over-aggressive saddle-search kick can drive atoms on top of
366 // each other; the EAM force overflows to NaN/Inf and LAMMPS returns it
367 // rather than crashing. The min-mode search then spins on non-finite
368 // gradients until the akmc pass times out (0 processes). Reject the
369 // evaluation so the search discards that displacement and continues.
370 // Reject the geometry, not the process. Nothing between this call and
371 // main catches a throw here, so the client terminates: a search that was
372 // making progress is lost, and every other search sharing the pass goes
373 // with it. A large finite energy with zeroed forces reads to the
374 // optimiser as an impassable wall, so it backs out of the step and the
375 // search abandons this configuration and carries on.
376 bool nonfinite = !std::isfinite(*U);
377 for (long i = 0; i < 3 * N && !nonfinite; ++i) {
378 nonfinite = !std::isfinite(F[i]);
379 }
380 if (nonfinite) {
381 EONC_LOG_WARNING("[LAMMPSPot] non-finite force or energy; rejecting "
382 "geometry (eon-7416)");
383 rejectGeometry(U, F, N);
384 }
385#endif
386}
387
388void LAMMPSPot::forceLocal(long N, const double *R, const int *atomicNrs,
389 double *F, double *U, const double *box) {
390 // Same contract as ASE / Metatomic: external pot libraries are not written
391 // for FE traps. Cover in-process (EONMPI / Windows) and any path that still
392 // has trapping armed when forceLocal runs. Always restore so FE traps do not
393 // stay demoted for the rest of the process after the first force call.
394 eonc::FPEHandler fpeh;
395 fpeh.eat_fpe();
396 try {
397 auto &lmp = eonc::LammpsLoader::instance();
398
399 bool newLammps = false;
400 for (int i = 0; i < 9; i++) {
401 if (oldBox[i] != box[i])
402 newLammps = true;
403 }
404 if (numberOfAtoms != N)
405 newLammps = true;
406 if (newLammps) {
407 makeNewLAMMPS(N, R, atomicNrs, box);
408 }
409 if (!LAMMPSObj) {
410 throw std::runtime_error("Should have a LAMMPS instance by now");
411 }
412
413 lmp.scatter_atoms(LAMMPSObj, "x", 1, 3, const_cast<double *>(R));
414 lmp.command(LAMMPSObj, "run 1 pre no post no");
415
416 auto *pe =
417 static_cast<double *>(lmp.extract_variable(LAMMPSObj, "pe", nullptr));
418 *U = *pe;
419 free(pe);
420
421 auto *fx =
422 static_cast<double *>(lmp.extract_variable(LAMMPSObj, "fx", "all"));
423 auto *fy =
424 static_cast<double *>(lmp.extract_variable(LAMMPSObj, "fy", "all"));
425 auto *fz =
426 static_cast<double *>(lmp.extract_variable(LAMMPSObj, "fz", "all"));
427
428 for (long i = 0; i < N; i++) {
429 F[3 * i + 0] = fx[i];
430 F[3 * i + 1] = fy[i];
431 F[3 * i + 2] = fz[i];
432 }
433
434 // Convert kCal/mol -> eV if LAMMPS is using real units
435 if (realunits) {
436 constexpr double kcalPerEv = 23.0609;
437 *U /= kcalPerEv;
438 for (long i = 0; i < 3 * N; i++) {
439 F[i] /= kcalPerEv;
440 }
441 }
442
443 free(fx);
444 free(fy);
445 free(fz);
446 } catch (...) {
447 fpeh.restore_fpe();
448 throw;
449 }
450 fpeh.restore_fpe();
451}
452
453void LAMMPSPot::makeNewLAMMPS(long N, const double *R, const int *atomicNrs,
454 const double *box) {
455 auto &lmp = eonc::LammpsLoader::instance();
456
457 numberOfAtoms = N;
458 std::memcpy(oldBox, box, 9 * sizeof(double));
459
460 if (LAMMPSObj != nullptr) {
462 LAMMPSObj = nullptr;
463 }
464
465 // Map atomic numbers to LAMMPS type indices (1-based)
466 std::map<int, int> type_map;
467 int ntypes = 0;
468 for (long i = 0; i < N; i++) {
469 if (type_map.count(atomicNrs[i]) == 0) {
470 type_map.insert({atomicNrs[i], ++ntypes});
471 }
472 }
473
474#ifdef EONMPI
475 const char *lmpargv[] = {"liblammps", "-log", "none", "-echo", "log",
476 "-screen", "none", "-suffix", "omp"};
477 int lmpargc = sizeof(lmpargv) / sizeof(const char *);
478 if (!lmp.open_mpi) {
479 throw std::runtime_error(
480 "LAMMPS library found but lacks MPI support (lammps_open not found).\n"
481 "Install an MPI-enabled LAMMPS build.");
482 }
483 MPI_Comm inst_comm = MPI_COMM_NULL;
484 MPI_Comm_dup(mpiComm, &inst_comm); // private comm per per-image instance
485 LAMMPSObj =
486 lmp.open_mpi(lmpargc, const_cast<char **>(lmpargv), inst_comm, nullptr);
487#else
488 const char *lmpargv[] = {"liblammps", "-log", "none", "-echo",
489 "log", "-screen", "none"};
490 int lmpargc = sizeof(lmpargv) / sizeof(const char *);
491 LAMMPSObj = lmp.open_no_mpi(lmpargc, const_cast<char **>(lmpargv), nullptr);
492#endif
493
494 if (lammpsThr > 0) {
495 std::string cmd = std::format("package omp {} force/neigh", lammpsThr);
496 lmp.command(LAMMPSObj, cmd.c_str());
497 }
498
499 // Detect units from in.lammps: look for "#!units real" marker
500 realunits = false;
501 if (std::filesystem::exists("in.lammps")) {
502 std::ifstream infile("in.lammps");
503 std::string line;
504 while (std::getline(infile, line)) {
505 if (line == "#!units real") {
506 realunits = true;
507 break;
508 }
509 }
510 } else {
511 EONC_LOG_ERROR("[LAMMPS] in.lammps not found in working directory");
512 return;
513 }
514
515 if (realunits) {
516 lmp.command(LAMMPSObj, "units real");
517 } else {
518 lmp.command(LAMMPSObj, "units metal");
519 }
520
521 lmp.command(LAMMPSObj, "atom_style charge");
522 lmp.command(LAMMPSObj, "atom_modify map array sort 0 0");
523 lmp.command(LAMMPSObj, "neigh_modify delay 1");
524
525 // Define periodic cell (prism for non-orthorhombic)
526 std::string region_cmd =
527 std::format("region cell prism 0 {} 0 {} 0 {} {} {} {} units box", box[0],
528 box[4], box[8], box[3], box[6], box[7]);
529 lmp.command(LAMMPSObj, region_cmd.c_str());
530
531 std::string create_box_cmd = std::format("create_box {} cell", ntypes);
532 lmp.command(LAMMPSObj, create_box_cmd.c_str());
533
534 // Initialize atoms
535 for (long i = 0; i < N; i++) {
536 std::string atom_cmd =
537 std::format("create_atoms {} single {} {} {} units box",
538 type_map[atomicNrs[i]], 0.0, 0.0, 0.0);
539 lmp.command(LAMMPSObj, atom_cmd.c_str());
540 }
541
542 lmp.command(LAMMPSObj, "mass * 1.0");
543
544 // Load user LAMMPS input script
545 lmp.file(LAMMPSObj, "in.lammps");
546
547 // Define variables for force/energy extraction
548 lmp.command(LAMMPSObj, "variable fx atom fx");
549 lmp.command(LAMMPSObj, "variable fy atom fy");
550 lmp.command(LAMMPSObj, "variable fz atom fz");
551 lmp.command(LAMMPSObj, "variable pe equal pe");
552}
#define EONC_LOG_ERROR(...)
Definition EonLogger.h:262
#define EONC_LOG_WARNING(...)
Definition EonLogger.h:256
double oldBox[9]
Definition LAMMPSPot.h:39
int reqFd
Definition LAMMPSPot.h:72
int workerPid
Definition LAMMPSPot.h:71
void makeNewLAMMPS(long N, const double *R, const int *atomicNrs, const double *box)
LAMMPSPot(const Parameters &p)
Definition LAMMPSPot.cpp:40
int lammpsThr
Definition LAMMPSPot.h:34
bool workerSpawned
Definition LAMMPSPot.h:74
void cleanMemory()
Definition LAMMPSPot.cpp:63
int workerRespawnsLeft
Definition LAMMPSPot.h:61
bool realunits
Definition LAMMPSPot.h:43
void stopWorker()
void * LAMMPSObj
Definition LAMMPSPot.h:40
void ensureWorker()
void runWorkerLoop()
long numberOfAtoms
Definition LAMMPSPot.h:38
void force(long N, const double *R, const int *atomicNrs, double *F, double *U, double *variance, const double *box) override
void forceLocal(long N, const double *R, const int *atomicNrs, double *F, double *U, const double *box)
std::mutex workerMutex
Definition LAMMPSPot.h:70
int resFd
Definition LAMMPSPot.h:73
void require_loaded()
Load on first use, then throw if liblammps is not available.
static LammpsLoader & instance()
Thread-safe singleton accessor (Meyer's pattern). Does not dlopen.
Potential(PotType a_ptype)
Definition Potential.h:35
void disableFPE()