Tutorials

1 Rust

1.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());
}

1.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!();
    }
}

1.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.

1.4 Building frames from data

Added in v0.4.0.

Use ConFrameBuilder to construct frames programmatically:

use readcon_core::types::ConFrameBuilder;

let mut builder = ConFrameBuilder::new([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);
let frame = builder.build();

1.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);

1.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());

1.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());

1.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();

2 Python

2.1 Installation

# From PyPI
pip install readcon

# From source
maturin develop --features python

# Via pixi
pixi r -e python python-build

2.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}")

2.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")

2.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,
)

2.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)

2.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)

2.7 ASE Atoms conversion

Added in v0.4.0.

Convert between ConFrame and ASE Atoms objects. ASE must be installed (it is an optional dependency).

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")

2.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

3 C

3.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;
}

3.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);

3.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);
}

3.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);

3.5 Memory management

The C API uses two ownership patterns:

  1. Opaque handles (RKRConFrame): allocated by Rust, freed with free_rkr_frame(). Used for lossless frame manipulation.

  2. Transparent structs (CFrame): extracted copies freed with free_c_frame(). Used for direct data access.

Always free both the CFrame and the RKRConFrame separately.

4 C++

4.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;
}

4.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();

4.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";

4.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);

4.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});

5 Julia

5.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")

5.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

5.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

5.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