32#include <system_error>
33#include <unordered_map>
38namespace fs = std::filesystem;
40constexpr uint8_t kConPrecision = 17;
41constexpr uint8_t kConvelPrecision = 6;
43std::string strip_nl(
const std::string &s) {
45 while (!str.empty() && (str.back() ==
'\n' || str.back() ==
'\r'))
50std::string canonical_generator_header(
const std::string &header) {
51 auto stripped = strip_nl(header);
52 if (!stripped.empty()) {
55 return "Generated by eOn";
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) {
70std::string symbol_for_z(
long atomic_nr) {
71 return readcon::z_to_symbol(
static_cast<uint64_t
>(atomic_nr));
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));
83void apply_frame_metadata(readcon::ConFrameBuilder &builder,
85 if (metadata ==
nullptr) {
89 builder.set_metadata_json(*metadata->
raw_json);
92 builder.set_energy(*metadata->
energy);
98 builder.set_time(*metadata->
time);
101 builder.set_timestep(*metadata->
timestep);
104 builder.set_neb_bead(*metadata->
neb_bead);
107 builder.set_neb_band(*metadata->
neb_band);
110 builder.set_string_metadata(
"potential_type", *metadata->
potential_type);
112 for (
const auto &[key, value] : metadata->
scalars) {
113 builder.set_scalar_metadata(key, value);
115 for (
const auto &[key, value] : metadata->
strings) {
116 builder.set_string_metadata(key, value);
121 const std::vector<readcon::ConFrame> &frames,
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());
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});
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;
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;
164 std::vector<uint64_t> ids;
166 for (
const auto &atom : atoms) {
167 ids.push_back(atom.atom_id);
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()) {
174 std::stable_sort(order.begin(), order.end(),
175 [&ids](
size_t a,
size_t b) { return ids[a] < ids[b]; });
183 std::uintmax_t size{0};
184 fs::file_time_type mtime{};
185 friend bool operator==(
const FileStamp &,
const FileStamp &) =
default;
188std::optional<FileStamp> stamp_file(
const fs::path &path) {
190 const auto size = fs::file_size(path, ec);
194 const auto mtime = fs::last_write_time(path, ec);
198 return FileStamp{size, mtime};
204std::string append_key(
const fs::path &path) {
206 auto resolved = fs::weakly_canonical(path, ec);
207 if (ec || resolved.empty()) {
208 resolved = fs::absolute(path, ec);
210 return path.lexically_normal().string();
213 return resolved.lexically_normal().string();
218std::mutex &append_mutex() {
223std::unordered_map<std::string, FileStamp> &append_stamps() {
224 static std::unordered_map<std::string, FileStamp> stamps;
229void remember_stamp(
const std::string &key,
const fs::path &path) {
230 if (
auto stamp = stamp_file(path)) {
231 append_stamps()[key] = *stamp;
233 append_stamps().erase(key);
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()) {
244 const auto stamp = stamp_file(path);
245 return stamp.has_value() && *stamp == it->second;
250std::optional<long> last_xyz_natoms(
const fs::path &path) {
251 std::ifstream in(path);
255 std::optional<long> last;
257 while (std::getline(in, line)) {
264 }
catch (
const std::exception &) {
270 if (!std::getline(in, line)) {
273 for (
long i = 0; i < n; ++i) {
274 if (!std::getline(in, line)) {
284bool ends_with_newline(
const fs::path &path) {
285 std::ifstream in(path, std::ios::binary | std::ios::ate);
289 const auto len =
static_cast<std::streamoff
>(in.tellg());
311 const std::vector<readcon::ConFrame> &frames,
313 static std::atomic<uint64_t> scratch_counter{0};
316 std::format(
".{}.eon-append-{}.tmp", path.filename().string(),
317 scratch_counter.fetch_add(1, std::memory_order_relaxed));
322 readcon::ConFrameWriter writer(
323 scratch, readcon::ConFrameWriter::Compression::None, precision);
324 writer.extend(frames);
326 std::ifstream in(scratch, std::ios::binary | std::ios::ate);
328 throw std::runtime_error(
"serialized frame not readable back");
330 const auto len =
static_cast<std::streamoff
>(in.tellg());
332 throw std::runtime_error(
"serialized frame is empty");
334 bytes.resize(
static_cast<size_t>(len));
336 if (!in.read(bytes.data(),
static_cast<std::streamsize
>(len))) {
337 throw std::runtime_error(
"short read of serialized frame");
339 }
catch (
const std::exception &e) {
341 fs::remove(scratch, ec);
342 EONC_LOG_ERROR(
"Failed to serialize frame for {}: {}", path.string(),
347 fs::remove(scratch, ec);
349 std::ofstream out(path, std::ios::binary | std::ios::app);
356 if (!ends_with_newline(path)) {
359 out.write(bytes.data(),
static_cast<std::streamsize
>(bytes.size()));
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) {
375 readcon::ConFrameBuilder builder(
376 {lengths[0], lengths[1], lengths[2]},
377 {angles_deg[0], angles_deg[1], angles_deg[2]}, prebox, postbox);
380 if (atom_ids.size() !=
static_cast<size_t>(n)) {
381 throw std::invalid_argument(
"seed_builder: atom_ids size mismatch");
383 for (
long i = 0; i < n; ++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));
391bool should_write_forces(
const Matter &m,
406void apply_geometry(readcon::ConFrameBuilder &builder,
Matter &m,
407 bool with_velocities,
416 builder.set_positions_from_flat(flat_row_major(m.
getPositions()));
423 builder.set_forces_from_flat(flat_row_major(m.
getForcesRaw()));
426 if (with_velocities) {
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)});
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) {
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));
444 prebox = {canonical_generator_header(hdr[0]), strip_nl(hdr[1])};
445 postbox = {strip_nl(hdr[3]), strip_nl(hdr[4])};
448readcon::ConFrame frame_from_matter(
Matter &m,
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);
456 auto builder = seed_builder(m, prebox, postbox, atom_ids);
461 if (metadata ==
nullptr) {
463 meta_ptr = &auto_meta;
464 }
else if (!metadata->
energy) {
465 auto_meta = *metadata;
467 meta_ptr = &auto_meta;
470 apply_frame_metadata(builder, meta_ptr);
471 apply_geometry(builder, m, with_velocities, meta_ptr);
472 return builder.build();
482std::atomic<bool> g_write_con_forces{
false};
487 return frame_from_matter(m, metadata,
false);
491 g_write_con_forces.store(enabled, std::memory_order_relaxed);
495 return g_write_con_forces.load(std::memory_order_relaxed);
500 meta.
energy = frame.energy_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();
507 const auto json = frame.metadata_json();
508 if (!
json.empty() &&
json !=
"{}") {
514std::pair<std::array<double, 3>, std::array<double, 3>>
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();
523 std::array<double, 3> angles;
525 cell.row(1).dot(cell.row(2)), lengths[1] * lengths[2])) *
528 cell.row(0).dot(cell.row(2)), lengths[0] * lengths[2])) *
531 cell.row(0).dot(cell.row(1)), lengths[0] * lengths[1])) *
533 return {lengths, angles};
537 const std::lock_guard<std::mutex> guard(append_mutex());
538 append_stamps().clear();
543 filename = ensure_extension(std::move(filename),
".con");
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;
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;
566 std::vector<readcon::ConFrame> frames;
567 if (append && exists && !streamable) {
571 frames = readcon::read_all_frames(path);
572 }
catch (
const std::exception &e) {
576 }
else if (concatenate && !tail_is_ours(key, path)) {
581 [[maybe_unused]]
const auto existing = readcon::read_all_frames(path);
582 }
catch (
const std::exception &e) {
588 frames.push_back(frame_from_matter(m, metadata,
false));
589 }
catch (
const std::exception &e) {
590 EONC_LOG_ERROR(
"Failed to build frame for {}: {}", filename, e.what());
594 const auto status = concatenate ? append_frames(path, frames, kConPrecision)
595 : write_frames(path, frames, kConPrecision);
597 remember_stamp(key, path);
599 append_stamps().erase(key);
605 filename = ensure_extension(std::move(filename),
".con");
607 auto frame = readcon::read_first_frame(filename);
609 }
catch (
const std::exception &e) {
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();
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) {
632 cell(0, 0) = lengths[0];
633 cell(1, 1) = lengths[1];
634 cell(2, 2) = lengths[2];
640 const double alpha = angles[0];
641 const double beta = angles[1];
642 const double gamma = angles[2];
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);
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];
662 std::format(
"{} {} {}\n", angles_deg[0], angles_deg[1], angles_deg[2]);
664 const auto n =
static_cast<Eigen::Index
>(atoms.size());
665 m.
resize(
static_cast<long>(atoms.size()));
669 const std::vector<size_t> order = matter_order(atoms);
671 AtomMatrix positions = 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;
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]});
689 static_cast<std::int64_t
>(atom.atom_id));
691 if (
auto vel = atom.velocity()) {
693 velocities(i, 0) = (*vel)[0];
694 velocities(i, 1) = (*vel)[1];
695 velocities(i, 2) = (*vel)[2];
697 if (
auto force = atom.force()) {
699 forces(i, 0) = (*force)[0];
700 forces(i, 1) = (*force)[1];
701 forces(i, 2) = (*force)[2];
709 if (any_velocity || frame.has_velocities()) {
714 if (out_metadata !=
nullptr) {
715 *out_metadata = meta;
722 const bool has_force_section = any_force || frame.has_forces();
723 if (meta.energy && has_force_section) {
729 }
else if (has_force_section) {
740 }
catch (
const std::exception &e) {
741 EONC_LOG_ERROR(
"Failed to convert frame to matter: {}", e.what());
747 filename = ensure_extension(std::move(filename),
".convel");
752 auto frame = frame_from_matter(m,
nullptr,
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());
763 filename = ensure_extension(std::move(filename),
".convel");
765 auto frame = readcon::read_first_frame(filename);
767 }
catch (
const std::exception &e) {
768 EONC_LOG_ERROR(
"Failed to read convel {}: {}", filename, e.what());
774 filename = ensure_extension(std::move(filename),
".xyz");
779 if (append && fs::exists(filename)) {
781 const auto sz = fs::file_size(filename, ec);
783 const auto prev = last_xyz_natoms(filename);
790 "matter2xyz: append atom count {} != last frame {} in {}", n, *prev,
799 append ? (std::ios::out | std::ios::app | std::ios::binary)
800 : (std::ios::out | std::ios::trunc | std::ios::binary));
808 "{}\nLattice=\"{:.17g} {:.17g} {:.17g} {:.17g} {:.17g} {:.17g} "
809 "{:.17g} {:.17g} {:.17g}\" Properties=species:S:1:pos:R:3 Generated "
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));
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),
837 "writeTibble: pot is dirty, {} omits the force and energy columns",
841 std::ofstream out(fname);
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");
851 out << std::format(
"{} {} {}", pos(idx, 0), pos(idx, 1), pos(idx, 2));
853 out << std::format(
" {} {} {} {}", fSys(idx, 0), fSys(idx, 1),
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),
871std::vector<readcon::ConFrame>
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());
881 for (
const auto &img : path) {
888 Matter &template_m = *path.front();
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);
896 frames.reserve(path.size());
898 auto seed = seed_builder(template_m, prebox, postbox, atom_ids);
899 for (
size_t i = 0; i < path.size(); ++i) {
904 "buildNebPathFrames: image {} atom count {} != template {}", i,
908 auto builder = seed.clone();
909 apply_frame_metadata(builder, &metadata_per_image[i]);
910 apply_geometry(builder, img,
false,
911 &metadata_per_image[i]);
912 frames.push_back(builder.build());
914 }
catch (
const std::exception &e) {
922 const std::vector<readcon::ConFrame> &frames) {
923 if (frames.empty()) {
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);
932 remember_stamp(key, path);
934 append_stamps().erase(key);
940 const std::vector<std::shared_ptr<Matter>> &path,
941 const std::vector<ConFrameMetadata> &metadata_per_image) {
943 if (frames.empty()) {
Eigen::Matrix< double, 3, 3, eOnStorageOrder > Matrix3d
Eigen::Matrix< double, Eigen::Dynamic, 3, eOnStorageOrder > AtomMatrix
#define EONC_LOG_ERROR(...)
#define EONC_LOG_WARNING(...)
double getPotentialEnergy() const
bool getWriteConForces() const noexcept
Parameters.main_options.writeConForces for this Matter, if bound.
void setAtomicNrs(const VectorXi &atmnrs)
void setMasses(const VectorXd &massesIn)
const AtomMatrix & getForces() const
bool recomputeMaskedForces
void setVelocities(const AtomMatrix &v)
void applyPeriodicBoundaryIfEnabled()
Apply MIC wrap when periodic boundaries are enabled (I/O path).
long int numberOfAtoms() const
void setAtomIndex(long int atom, std::int64_t index)
void resize(long int nAtoms)
const AtomMatrix & getForcesRaw() const
void setCell(const Matrix3d &newCell)
bool needsForceUpdate() const
Whether forces need recomputation (positions changed since last eval).
std::array< std::string, 5 > headerCon
std::array< bool, 3 > getFixedMask(long int atom) const
Per-axis CON column-4 mask (bit0=x, bit1=y, bit2=z).
const std::array< std::string, 5 > & getHeaderCon() const
CON header lines (indices 0..4); public for I/O / bindings.
std::int64_t getAtomIndex(long int atom) const
.con column-5 index (pre-grouping); public for I/O / bindings.
long getAtomicNr(long int atom) const
void setFixedMask(long int atom, std::array< bool, 3 > mask)
AtomMatrix getVelocities() const
void setPositions(const AtomMatrix &pos)
double getMass(long int atom) const
const AtomMatrix & getPositions() const
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).
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
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)
double safe_acos(double x)
double safe_sqrt(double x)