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