33#include <system_error>
34#include <unordered_map>
40namespace fs = std::filesystem;
42constexpr uint8_t kConPrecision = 17;
43constexpr uint8_t kConvelPrecision = 6;
45std::string strip_nl(
const std::string &s) {
47 while (!str.empty() && (str.back() ==
'\n' || str.back() ==
'\r'))
52std::string canonical_generator_header(
const std::string &header) {
53 auto stripped = strip_nl(header);
54 if (!stripped.empty()) {
57 return "Generated by eOn";
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) {
72std::string symbol_for_z(
long atomic_nr) {
73 return readcon::z_to_symbol(
static_cast<uint64_t
>(atomic_nr));
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));
85void apply_frame_metadata(readcon::ConFrameBuilder &builder,
87 if (metadata ==
nullptr) {
91 builder.set_metadata_json(*metadata->
raw_json);
94 builder.set_energy(*metadata->
energy);
100 builder.set_time(*metadata->
time);
103 builder.set_timestep(*metadata->
timestep);
106 builder.set_neb_bead(*metadata->
neb_bead);
109 builder.set_neb_band(*metadata->
neb_band);
112 builder.set_string_metadata(
"potential_type", *metadata->
potential_type);
114 for (
const auto &[key, value] : metadata->
scalars) {
115 builder.set_scalar_metadata(key, value);
117 for (
const auto &[key, value] : metadata->
strings) {
118 builder.set_string_metadata(key, value);
123 const std::vector<readcon::ConFrame> &frames,
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());
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});
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;
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;
166 std::vector<uint64_t> ids;
168 for (
const auto &atom : atoms) {
169 ids.push_back(atom.atom_id);
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()) {
176 std::stable_sort(order.begin(), order.end(),
177 [&ids](
size_t a,
size_t b) { return ids[a] < ids[b]; });
185 std::uintmax_t size{0};
186 fs::file_time_type mtime{};
187 friend bool operator==(
const FileStamp &,
const FileStamp &) =
default;
190std::optional<FileStamp> stamp_file(
const fs::path &path) {
192 const auto size = fs::file_size(path, ec);
196 const auto mtime = fs::last_write_time(path, ec);
200 return FileStamp{size, mtime};
206std::string append_key(
const fs::path &path) {
208 auto resolved = fs::weakly_canonical(path, ec);
209 if (ec || resolved.empty()) {
210 resolved = fs::absolute(path, ec);
212 return path.lexically_normal().string();
215 return resolved.lexically_normal().string();
220std::mutex &append_mutex() {
225std::unordered_map<std::string, FileStamp> &append_stamps() {
226 static std::unordered_map<std::string, FileStamp> stamps;
231void remember_stamp(
const std::string &key,
const fs::path &path) {
232 if (
auto stamp = stamp_file(path)) {
233 append_stamps()[key] = *stamp;
235 append_stamps().erase(key);
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()) {
246 const auto stamp = stamp_file(path);
247 return stamp.has_value() && *stamp == it->second;
252std::optional<long> last_xyz_natoms(
const fs::path &path) {
253 std::ifstream in(path);
257 std::optional<long> last;
259 while (std::getline(in, line)) {
266 }
catch (
const std::exception &) {
272 if (!std::getline(in, line)) {
275 for (
long i = 0; i < n; ++i) {
276 if (!std::getline(in, line)) {
286bool ends_with_newline(
const fs::path &path) {
287 std::ifstream in(path, std::ios::binary | std::ios::ate);
291 const auto len =
static_cast<std::streamoff
>(in.tellg());
313 const std::vector<readcon::ConFrame> &frames,
315 static std::atomic<uint64_t> scratch_counter{0};
318 std::format(
".{}.eon-append-{}.tmp", path.filename().string(),
319 scratch_counter.fetch_add(1, std::memory_order_relaxed));
324 readcon::ConFrameWriter writer(
325 scratch, readcon::ConFrameWriter::Compression::None, precision);
326 writer.extend(frames);
328 std::ifstream in(scratch, std::ios::binary | std::ios::ate);
330 throw std::runtime_error(
"serialized frame not readable back");
332 const auto len =
static_cast<std::streamoff
>(in.tellg());
334 throw std::runtime_error(
"serialized frame is empty");
336 bytes.resize(
static_cast<size_t>(len));
338 if (!in.read(bytes.data(),
static_cast<std::streamsize
>(len))) {
339 throw std::runtime_error(
"short read of serialized frame");
341 }
catch (
const std::exception &e) {
343 fs::remove(scratch, ec);
344 EONC_LOG_ERROR(
"Failed to serialize frame for {}: {}", path.string(),
349 fs::remove(scratch, ec);
351 std::ofstream out(path, std::ios::binary | std::ios::app);
358 if (!ends_with_newline(path)) {
361 out.write(bytes.data(),
static_cast<std::streamsize
>(bytes.size()));
373 const std::array<std::string, 2> &prebox,
374 const std::array<std::string, 2> &postbox,
375 const std::vector<uint64_t> &atom_ids) {
377 readcon::ConFrameBuilder builder(
378 {lengths[0], lengths[1], lengths[2]},
379 {angles_deg[0], angles_deg[1], angles_deg[2]}, prebox, postbox);
382 if (atom_ids.size() !=
static_cast<size_t>(n)) {
383 throw std::invalid_argument(
"seed_builder: atom_ids size mismatch");
385 for (
long i = 0; i < n; ++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));
393bool should_write_forces(
const Matter &m,
402 if (m.getWriteConForces()) {
408void apply_geometry(readcon::ConFrameBuilder &builder, Matter &m,
409 bool with_velocities,
411 const long n = m.numberOfAtoms();
418 builder.set_positions_from_flat(flat_row_major(m.getPositions()));
424 if (should_write_forces(m, metadata) && !m.needsForceUpdate()) {
425 builder.set_forces_from_flat(flat_row_major(m.getForcesRaw()));
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");
432 builder.set_displacements_from_flat(metadata->
displacements);
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");
438 builder.set_spreads_from_flat(metadata->
spreads);
441 if (with_velocities) {
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)});
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));
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])};
463readcon::ConFrame frame_from_matter(Matter &m,
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);
471 auto builder = seed_builder(m, prebox, postbox, atom_ids);
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;
485 apply_frame_metadata(builder, meta_ptr);
486 apply_geometry(builder, m, with_velocities, meta_ptr);
487 return builder.build();
497std::atomic<bool> g_write_con_forces{
false};
502 return frame_from_matter(m, metadata,
false);
506 g_write_con_forces.store(enabled, std::memory_order_relaxed);
510 return g_write_con_forces.load(std::memory_order_relaxed);
515 meta.
energy = frame.energy_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();
522 const auto json = frame.metadata_json();
523 if (!
json.empty() &&
json !=
"{}") {
529std::pair<std::array<double, 3>, std::array<double, 3>>
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();
538 std::array<double, 3> angles;
540 cell.row(1).dot(cell.row(2)), lengths[1] * lengths[2])) *
543 cell.row(0).dot(cell.row(2)), lengths[0] * lengths[2])) *
546 cell.row(0).dot(cell.row(1)), lengths[0] * lengths[1])) *
548 return {lengths, angles};
552 const std::lock_guard<std::mutex> guard(append_mutex());
553 append_stamps().clear();
558 filename = ensure_extension(std::move(filename),
".con");
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;
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;
581 std::vector<readcon::ConFrame> frames;
582 if (append && exists && !streamable) {
586 frames = readcon::read_all_frames(path);
587 }
catch (
const std::exception &e) {
591 }
else if (concatenate && !tail_is_ours(key, path)) {
596 [[maybe_unused]]
const auto existing = readcon::read_all_frames(path);
597 }
catch (
const std::exception &e) {
603 frames.push_back(frame_from_matter(m, metadata,
false));
604 }
catch (
const std::exception &e) {
605 EONC_LOG_ERROR(
"Failed to build frame for {}: {}", filename, e.what());
609 const auto status = concatenate ? append_frames(path, frames, kConPrecision)
610 : write_frames(path, frames, kConPrecision);
612 remember_stamp(key, path);
617 append_stamps().erase(key);
623 filename = ensure_extension(std::move(filename),
".con");
625 auto frame = readcon::read_first_frame(filename);
631 }
catch (
const std::exception &e) {
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();
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) {
654 cell(0, 0) = lengths[0];
655 cell(1, 1) = lengths[1];
656 cell(2, 2) = lengths[2];
662 const double alpha = angles[0];
663 const double beta = angles[1];
664 const double gamma = angles[2];
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);
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];
684 std::format(
"{} {} {}\n", angles_deg[0], angles_deg[1], angles_deg[2]);
686 const auto n =
static_cast<Eigen::Index
>(atoms.size());
687 m.
resize(
static_cast<long>(atoms.size()));
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);
698 AtomMatrix positions = 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;
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]});
716 static_cast<std::int64_t
>(atom.atom_id));
718 if (
auto vel = atom.velocity()) {
720 velocities(i, 0) = (*vel)[0];
721 velocities(i, 1) = (*vel)[1];
722 velocities(i, 2) = (*vel)[2];
724 if (
auto force = atom.force()) {
726 forces(i, 0) = (*force)[0];
727 forces(i, 1) = (*force)[1];
728 forces(i, 2) = (*force)[2];
736 if (any_velocity || frame.has_velocities()) {
741 if (out_metadata !=
nullptr) {
742 *out_metadata = meta;
749 const bool has_force_section = any_force || frame.has_forces();
750 if (meta.energy && has_force_section) {
752 }
else if (has_force_section) {
761 }
catch (
const std::exception &e) {
762 EONC_LOG_ERROR(
"Failed to convert frame to matter: {}", e.what());
768 filename = ensure_extension(std::move(filename),
".convel");
773 auto frame = frame_from_matter(m,
nullptr,
true);
774 std::vector<readcon::ConFrame> frames;
775 frames.push_back(std::move(frame));
776 const IoStatus status = write_frames(filename, frames, kConvelPrecision);
781 }
catch (
const std::exception &e) {
782 EONC_LOG_ERROR(
"Failed to write convel {}: {}", filename, e.what());
788 filename = ensure_extension(std::move(filename),
".convel");
790 auto frame = readcon::read_first_frame(filename);
796 }
catch (
const std::exception &e) {
797 EONC_LOG_ERROR(
"Failed to read convel {}: {}", filename, e.what());
803 filename = ensure_extension(std::move(filename),
".xyz");
808 if (append && fs::exists(filename)) {
810 const auto sz = fs::file_size(filename, ec);
812 const auto prev = last_xyz_natoms(filename);
819 "matter2xyz: append atom count {} != last frame {} in {}", n, *prev,
828 append ? (std::ios::out | std::ios::app | std::ios::binary)
829 : (std::ios::out | std::ios::trunc | std::ios::binary));
837 if (append && !ends_with_newline(filename)) {
843 "{}\nLattice=\"{:.17g} {:.17g} {:.17g} {:.17g} {:.17g} {:.17g} "
844 "{:.17g} {:.17g} {:.17g}\" Properties=species:S:1:pos:R:3 Generated "
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));
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),
872 "writeTibble: pot is dirty, {} omits the force and energy columns",
876 std::ofstream out(fname);
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");
886 out << std::format(
"{} {} {}", pos(idx, 0), pos(idx, 1), pos(idx, 2));
888 out << std::format(
" {} {} {} {}", fSys(idx, 0), fSys(idx, 1),
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),
906std::vector<readcon::ConFrame>
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());
916 for (
const auto &img : path) {
923 Matter &template_m = *path.front();
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);
931 frames.reserve(path.size());
933 auto seed = seed_builder(template_m, prebox, postbox, atom_ids);
934 for (
size_t i = 0; i < path.size(); ++i) {
939 "buildNebPathFrames: image {} atom count {} != template {}", i,
943 auto builder = seed.clone();
944 apply_frame_metadata(builder, &metadata_per_image[i]);
945 apply_geometry(builder, img,
false,
946 &metadata_per_image[i]);
947 frames.push_back(builder.build());
949 }
catch (
const std::exception &e) {
957 const std::vector<readcon::ConFrame> &frames) {
958 if (frames.empty()) {
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);
967 remember_stamp(key, path);
970 append_stamps().erase(key);
976 const std::vector<std::shared_ptr<Matter>> &path,
977 const std::vector<ConFrameMetadata> &metadata_per_image) {
979 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(...)
void setFixedMask(long int atom, std::array< bool, 3 > mask)
const AtomMatrix & getPositions() const
void setPositions(const AtomMatrix &pos)
void setCell(const Matrix3d &newCell)
void applyPeriodicBoundaryIfEnabled()
Apply MIC wrap when periodic boundaries are enabled (I/O path).
long int numberOfAtoms() const
void setAtomicNrs(const VectorXi &atmnrs)
void setVelocities(const AtomMatrix &v)
void setAtomIndex(long int atom, std::int64_t index)
void resize(long int nAtoms)
void restoreFileForces(const AtomMatrix &fileForces, bool trustEnergy, double energy)
Write .con forces without the fixed-atom mask setForces applies.
double getMass(long int atom) const
void setMasses(const VectorXd &massesIn)
double getPotentialEnergy() const
bool needsForceUpdate() const
Whether forces need recomputation (positions changed since last eval).
std::array< std::string, 5 > headerCon
long getAtomicNr(long int atom) const
std::array< bool, 3 > getFixedMask(long int atom) const
Per-axis CON column-4 mask (bit0=x, bit1=y, bit2=z).
std::int64_t getAtomIndex(long int atom) const
.con column-5 index (pre-grouping); public for I/O / bindings.
const AtomMatrix & getForces() const
void setFileToMatter(std::vector< long > map)
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 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
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)