Tutorials¶
1 Querying the spec version¶
Added in v0.5.0. Per-frame spec:sub:`version`added in v0.6.0.
There are two distinct version queries:
Library spec version: the highest spec version the library implements (compile-time constant).
File spec version: the version a particular file was written with, parsed from the JSON metadata on line 2. Legacy files without JSON metadata report version 1.
1.1 Rust¶
use readcon_core::{CON_SPEC_VERSION, VERSION};
// Library version
assert_eq!(CON_SPEC_VERSION, 2);
println!("readcon-core {} (spec v{})", VERSION, CON_SPEC_VERSION);
// Per-frame version (from parsed file)
use readcon_core::iterators::read_first_frame;
let frame = read_first_frame(std::path::Path::new("input.con")).unwrap();
println!("File spec version: {}", frame.header.spec_version);
println!("Extra metadata: {:?}", frame.header.metadata);
1.2 C/C++¶
#include "readcon-core.h"
// Compile-time library version check
#if RKR_CON_SPEC_VERSION >= 2
// atom_id semantics available
#endif
// Runtime library version
printf("spec v%u, lib %s\n",
rkr_con_spec_version(), rkr_library_version());
// Per-frame version (from parsed file)
RKRConFrame *handle = rkr_read_first_frame("input.con");
printf("File spec version: %u\n", rkr_frame_spec_version(handle));
// Full JSON metadata
char *json = rkr_frame_metadata_json(handle);
printf("Metadata: %s\n", json);
rkr_free_string(json);
free_rkr_frame(handle);
1.3 Python¶
import readcon
# Library version
print(readcon.__version__) # e.g. "0.6.0"
print(readcon.CON_SPEC_VERSION) # 2
# Per-frame version (from parsed file)
frames = readcon.read_con("input.con")
print(frames[0].spec_version) # 2 for v2 files, 1 for legacy
print(frames[0].metadata) # dict of extra JSON keys
2 Working with JSON metadata¶
Added in v0.6.0.
Line 2 of every v2 CON file contains a JSON object. The writer always
stamps con_spec_version; additional keys survive round-trips.
2.1 Rust: attaching custom metadata¶
use readcon_core::types::ConFrameBuilder;
use std::collections::BTreeMap;
let mut meta = BTreeMap::new();
meta.insert("generator".to_string(),
serde_json::Value::String("my-tool 1.0".to_string()));
meta.insert("temperature_K".to_string(),
serde_json::json!(300.0));
let builder = ConFrameBuilder::new([10.0, 10.0, 10.0], [90.0, 90.0, 90.0])
.metadata(meta);
// ... add atoms, build, write ...
// Line 2 of the output:
// {"con_spec_version":2,"generator":"my-tool 1.0","temperature_K":300.0}
2.2 Python: reading and attaching metadata¶
import readcon
# Reading: metadata comes from the file's JSON line
frames = readcon.read_con("input.con")
print(frames[0].metadata) # e.g. {"generator": "\"my-tool 1.0\""}
# Writing: metadata dict is passed through to the JSON line
frame = readcon.ConFrame(
cell=[10.0, 10.0, 10.0],
angles=[90.0, 90.0, 90.0],
atoms=[...],
metadata={"temperature_K": "300.0"},
)
readcon.write_con("output.con", [frame])
2.3 Legacy file detection¶
import warnings
import readcon
frames = readcon.read_con("old_eon_file.con")
if frames[0].spec_version < 2:
warnings.warn(
"Legacy CON file (spec v1): atom_id values may be sequential placeholders",
stacklevel=2,
)
3 Rust¶
3.1 Reading a CON file¶
use std::fs;
use readcon_core::iterators::ConFrameIterator;
let contents = fs::read_to_string("trajectory.con").unwrap();
let iter = ConFrameIterator::new(&contents);
for result in iter {
let frame = result.unwrap();
println!("Cell: {:?}", frame.header.boxl);
println!("Atoms: {}", frame.atom_data.len());
}
3.2 Reading a convel file with velocities¶
The same iterator handles both .con and .convel files.
Velocity data is detected automatically by the parser.
use std::fs;
use readcon_core::iterators::ConFrameIterator;
let contents = fs::read_to_string("trajectory.convel").unwrap();
let iter = ConFrameIterator::new(&contents);
for result in iter {
let frame = result.unwrap();
println!("Has velocities: {}", frame.has_velocities());
for atom in &frame.atom_data {
print!("{} ({:.4}, {:.4}, {:.4})", atom.symbol, atom.x, atom.y, atom.z);
if atom.has_velocity() {
print!(" vel=({:.6}, {:.6}, {:.6})",
atom.vx.unwrap(), atom.vy.unwrap(), atom.vz.unwrap());
}
println!();
}
}
3.3 Writing frames¶
use std::fs::File;
use readcon_core::writer::ConFrameWriter;
let file = File::create("output.con").unwrap();
let mut writer = ConFrameWriter::new(file);
for frame in &frames {
writer.write_frame(frame).unwrap();
}
Velocity data is written automatically when
frame.has_velocities() returns true.
3.4 Building frames from data¶
Added in v0.4.0.
Use ConFrameBuilder to construct frames programmatically. The
atom_id parameter (6th argument to add_atom) is the original atom
index – the position of this atom in your input before type-based
grouping. The builder groups atoms by symbol automatically, but
preserves each atom’s atom_id so the original ordering can be
reconstructed.
use readcon_core::types::ConFrameBuilder;
let mut builder = ConFrameBuilder::new([10.0, 10.0, 10.0], [90.0, 90.0, 90.0]);
// atom_id 0 and 1 for Cu, 2 for H -- these survive type-based reordering
builder.add_atom("Cu", 0.0, 0.0, 0.0, false, 0, 63.546);
builder.add_atom("H", 1.0, 2.0, 3.0, false, 2, 1.008);
builder.add_atom("Cu", 2.55, 2.55, 0.0, false, 1, 63.546);
let frame = builder.build();
// Atoms are now grouped: Cu(id=0), Cu(id=1), H(id=2)
3.5 Writing with custom precision¶
Added in v0.4.0.
Control the number of decimal places in output coordinates:
use std::fs::File;
use readcon_core::writer::ConFrameWriter;
// Default precision (6 decimal places)
let file = File::create("output.con").unwrap();
let mut writer = ConFrameWriter::new(file);
// High precision (17 decimal places for lossless roundtrip)
let file = File::create("precise.con").unwrap();
let mut writer = ConFrameWriter::with_precision(file, 17);
3.6 Reading a single frame efficiently¶
Added in v0.4.3.
When only the first frame is needed, read_first_frame stops after
parsing it. Files under 64 KiB are read into memory directly; larger
files use memory-mapped I/O.
use readcon_core::iterators::read_first_frame;
use std::path::Path;
let frame = read_first_frame(Path::new("single.con")).unwrap();
println!("Atoms: {}", frame.atom_data.len());
3.7 Reading all frames from a file¶
For trajectory files with many frames, read_all_frames applies
the same small-file optimization and uses memory-mapped I/O for
larger files.
use readcon_core::iterators::read_all_frames;
use std::path::Path;
let frames = read_all_frames(Path::new("large_trajectory.con")).unwrap();
println!("Loaded {} frames", frames.len());
3.8 Parallel parsing¶
Behind the parallel feature gate, multi-frame files can be parsed
concurrently using rayon.
#[cfg(feature = "parallel")]
use readcon_core::iterators::parse_frames_parallel;
let contents = std::fs::read_to_string("large_trajectory.con").unwrap();
let results = parse_frames_parallel(&contents);
let frames: Vec<_> = results.into_iter().filter_map(|r| r.ok()).collect();
4 Python¶
4.1 Installation¶
# From PyPI
pip install readcon
# From source
maturin develop --features python
# Via pixi
pixi r -e python python-build
4.2 Reading and inspecting frames¶
import readcon
# Read from file
frames = readcon.read_con("trajectory.con")
# Read from string
with open("trajectory.con") as f:
frames = readcon.read_con_string(f.read())
for frame in frames:
print(f"Cell: {frame.cell}")
print(f"Angles: {frame.angles}")
print(f"Atom count: {len(frame)}")
print(f"Has velocities: {frame.has_velocities}")
print(f"Pre-box header: {frame.prebox_header}")
for atom in frame.atoms:
print(f" {atom.symbol}: ({atom.x:.4f}, {atom.y:.4f}, {atom.z:.4f})")
print(f" fixed={atom.is_fixed}, id={atom.atom_id}, mass={atom.mass}")
4.3 Working with convel velocity data¶
import readcon
frames = readcon.read_con("trajectory.convel")
frame = frames[0]
if frame.has_velocities:
for atom in frame.atoms:
if atom.has_velocity:
print(f"{atom.symbol}: vel=({atom.vx:.6f}, {atom.vy:.6f}, {atom.vz:.6f})")
else:
print(f"{atom.symbol}: no velocity")
4.4 Constructing frames from data¶
Added in v0.4.0.
Build frames programmatically using Atom and ConFrame constructors:
import readcon
atoms = [
readcon.Atom(symbol="Cu", x=0.0, y=0.0, z=0.0,
is_fixed=False, atom_id=1),
readcon.Atom(symbol="Cu", x=2.55, y=2.55, z=0.0,
is_fixed=False, atom_id=2),
]
frame = readcon.ConFrame(
cell=[10.0, 10.0, 10.0],
angles=[90.0, 90.0, 90.0],
atoms=atoms,
)
4.5 Writing frames¶
import readcon
# Read, modify, and write back
frames = readcon.read_con("input.con")
# Write to file
readcon.write_con("output.con", frames)
# Write to string
output = readcon.write_con_string(frames)
print(output)
4.6 Writing with custom precision¶
Added in v0.4.0.
Pass the precision keyword to control decimal places:
import readcon
# Default (6 decimal places)
readcon.write_con("output.con", frames)
# Lossless roundtrip (17 decimal places)
readcon.write_con("precise.con", frames, precision=17)
output = readcon.write_con_string(frames, precision=17)
4.7 ASE Atoms conversion¶
Added in v0.4.0. atom:sub:`id`, velocity, and mass transfer added in v0.5.1.
Convert between ConFrame and ASE Atoms objects. ASE must be
installed (it is an optional dependency). As of v0.5.1, the
conversion preserves atomid(via a custom per-atom array),
velocities (for convel data), and masses.
import readcon
# ConFrame -> ase.Atoms
frames = readcon.read_con("input.con")
ase_atoms = frames[0].to_ase()
# ase.Atoms -> ConFrame
frame = readcon.ConFrame.from_ase(ase_atoms)
# Convenience: read a .con file directly as ase.Atoms list
ase_list = readcon.read_con_as_ase("trajectory.con")
4.7.1 atomidin ASE roundtrips¶
to_ase() stores each atom’s atom_id as a custom atom_id
per-atom array on the ASE Atoms object. Tags are left untouched
so they remain available for other uses (slab layers, etc.).
import readcon
frames = readcon.read_con("saddle.con")
ase_atoms = frames[0].to_ase()
# atom_id is stored as a named per-atom array
print(ase_atoms.get_array("atom_id")) # e.g. [3, 0, 1, 4, 2]
# Roundtrip preserves atom_id
frame_back = readcon.ConFrame.from_ase(ase_atoms)
assert [a.atom_id for a in frame_back.atoms] == [3, 0, 1, 4, 2]
When converting from an ASE Atoms object that was not created by
readcon, from_ase() checks for the atom_id array first, then
falls back to tags (if any are non-zero), and finally uses
sequential indices.
4.7.2 Velocities and masses in ASE roundtrips¶
Velocities from .convel files are transferred via
set_velocities() / get_velocities(). Custom masses (when present
on the ConFrame) override ASE’s atomic-number defaults via
set_masses().
import readcon
# convel -> ASE preserves velocities
frames = readcon.read_con("trajectory.convel")
ase_atoms = frames[0].to_ase()
print(ase_atoms.get_velocities()) # non-zero velocity array
# Roundtrip back to ConFrame
frame_back = readcon.ConFrame.from_ase(ase_atoms)
assert frame_back.has_velocities
4.8 Roundtrip test pattern¶
import readcon
frames = readcon.read_con("input.convel")
output = readcon.write_con_string(frames)
frames2 = readcon.read_con_string(output)
assert len(frames) == len(frames2)
for orig, reread in zip(frames, frames2):
assert len(orig) == len(reread)
assert orig.has_velocities == reread.has_velocities
5 C¶
5.1 Reading and printing frames¶
#include "readcon-core.h"
#include <stdio.h>
int main(int argc, char *argv[]) {
CConFrameIterator *iter = read_con_file_iterator(argv[1]);
if (!iter) return 1;
RKRConFrame *handle;
while ((handle = con_frame_iterator_next(iter)) != NULL) {
CFrame *frame = rkr_frame_to_c_frame(handle);
printf("Cell: [%.4f, %.4f, %.4f]\n",
frame->cell[0], frame->cell[1], frame->cell[2]);
printf("Atoms: %zu, Has velocities: %s\n",
frame->num_atoms, frame->has_velocities ? "yes" : "no");
for (size_t i = 0; i < frame->num_atoms; i++) {
CAtom *a = &frame->atoms[i];
printf(" Atom %zu: (%.4f, %.4f, %.4f) fixed=%d id=%llu\n",
i, a->x, a->y, a->z, a->is_fixed,
(unsigned long long)a->atom_id);
if (a->has_velocity) {
printf(" vel=(%.6f, %.6f, %.6f)\n", a->vx, a->vy, a->vz);
}
}
free_c_frame(frame);
free_rkr_frame(handle);
}
free_con_frame_iterator(iter);
return 0;
}
5.2 Building frames from data¶
Added in v0.4.0.
#include "readcon-core.h"
double cell[3] = {10.0, 10.0, 10.0};
double angles[3] = {90.0, 90.0, 90.0};
RKRConFrame *frame = rkr_frame_new(cell, angles, "", "", "", "");
rkr_frame_add_atom(frame, "Cu", 0.0, 0.0, 0.0, false, 1, 63.546);
rkr_frame_add_atom(frame, "Cu", 2.55, 2.55, 0.0, false, 2, 63.546);
rkr_frame_finalize(frame);
// Write with custom precision
RKRConFrameWriter *writer =
create_writer_from_path_with_precision_c("output.con", 17);
rkr_writer_extend(writer, (const RKRConFrame **)&frame, 1);
free_rkr_writer(writer);
free_rkr_frame(frame);
5.3 Reading a single frame¶
Added in v0.4.0.
RKRConFrame *frame = rkr_read_first_frame("input.con");
if (frame) {
CFrame *cf = rkr_frame_to_c_frame(frame);
printf("Atoms: %zu\n", cf->num_atoms);
free_c_frame(cf);
free_rkr_frame(frame);
}
5.4 Writing frames¶
#include "readcon-core.h"
// After collecting handles into an array:
RKRConFrameWriter *writer = create_writer_from_path_c("output.con");
int result = rkr_writer_extend(writer,
(const RKRConFrame **)handles,
num_frames);
free_rkr_writer(writer);
5.5 Memory management¶
The C API uses two ownership patterns:
Opaque handles (
RKRConFrame): allocated by Rust, freed withfree_rkr_frame(). Used for lossless frame manipulation.Transparent structs (
CFrame): extracted copies freed withfree_c_frame(). Used for direct data access.
Always free both the CFrame and the RKRConFrame separately.
6 C++¶
6.1 RAII iteration with range-based for¶
#include "readcon-core.hpp"
#include <iostream>
int main(int argc, char *argv[]) {
readcon::ConFrameIterator frames(argv[1]);
for (auto&& frame : frames) {
auto cell = frame.cell();
auto angles = frame.angles();
std::cout << "Cell: " << cell[0] << ", " << cell[1] << ", "
<< cell[2] << "\n";
std::cout << "Has velocities: " << std::boolalpha
<< frame.has_velocities() << "\n";
for (const auto& atom : frame.atoms()) {
std::cout << " (" << atom.x << ", " << atom.y << ", "
<< atom.z << ")";
if (atom.has_velocity) {
std::cout << " vel=(" << atom.vx << ", " << atom.vy
<< ", " << atom.vz << ")";
}
std::cout << "\n";
}
}
return 0;
}
6.2 Building frames from data¶
Added in v0.4.0.
#include "readcon-core.hpp"
readcon::ConFrameBuilder builder(
{10.0, 10.0, 10.0}, {90.0, 90.0, 90.0});
builder.add_atom("Cu", 0.0, 0.0, 0.0, false, 1, 63.546);
builder.add_atom("Cu", 2.55, 2.55, 0.0, false, 2, 63.546);
auto frame = builder.build();
6.3 Reading a single frame¶
Added in v0.4.0.
auto frame = readcon::read_first_frame("input.con");
std::cout << "Atoms: " << frame.atoms().size() << "\n";
6.4 Collecting and writing frames¶
#include "readcon-core.hpp"
#include <vector>
readcon::ConFrameIterator input("input.con");
std::vector<readcon::ConFrame> all_frames;
for (auto&& frame : input) {
all_frames.push_back(std::move(frame));
}
// Default precision (6 decimal places)
readcon::ConFrameWriter writer("output.con");
writer.extend(all_frames);
// High precision (17 decimal places)
readcon::ConFrameWriter precise_writer("precise.con", 17);
precise_writer.extend(all_frames);
6.5 Integration with eOn¶
The C++ RAII API is used by eOn for all .con and .convel I/O:
// Reading: mmap-based single frame read
auto frame = readcon::read_first_frame(con_path);
// frame.cell(), frame.atoms(), frame.has_velocities()
// Writing: builder with precision control
readcon::ConFrameBuilder builder(lengths, angles);
for (int i = 0; i < nAtoms; i++) {
builder.add_atom(symbol, x, y, z, is_fixed, id, mass);
}
auto out_frame = builder.build();
readcon::ConFrameWriter writer(path, 17); // 17 digits for lossless roundtrip
writer.extend({out_frame});
7 Julia¶
7.1 Installation and setup¶
# Point to the shared library
ENV["READCON_LIB_PATH"] = "/path/to/libreadcon_core.so"
# Or build from source (library auto-discovered)
using Pkg
Pkg.develop(path="julia/ReadCon")
7.2 Reading frames¶
using ReadCon
frames = read_con("trajectory.con")
for frame in frames
println("Cell: ", frame.cell)
println("Angles: ", frame.angles)
println("Atoms: ", length(frame.atoms))
println("Has velocities: ", frame.has_velocities)
println("Header: ", frame.prebox_header)
for atom in frame.atoms
@printf(" (%.4f, %.4f, %.4f) fixed=%s id=%d\n",
atom.x, atom.y, atom.z, atom.is_fixed, atom.atom_id)
end
end
7.3 Working with convel velocity data¶
using ReadCon
frames = read_con("trajectory.convel")
frame = frames[1]
if frame.has_velocities
for atom in frame.atoms
if atom.has_velocity
@printf("vel=(%.6f, %.6f, %.6f)\n", atom.vx, atom.vy, atom.vz)
end
end
end
7.4 Multi-frame trajectory processing¶
using ReadCon
frames = read_con("neb_trajectory.con")
println("Loaded $(length(frames)) images")
# Process each NEB image
for (i, frame) in enumerate(frames)
n_free = count(a -> !a.is_fixed, frame.atoms)
println("Image $i: $n_free free atoms")
end