Loading...
Searching...
No Matches
ConFileIO.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*/
12#include "eon/ConFileIO.h"
13#include "eon/Eigen.h"
14#include "eon/EonLogger.h"
15#include "eon/HelperFunctions.h"
16#include "eon/Matter.h"
17#include "eon/SafeMath.h"
18
19#include <algorithm>
20#include <atomic>
21#include <cmath>
22#include <cstdint>
23#include <cstring>
24#include <filesystem>
25#include <format>
26#include <fstream>
27#include <mutex>
28#include <numeric>
29#include <optional>
30#include <stdexcept>
31#include <string>
32#include <system_error>
33#include <unordered_map>
34#include <vector>
35
36namespace {
37
38namespace fs = std::filesystem;
39
40constexpr uint8_t kConPrecision = 17;
41constexpr uint8_t kConvelPrecision = 6;
42
43std::string strip_nl(const std::string &s) {
44 std::string str(s);
45 while (!str.empty() && (str.back() == '\n' || str.back() == '\r'))
46 str.pop_back();
47 return str;
48}
49
50std::string canonical_generator_header(const std::string &header) {
51 auto stripped = strip_nl(header);
52 if (!stripped.empty()) {
53 return stripped;
54 }
55 return "Generated by eOn";
56}
57
58std::string ensure_extension(std::string filename, std::string_view ext) {
59 fs::path path(filename);
60 const auto name = path.filename().string();
61 const bool has_compound =
62 name.size() > ext.size() && (name.ends_with(std::string(ext) + ".gz") ||
63 name.ends_with(std::string(ext) + ".zst"));
64 if (!has_compound && path.extension() != ext) {
65 path += ext;
66 }
67 return path.string();
68}
69
70std::string symbol_for_z(long atomic_nr) {
71 return readcon::z_to_symbol(static_cast<uint64_t>(atomic_nr));
72}
73
74std::vector<double> flat_row_major(const AtomMatrix &m) {
75 static_assert(AtomMatrix::IsRowMajor,
76 "flat_row_major assumes AtomMatrix RowMajor Nx3 layout");
77 const auto n = static_cast<size_t>(m.rows());
78 std::vector<double> flat(n * 3);
79 std::memcpy(flat.data(), m.data(), flat.size() * sizeof(double));
80 return flat;
81}
82
83void apply_frame_metadata(readcon::ConFrameBuilder &builder,
84 const eonc::io::ConFrameMetadata *metadata) {
85 if (metadata == nullptr) {
86 return;
87 }
88 if (metadata->raw_json && !metadata->raw_json->empty()) {
89 builder.set_metadata_json(*metadata->raw_json);
90 }
91 if (metadata->energy) {
92 builder.set_energy(*metadata->energy);
93 }
94 if (metadata->frame_index) {
95 builder.set_frame_index(*metadata->frame_index);
96 }
97 if (metadata->time) {
98 builder.set_time(*metadata->time);
99 }
100 if (metadata->timestep) {
101 builder.set_timestep(*metadata->timestep);
102 }
103 if (metadata->neb_bead) {
104 builder.set_neb_bead(*metadata->neb_bead);
105 }
106 if (metadata->neb_band) {
107 builder.set_neb_band(*metadata->neb_band);
108 }
109 if (metadata->potential_type && !metadata->potential_type->empty()) {
110 builder.set_string_metadata("potential_type", *metadata->potential_type);
111 }
112 for (const auto &[key, value] : metadata->scalars) {
113 builder.set_scalar_metadata(key, value);
114 }
115 for (const auto &[key, value] : metadata->strings) {
116 builder.set_string_metadata(key, value);
117 }
118}
119
120eonc::io::IoStatus write_frames(const fs::path &path,
121 const std::vector<readcon::ConFrame> &frames,
122 uint8_t precision) {
123 try {
124 const auto compression =
125 readcon::ConFrameWriter::compression_from_extension(path);
126 readcon::ConFrameWriter writer(path, compression, precision);
127 writer.extend(frames);
129 } catch (const std::exception &e) {
130 EONC_LOG_ERROR("Failed to write {}: {}", path.string(), e.what());
132 }
133}
134
145std::vector<size_t> matter_order(const std::vector<readcon::Atom> &atoms) {
146 const size_t n = atoms.size();
147 std::vector<size_t> order(n);
148 std::iota(order.begin(), order.end(), size_t{0});
149 if (n < 2) {
150 return order;
151 }
152 const bool ascending =
153 std::is_sorted(atoms.begin(), atoms.end(),
154 [](const readcon::Atom &a, const readcon::Atom &b) {
155 return a.atom_id < b.atom_id;
156 }) &&
157 std::adjacent_find(atoms.begin(), atoms.end(),
158 [](const readcon::Atom &a, const readcon::Atom &b) {
159 return a.atom_id == b.atom_id;
160 }) == atoms.end();
161 if (ascending) {
162 return order;
163 }
164 std::vector<uint64_t> ids;
165 ids.reserve(n);
166 for (const auto &atom : atoms) {
167 ids.push_back(atom.atom_id);
168 }
169 std::vector<uint64_t> sorted(ids);
170 std::sort(sorted.begin(), sorted.end());
171 if (std::adjacent_find(sorted.begin(), sorted.end()) != sorted.end()) {
172 return order;
173 }
174 std::stable_sort(order.begin(), order.end(),
175 [&ids](size_t a, size_t b) { return ids[a] < ids[b]; });
176 return order;
177}
178
182struct FileStamp {
183 std::uintmax_t size{0};
184 fs::file_time_type mtime{};
185 friend bool operator==(const FileStamp &, const FileStamp &) = default;
186};
187
188std::optional<FileStamp> stamp_file(const fs::path &path) {
189 std::error_code ec;
190 const auto size = fs::file_size(path, ec);
191 if (ec) {
192 return std::nullopt;
193 }
194 const auto mtime = fs::last_write_time(path, ec);
195 if (ec) {
196 return std::nullopt;
197 }
198 return FileStamp{size, mtime};
199}
200
204std::string append_key(const fs::path &path) {
205 std::error_code ec;
206 auto resolved = fs::weakly_canonical(path, ec);
207 if (ec || resolved.empty()) {
208 resolved = fs::absolute(path, ec);
209 if (ec) {
210 return path.lexically_normal().string();
211 }
212 }
213 return resolved.lexically_normal().string();
214}
215
218std::mutex &append_mutex() {
219 static std::mutex m;
220 return m;
221}
222
223std::unordered_map<std::string, FileStamp> &append_stamps() {
224 static std::unordered_map<std::string, FileStamp> stamps;
225 return stamps;
226}
227
229void remember_stamp(const std::string &key, const fs::path &path) {
230 if (auto stamp = stamp_file(path)) {
231 append_stamps()[key] = *stamp;
232 } else {
233 append_stamps().erase(key);
234 }
235}
236
239bool tail_is_ours(const std::string &key, const fs::path &path) {
240 const auto it = append_stamps().find(key);
241 if (it == append_stamps().end()) {
242 return false;
243 }
244 const auto stamp = stamp_file(path);
245 return stamp.has_value() && *stamp == it->second;
246}
247
250std::optional<long> last_xyz_natoms(const fs::path &path) {
251 std::ifstream in(path);
252 if (!in) {
253 return std::nullopt;
254 }
255 std::optional<long> last;
256 std::string line;
257 while (std::getline(in, line)) {
258 if (line.empty()) {
259 continue;
260 }
261 long n = 0;
262 try {
263 n = std::stol(line);
264 } catch (const std::exception &) {
265 return std::nullopt;
266 }
267 if (n < 0) {
268 return std::nullopt;
269 }
270 if (!std::getline(in, line)) {
271 return std::nullopt;
272 }
273 for (long i = 0; i < n; ++i) {
274 if (!std::getline(in, line)) {
275 return std::nullopt;
276 }
277 }
278 last = n;
279 }
280 return last;
281}
282
284bool ends_with_newline(const fs::path &path) {
285 std::ifstream in(path, std::ios::binary | std::ios::ate);
286 if (!in) {
287 return true;
288 }
289 const auto len = static_cast<std::streamoff>(in.tellg());
290 if (len <= 0) {
291 return true;
292 }
293 in.seekg(len - 1);
294 char last = '\n';
295 if (!in.get(last)) {
296 return true;
297 }
298 return last == '\n';
299}
300
310eonc::io::IoStatus append_frames(const fs::path &path,
311 const std::vector<readcon::ConFrame> &frames,
312 uint8_t precision) {
313 static std::atomic<uint64_t> scratch_counter{0};
314 const auto scratch =
315 path.parent_path() /
316 std::format(".{}.eon-append-{}.tmp", path.filename().string(),
317 scratch_counter.fetch_add(1, std::memory_order_relaxed));
318
319 std::string bytes;
320 try {
321 {
322 readcon::ConFrameWriter writer(
323 scratch, readcon::ConFrameWriter::Compression::None, precision);
324 writer.extend(frames);
325 }
326 std::ifstream in(scratch, std::ios::binary | std::ios::ate);
327 if (!in) {
328 throw std::runtime_error("serialized frame not readable back");
329 }
330 const auto len = static_cast<std::streamoff>(in.tellg());
331 if (len <= 0) {
332 throw std::runtime_error("serialized frame is empty");
333 }
334 bytes.resize(static_cast<size_t>(len));
335 in.seekg(0);
336 if (!in.read(bytes.data(), static_cast<std::streamsize>(len))) {
337 throw std::runtime_error("short read of serialized frame");
338 }
339 } catch (const std::exception &e) {
340 std::error_code ec;
341 fs::remove(scratch, ec);
342 EONC_LOG_ERROR("Failed to serialize frame for {}: {}", path.string(),
343 e.what());
345 }
346 std::error_code ec;
347 fs::remove(scratch, ec);
348
349 std::ofstream out(path, std::ios::binary | std::ios::app);
350 if (!out) {
351 EONC_LOG_ERROR("Failed to open {} for append", path.string());
353 }
354 // A frame header must start its own line. eOn's own frames end in a
355 // newline; a hand-written or foreign tail may not.
356 if (!ends_with_newline(path)) {
357 out.put('\n');
358 }
359 out.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
360 out.flush();
361 if (!out) {
362 EONC_LOG_ERROR("Failed to append to {}", path.string());
364 }
366}
367
370readcon::ConFrameBuilder seed_builder(Matter &m,
371 const std::array<std::string, 2> &prebox,
372 const std::array<std::string, 2> &postbox,
373 const std::vector<uint64_t> &atom_ids) {
374 auto [lengths, angles_deg] = eonc::io::cell_to_lengths_angles(m);
375 readcon::ConFrameBuilder builder(
376 {lengths[0], lengths[1], lengths[2]},
377 {angles_deg[0], angles_deg[1], angles_deg[2]}, prebox, postbox);
378
379 const long n = m.numberOfAtoms();
380 if (atom_ids.size() != static_cast<size_t>(n)) {
381 throw std::invalid_argument("seed_builder: atom_ids size mismatch");
382 }
383 for (long i = 0; i < n; ++i) {
384 const auto mask = m.getFixedMask(i);
385 builder.add_atom(symbol_for_z(m.getAtomicNr(i)), 0.0, 0.0, 0.0, mask,
386 atom_ids[static_cast<size_t>(i)], m.getMass(i));
387 }
388 return builder;
389}
390
391bool should_write_forces(const Matter &m,
392 const eonc::io::ConFrameMetadata *metadata) {
393 // Per-write metadata wins so two callers can disagree without a race on
394 // the process-wide flag. Parameters next, so pyeonclient setting the
395 // field on the bound Parameters object is enough. The atomic global is
396 // last (INI parse and the explicit setter).
397 if (metadata && metadata->write_con_forces.has_value()) {
398 return *metadata->write_con_forces;
399 }
400 if (m.getWriteConForces()) {
401 return true;
402 }
404}
405
406void apply_geometry(readcon::ConFrameBuilder &builder, Matter &m,
407 bool with_velocities,
408 const eonc::io::ConFrameMetadata *metadata) {
409 const long n = m.numberOfAtoms();
410 if (n <= 0) {
411 return;
412 }
413
414 // Prefer bulk flat setters (declared sections, no pointer-lifetime hazards
415 // under ASAN). AtomMatrix is RowMajor Nx3 matching the flat layout.
416 builder.set_positions_from_flat(flat_row_major(m.getPositions()));
417
418 // getForcesRaw() may trigger pot evaluation if recomputePotential is set;
419 // only enter when the pot is already clean so we serialize cached forces.
420 // Gated: force sections are opt-in ([Main] write_con_forces) because
421 // ASE-class readers reject frames that carry them.
422 if (should_write_forces(m, metadata) && !m.needsForceUpdate()) {
423 builder.set_forces_from_flat(flat_row_major(m.getForcesRaw()));
424 }
425
426 if (with_velocities) {
427 const AtomMatrix vel = m.getVelocities();
428 for (long i = 0; i < n; ++i) {
429 builder.set_atom_velocity(static_cast<size_t>(i),
430 {vel(i, 0), vel(i, 1), vel(i, 2)});
431 }
432 }
433}
434
435void collect_ids_headers(Matter &m, std::vector<uint64_t> &atom_ids,
436 std::array<std::string, 2> &prebox,
437 std::array<std::string, 2> &postbox) {
438 const long n = m.numberOfAtoms();
439 atom_ids.resize(static_cast<size_t>(n));
440 for (long i = 0; i < n; ++i) {
441 atom_ids[static_cast<size_t>(i)] = static_cast<uint64_t>(m.getAtomIndex(i));
442 }
443 const auto &hdr = m.getHeaderCon();
444 prebox = {canonical_generator_header(hdr[0]), strip_nl(hdr[1])};
445 postbox = {strip_nl(hdr[3]), strip_nl(hdr[4])};
446}
447
448readcon::ConFrame frame_from_matter(Matter &m,
449 const eonc::io::ConFrameMetadata *metadata,
450 bool with_velocities) {
451 std::vector<uint64_t> atom_ids;
452 std::array<std::string, 2> prebox;
453 std::array<std::string, 2> postbox;
454 collect_ids_headers(m, atom_ids, prebox, postbox);
455
456 auto builder = seed_builder(m, prebox, postbox, atom_ids);
457
459 const eonc::io::ConFrameMetadata *meta_ptr = metadata;
460 if (!m.needsForceUpdate()) {
461 if (metadata == nullptr) {
462 auto_meta.energy = m.getPotentialEnergy();
463 meta_ptr = &auto_meta;
464 } else if (!metadata->energy) {
465 auto_meta = *metadata;
466 auto_meta.energy = m.getPotentialEnergy();
467 meta_ptr = &auto_meta;
468 }
469 }
470 apply_frame_metadata(builder, meta_ptr);
471 apply_geometry(builder, m, with_velocities, meta_ptr);
472 return builder.build();
473}
474
475} // namespace
476
477namespace eonc::io {
478
479namespace {
480// Process-wide opt-in for force sections in written frames; classic con
481// layout (no sections) keeps ASE-class readers working on our outputs.
482std::atomic<bool> g_write_con_forces{false};
483} // namespace
484
485readcon::ConFrame matterToConFrame(Matter &m,
486 const ConFrameMetadata *metadata) {
487 return frame_from_matter(m, metadata, /*with_velocities=*/false);
488}
489
490void set_write_con_forces(bool enabled) noexcept {
491 g_write_con_forces.store(enabled, std::memory_order_relaxed);
492}
493
494bool write_con_forces() noexcept {
495 return g_write_con_forces.load(std::memory_order_relaxed);
496}
497
498ConFrameMetadata metadata_from_frame(const readcon::ConFrame &frame) {
499 ConFrameMetadata meta;
500 meta.energy = frame.energy_opt();
501 meta.frame_index = frame.frame_index_opt();
502 meta.time = frame.time_opt();
503 meta.timestep = frame.timestep_opt();
504 meta.neb_bead = frame.neb_bead_opt();
505 meta.neb_band = frame.neb_band_opt();
506 meta.potential_type = frame.potential_type();
507 const auto json = frame.metadata_json();
508 if (!json.empty() && json != "{}") {
509 meta.raw_json = json;
510 }
511 return meta;
512}
513
514std::pair<std::array<double, 3>, std::array<double, 3>>
516 const Matrix3d cell = m.getCell();
517 std::array<double, 3> lengths;
518 lengths[0] = cell.row(0).norm();
519 lengths[1] = cell.row(1).norm();
520 lengths[2] = cell.row(2).norm();
521 // CON header line 4 is alpha beta gamma:
522 // alpha = angle(b,c), beta = angle(a,c), gamma = angle(a,b).
523 std::array<double, 3> angles;
525 cell.row(1).dot(cell.row(2)), lengths[1] * lengths[2])) *
526 180.0 / eonc::helpers::pi;
528 cell.row(0).dot(cell.row(2)), lengths[0] * lengths[2])) *
529 180.0 / eonc::helpers::pi;
531 cell.row(0).dot(cell.row(1)), lengths[0] * lengths[1])) *
532 180.0 / eonc::helpers::pi;
533 return {lengths, angles};
534}
535
537 const std::lock_guard<std::mutex> guard(append_mutex());
538 append_stamps().clear();
539}
540
541IoStatus matter2con(Matter &m, std::string filename, bool append,
542 const ConFrameMetadata *metadata) {
543 filename = ensure_extension(std::move(filename), ".con");
544
546
547 const fs::path path(filename);
548 const auto compression =
549 readcon::ConFrameWriter::compression_from_extension(path);
550 const bool streamable =
551 compression == readcon::ConFrameWriter::Compression::None;
552
553 // The lock covers the stamp table and the read-then-extend window on one
554 // path. Nothing under it evaluates a potential: frame_from_matter reads
555 // cached energy and forces only while the pot is clean.
556 const std::lock_guard<std::mutex> guard(append_mutex());
557 const auto key = append_key(path);
558 const bool exists = fs::exists(path);
559 const bool concatenate = append && exists && streamable;
560
561 // ConFrame is move-only (no default ctor), so frames carries the new frame
562 // either way. A gzip or zstd target cannot be concatenated: readcon-core
563 // reads a single gzip member, so appended members would be invisible
564 // on read-back, and the streaming writers cannot be flushed frame by frame.
565 // Those targets keep the whole-file rewrite, which stays O(N) per append.
566 std::vector<readcon::ConFrame> frames;
567 if (append && exists && !streamable) {
568 try {
569 // Prefer read_all_frames (single ownership hand-off) over the iterator
570 // path for append rewrite; simpler lifetime for ASAN/CI envs.
571 frames = readcon::read_all_frames(path);
572 } catch (const std::exception &e) {
573 EONC_LOG_ERROR("Failed to append to {}: {}", filename, e.what());
575 }
576 } else if (concatenate && !tail_is_ours(key, path)) {
577 // Refuse to extend a file eOn cannot parse, matching the rewrite path:
578 // the target keeps its bytes and the caller sees AppendError. Frames this
579 // process wrote and nobody touched since need no such check.
580 try {
581 [[maybe_unused]] const auto existing = readcon::read_all_frames(path);
582 } catch (const std::exception &e) {
583 EONC_LOG_ERROR("Failed to append to {}: {}", filename, e.what());
585 }
586 }
587 try {
588 frames.push_back(frame_from_matter(m, metadata, /*with_velocities=*/false));
589 } catch (const std::exception &e) {
590 EONC_LOG_ERROR("Failed to build frame for {}: {}", filename, e.what());
592 }
593
594 const auto status = concatenate ? append_frames(path, frames, kConPrecision)
595 : write_frames(path, frames, kConPrecision);
596 if (io_ok(status)) {
597 remember_stamp(key, path);
598 } else {
599 append_stamps().erase(key);
600 }
601 return status;
602}
603
604IoStatus con2matter(Matter &m, std::string filename) {
605 filename = ensure_extension(std::move(filename), ".con");
606 try {
607 auto frame = readcon::read_first_frame(filename);
608 return con2matter(m, frame, nullptr);
609 } catch (const std::exception &e) {
610 EONC_LOG_ERROR("Failed to read {}: {}", filename, e.what());
611 return IoStatus::ReadError;
612 }
613}
614
615IoStatus con2matter(Matter &m, const readcon::ConFrame &frame,
616 ConFrameMetadata *out_metadata) {
617 try {
618 const auto &atoms = frame.atoms();
619 const auto &lengths = frame.cell();
620 const auto &angles_deg = frame.angles();
621 const auto &prebox = frame.prebox_header();
622 const auto &postbox = frame.postbox_header();
623
624 m.headerCon[0] = prebox[0] + "\n";
625 m.headerCon[1] = prebox[1] + "\n";
626 m.headerCon[3] = postbox[0] + "\n";
627 m.headerCon[4] = postbox[1] + "\n";
628
629 double angles[3] = {angles_deg[0], angles_deg[1], angles_deg[2]};
630 if (angles[0] == 90.0 && angles[1] == 90.0 && angles[2] == 90.0) {
631 Matrix3d cell = Matrix3d::Zero();
632 cell(0, 0) = lengths[0];
633 cell(1, 1) = lengths[1];
634 cell(2, 2) = lengths[2];
635 m.setCell(cell);
636 } else {
637 angles[0] *= eonc::helpers::pi / 180.0;
638 angles[1] *= eonc::helpers::pi / 180.0;
639 angles[2] *= eonc::helpers::pi / 180.0;
640 const double alpha = angles[0];
641 const double beta = angles[1];
642 const double gamma = angles[2];
643
644 Matrix3d cell = Matrix3d::Zero();
645 cell(0, 0) = 1.0;
646 cell(1, 0) = cos(gamma);
647 cell(1, 1) = sin(gamma);
648 cell(2, 0) = cos(beta);
649 cell(2, 1) = (cos(alpha) - cell(1, 0) * cell(2, 0)) / cell(1, 1);
650 cell(2, 2) = eonc::safemath::safe_sqrt(1.0 - pow(cell(2, 0), 2) -
651 pow(cell(2, 1), 2));
652
653 cell(0, 0) *= lengths[0];
654 cell(1, 0) *= lengths[1];
655 cell(1, 1) *= lengths[1];
656 cell(2, 0) *= lengths[2];
657 cell(2, 1) *= lengths[2];
658 cell(2, 2) *= lengths[2];
659 m.setCell(cell);
660 }
661 m.headerCon[2] =
662 std::format("{} {} {}\n", angles_deg[0], angles_deg[1], angles_deg[2]);
663
664 const auto n = static_cast<Eigen::Index>(atoms.size());
665 m.resize(static_cast<long>(atoms.size()));
666
667 // Undo the species grouping the .con format imposes, so an index into
668 // Matter addresses the same atom as the matching row of mode.dat.
669 const std::vector<size_t> order = matter_order(atoms);
670
671 AtomMatrix positions = AtomMatrix::Zero(n, 3);
672 AtomMatrix forces = AtomMatrix::Zero(n, 3);
673 AtomMatrix velocities = AtomMatrix::Zero(n, 3);
674 VectorXd masses = VectorXd::Zero(n);
675 VectorXi atomic_nrs = VectorXi::Zero(n);
676 bool any_force = false;
677 bool any_velocity = false;
678
679 for (Eigen::Index i = 0; i < n; ++i) {
680 const auto &atom = atoms[order[static_cast<size_t>(i)]];
681 positions(i, 0) = atom.x;
682 positions(i, 1) = atom.y;
683 positions(i, 2) = atom.z;
684 masses(i) = atom.mass;
685 atomic_nrs(i) = static_cast<int>(atom.atomic_number);
686 const auto fixed = atom.fixed_mask();
687 m.setFixedMask(static_cast<long>(i), {fixed[0], fixed[1], fixed[2]});
688 m.setAtomIndex(static_cast<long>(i),
689 static_cast<std::int64_t>(atom.atom_id));
690
691 if (auto vel = atom.velocity()) {
692 any_velocity = true;
693 velocities(i, 0) = (*vel)[0];
694 velocities(i, 1) = (*vel)[1];
695 velocities(i, 2) = (*vel)[2];
696 }
697 if (auto force = atom.force()) {
698 any_force = true;
699 forces(i, 0) = (*force)[0];
700 forces(i, 1) = (*force)[1];
701 forces(i, 2) = (*force)[2];
702 }
703 }
704
705 m.setMasses(masses);
706 m.setAtomicNrs(atomic_nrs);
707 m.setPositions(positions);
708
709 if (any_velocity || frame.has_velocities()) {
710 m.setVelocities(velocities);
711 }
712
713 const auto meta = metadata_from_frame(frame);
714 if (out_metadata != nullptr) {
715 *out_metadata = meta;
716 }
717
718 // Trust file energy+forces only when both are present. Energy-only must not
719 // mark the pot clean with a zero force matrix (optimizer footgun). Prefer
720 // writing raw forces (friend) so fixed-atom components survive RT; then
721 // mark clean without setComputedPotential's net-force adjustment on zeros.
722 const bool has_force_section = any_force || frame.has_forces();
723 if (meta.energy && has_force_section) {
724 m.forces = forces;
725 m.potentialEnergy = *meta.energy;
726 m.energyVariance = 0.0;
727 m.recomputePotential = false;
728 m.recomputeMaskedForces = true;
729 } else if (has_force_section) {
730 m.forces = forces;
731 m.recomputePotential = true;
732 m.recomputeMaskedForces = true;
733 } else {
734 // Classic geometry-only files: always recompute pot (main-era behavior).
735 m.recomputePotential = true;
736 }
737
738 // setPositions already applied PBC when enabled; no second wrap here.
739 return IoStatus::Ok;
740 } catch (const std::exception &e) {
741 EONC_LOG_ERROR("Failed to convert frame to matter: {}", e.what());
742 return IoStatus::ReadError;
743 }
744}
745
746IoStatus matter2convel(Matter &m, std::string filename) {
747 filename = ensure_extension(std::move(filename), ".convel");
748
750
751 try {
752 auto frame = frame_from_matter(m, nullptr, /*with_velocities=*/true);
753 std::vector<readcon::ConFrame> frames;
754 frames.push_back(std::move(frame));
755 return write_frames(filename, frames, kConvelPrecision);
756 } catch (const std::exception &e) {
757 EONC_LOG_ERROR("Failed to write convel {}: {}", filename, e.what());
759 }
760}
761
762IoStatus convel2matter(Matter &m, std::string filename) {
763 filename = ensure_extension(std::move(filename), ".convel");
764 try {
765 auto frame = readcon::read_first_frame(filename);
766 return con2matter(m, frame, nullptr);
767 } catch (const std::exception &e) {
768 EONC_LOG_ERROR("Failed to read convel {}: {}", filename, e.what());
769 return IoStatus::ReadError;
770 }
771}
772
773IoStatus matter2xyz(Matter &m, std::string filename, bool append) {
774 filename = ensure_extension(std::move(filename), ".xyz");
775
777 const long n = m.numberOfAtoms();
778
779 if (append && fs::exists(filename)) {
780 std::error_code ec;
781 const auto sz = fs::file_size(filename, ec);
782 if (!ec && sz > 0) {
783 const auto prev = last_xyz_natoms(filename);
784 if (!prev) {
785 EONC_LOG_ERROR("matter2xyz: cannot parse existing {}", filename);
787 }
788 if (*prev != n) {
790 "matter2xyz: append atom count {} != last frame {} in {}", n, *prev,
791 filename);
793 }
794 }
795 }
796
797 std::ofstream out;
798 out.open(filename,
799 append ? (std::ios::out | std::ios::app | std::ios::binary)
800 : (std::ios::out | std::ios::trunc | std::ios::binary));
801 if (!out) {
802 EONC_LOG_ERROR("matter2xyz: cannot open {}", filename);
803 return IoStatus::OpenError;
804 }
805
806 const Matrix3d cell = m.getCell();
807 out << std::format(
808 "{}\nLattice=\"{:.17g} {:.17g} {:.17g} {:.17g} {:.17g} {:.17g} "
809 "{:.17g} {:.17g} {:.17g}\" Properties=species:S:1:pos:R:3 Generated "
810 "by eOn\n",
811 n, cell(0, 0), cell(0, 1), cell(0, 2), cell(1, 0), cell(1, 1), cell(1, 2),
812 cell(2, 0), cell(2, 1), cell(2, 2));
813 const AtomMatrix pos = m.getPositions();
814 for (long i = 0; i < n; ++i) {
815 out << std::format("{}\t{:.17g}\t{:.17g}\t{:.17g}\n",
816 symbol_for_z(m.getAtomicNr(i)), pos(i, 0), pos(i, 1),
817 pos(i, 2));
818 }
819 // Close before testing: the destructor's flush is where a full disk or a
820 // short write surfaces, and by then the state is gone.
821 out.close();
822 if (!out) {
823 EONC_LOG_ERROR("matter2xyz: failed to write {}", filename);
825 }
826 return IoStatus::Ok;
827}
828
829IoStatus writeTibble(Matter &m, std::string fname) {
830 // Debug table, not a structure format. getForces()/getPotentialEnergy()
831 // run computePotential() on a dirty pot, which would charge a dump to
832 // the force-call count in results.dat. Cached values only; the header
833 // drops the columns it cannot fill. atmID is the CON column-5 id.
834 const bool have_forces = !m.needsForceUpdate();
835 if (!have_forces) {
837 "writeTibble: pot is dirty, {} omits the force and energy columns",
838 fname);
839 }
840 const AtomMatrix pos = m.getPositions();
841 std::ofstream out(fname);
842 if (!out) {
843 EONC_LOG_ERROR("writeTibble: cannot open {}", fname);
844 return IoStatus::OpenError;
845 }
846 out << (have_forces ? "x y z fx fy fz energy mass symbol atmID fixed\n"
847 : "x y z mass symbol atmID fixed\n");
848 const AtomMatrix fSys = have_forces ? m.getForces() : AtomMatrix();
849 const double eSys = have_forces ? m.getPotentialEnergy() : 0.0;
850 for (long idx = 0; idx < m.numberOfAtoms(); ++idx) {
851 out << std::format("{} {} {}", pos(idx, 0), pos(idx, 1), pos(idx, 2));
852 if (have_forces) {
853 out << std::format(" {} {} {} {}", fSys(idx, 0), fSys(idx, 1),
854 fSys(idx, 2), eSys);
855 }
856 const auto mask = m.getFixedMask(idx);
857 const int fixed_bits =
858 (mask[0] ? 1 : 0) | (mask[1] ? 2 : 0) | (mask[2] ? 4 : 0);
859 out << std::format(" {} {} {} {}\n", m.getMass(idx),
860 symbol_for_z(m.getAtomicNr(idx)), m.getAtomIndex(idx),
861 fixed_bits);
862 }
863 out.close();
864 if (!out) {
865 EONC_LOG_ERROR("writeTibble: failed to write {}", fname);
867 }
868 return IoStatus::Ok;
869}
870
871std::vector<readcon::ConFrame>
872buildNebPathFrames(const std::vector<std::shared_ptr<Matter>> &path,
873 const std::vector<ConFrameMetadata> &metadata_per_image) {
874 std::vector<readcon::ConFrame> frames;
875 if (path.empty() || path.size() != metadata_per_image.size()) {
877 "buildNebPathFrames: path/metadata size mismatch (path={}, meta={})",
878 path.size(), metadata_per_image.size());
879 return frames;
880 }
881 for (const auto &img : path) {
882 if (!img) {
883 EONC_LOG_ERROR("buildNebPathFrames: null Matter in path");
884 return {};
885 }
886 }
887
888 Matter &template_m = *path.front();
890
891 std::vector<uint64_t> atom_ids;
892 std::array<std::string, 2> prebox;
893 std::array<std::string, 2> postbox;
894 collect_ids_headers(template_m, atom_ids, prebox, postbox);
895
896 frames.reserve(path.size());
897 try {
898 auto seed = seed_builder(template_m, prebox, postbox, atom_ids);
899 for (size_t i = 0; i < path.size(); ++i) {
900 Matter &img = *path[i];
902 if (img.numberOfAtoms() != template_m.numberOfAtoms()) {
904 "buildNebPathFrames: image {} atom count {} != template {}", i,
905 img.numberOfAtoms(), template_m.numberOfAtoms());
906 return {};
907 }
908 auto builder = seed.clone();
909 apply_frame_metadata(builder, &metadata_per_image[i]);
910 apply_geometry(builder, img, /*with_velocities=*/false,
911 &metadata_per_image[i]);
912 frames.push_back(builder.build());
913 }
914 } catch (const std::exception &e) {
915 EONC_LOG_ERROR("buildNebPathFrames failed: {}", e.what());
916 return {};
917 }
918 return frames;
919}
920
921IoStatus writeConFrames(std::string filename,
922 const std::vector<readcon::ConFrame> &frames) {
923 if (frames.empty()) {
925 }
926 filename = ensure_extension(std::move(filename), ".con");
927 const fs::path path(filename);
928 const std::lock_guard<std::mutex> guard(append_mutex());
929 const auto key = append_key(path);
930 const auto status = write_frames(path, frames, kConPrecision);
931 if (io_ok(status)) {
932 remember_stamp(key, path);
933 } else {
934 append_stamps().erase(key);
935 }
936 return status;
937}
938
939IoStatus writeNebPath(std::string filename,
940 const std::vector<std::shared_ptr<Matter>> &path,
941 const std::vector<ConFrameMetadata> &metadata_per_image) {
942 auto frames = buildNebPathFrames(path, metadata_per_image);
943 if (frames.empty()) {
945 }
946 return writeConFrames(std::move(filename), frames);
947}
948
949} // namespace eonc::io
Eigen::Matrix< double, 3, 3, eOnStorageOrder > Matrix3d
Definition Eigen.h:35
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
Definition Eigen.h:37
#define EONC_LOG_ERROR(...)
Definition EonLogger.h:262
#define EONC_LOG_WARNING(...)
Definition EonLogger.h:256
nlohmann::json json
double potentialEnergy
Definition Matter.h:386
double getPotentialEnergy() const
Definition Matter.cpp:446
bool getWriteConForces() const noexcept
Parameters.main_options.writeConForces for this Matter, if bound.
Definition Matter.h:261
void setAtomicNrs(const VectorXi &atmnrs)
Definition Matter.cpp:571
void setMasses(const VectorXd &massesIn)
Definition Matter.cpp:386
const AtomMatrix & getForces() const
Definition Matter.cpp:324
bool recomputeMaskedForces
Definition Matter.h:381
void setVelocities(const AtomMatrix &v)
Definition Matter.cpp:604
void applyPeriodicBoundaryIfEnabled()
Apply MIC wrap when periodic boundaries are enabled (I/O path).
Definition Matter.h:327
long int numberOfAtoms() const
Definition Matter.cpp:209
void setAtomIndex(long int atom, std::int64_t index)
Definition Matter.h:303
void resize(long int nAtoms)
Definition Matter.cpp:173
AtomMatrix forces
Definition Matter.h:369
const AtomMatrix & getForcesRaw() const
Definition Matter.cpp:335
bool recomputePotential
Definition Matter.h:348
void setCell(const Matrix3d &newCell)
Definition Matter.cpp:213
bool needsForceUpdate() const
Whether forces need recomputation (positions changed since last eval).
Definition Matter.h:232
std::array< std::string, 5 > headerCon
Definition Matter.h:354
std::array< bool, 3 > getFixedMask(long int atom) const
Per-axis CON column-4 mask (bit0=x, bit1=y, bit2=z).
Definition Matter.cpp:413
const std::array< std::string, 5 > & getHeaderCon() const
CON header lines (indices 0..4); public for I/O / bindings.
Definition Matter.h:308
std::int64_t getAtomIndex(long int atom) const
.con column-5 index (pre-grouping); public for I/O / bindings.
Definition Matter.h:300
long getAtomicNr(long int atom) const
Definition Matter.cpp:392
void setFixedMask(long int atom, std::array< bool, 3 > mask)
Definition Matter.cpp:433
Matrix3d getCell() const
Definition Matter.cpp:211
AtomMatrix getVelocities() const
Definition Matter.cpp:600
double energyVariance
Definition Matter.h:384
void setPositions(const AtomMatrix &pos)
Definition Matter.cpp:273
double getMass(long int atom) const
Definition Matter.cpp:380
const AtomMatrix & getPositions() const
Definition Matter.cpp:236
constexpr double pi
IoStatus writeConFrames(std::string filename, const std::vector< readcon::ConFrame > &frames)
Write already-built ConFrames to a multi-frame .con (temp or durable).
IoStatus convel2matter(Matter &m, std::string filename)
IoStatus matter2convel(Matter &m, std::string filename)
void resetConAppendState()
Drop the per-path bookkeeping that lets append-mode writes skip re-parsing frames this process wrote.
std::pair< std::array< double, 3 >, std::array< double, 3 > > cell_to_lengths_angles(const Matter &m)
ConFrameMetadata metadata_from_frame(const readcon::ConFrame &frame)
Extract known frame-level fields from a parsed readcon frame.
bool write_con_forces() noexcept
IoStatus matter2con(Matter &m, std::string filename, bool append, const ConFrameMetadata *metadata)
Append a frame to a .con, or truncate and write one frame.
IoStatus con2matter(Matter &m, std::string filename)
std::vector< readcon::ConFrame > buildNebPathFrames(const std::vector< std::shared_ptr< Matter > > &path, const std::vector< ConFrameMetadata > &metadata_per_image)
Build NEB band ConFrames without writing (clone builder path of writeNebPath).
IoStatus matter2xyz(Matter &m, std::string filename, bool append)
Write one extended-XYZ frame: Lattice= cell and 17-digit coordinates.
IoStatus
Structured I/O result for the client surface (nanobind-friendly).
Definition ConFileIO.h:29
void set_write_con_forces(bool enabled) noexcept
Whether written .con frames carry "Forces of Component" sections.
readcon::ConFrame matterToConFrame(Matter &m, const ConFrameMetadata *metadata)
Build a single stamped ConFrame from Matter (same builder as matter2con).
constexpr bool io_ok(IoStatus s) noexcept
Definition ConFileIO.h:38
IoStatus writeTibble(Matter &m, std::string fname)
Debug table (positions, optional cached forces). Not a structure format.
IoStatus writeNebPath(std::string filename, const std::vector< std::shared_ptr< Matter > > &path, const std::vector< ConFrameMetadata > &metadata_per_image)
Write a full NEB path as one multi-frame .con using ConFrameBuilder::clone().
constexpr double safe_div(double num, double denom, double fallback=0.0)
Definition SafeMath.h:21
double safe_acos(double x)
Definition SafeMath.h:34
double safe_sqrt(double x)
Definition SafeMath.h:38
std::optional< uint64_t > frame_index
Definition ConFileIO.h:72
std::optional< uint64_t > neb_band
Definition ConFileIO.h:77
std::vector< ConMetadataValue > scalars
Definition ConFileIO.h:79
std::optional< double > energy
Definition ConFileIO.h:73
std::optional< double > timestep
Definition ConFileIO.h:75
std::vector< ConMetadataText > strings
Definition ConFileIO.h:80
std::optional< double > time
Definition ConFileIO.h:74
std::optional< std::string > raw_json
Definition ConFileIO.h:81
std::optional< std::string > potential_type
Definition ConFileIO.h:78
std::optional< uint64_t > neb_bead
Definition ConFileIO.h:76
std::optional< bool > write_con_forces
When set, this write includes or omits force sections regardless of Parameters.main_options....
Definition ConFileIO.h:84