readcon-core 0.14.8

An oxidized single and multiple CON file reader and writer with FFI bindings for ergonomic C/C++ usage. Implements CON spec v3.
Documentation
"""Chemfiles → CON ingress via Python (read_chemfiles*).

Skips when the wheel is lean (has_chemfiles_support is False).
"""

from __future__ import annotations

from pathlib import Path

import pytest

import readcon

pytestmark = pytest.mark.skipif(
    not getattr(readcon, "has_chemfiles_support", lambda: False)(),
    reason="chemfiles not linked in this wheel (install readcon-chemfiles)",
)


@pytest.fixture
def water_xyz(tmp_path: Path) -> Path:
    p = tmp_path / "water.xyz"
    p.write_text(
        "3\n"
        "water for readcon chemfiles import\n"
        "O  0.000  0.000  0.000\n"
        "H  0.957  0.000  0.000\n"
        "H -0.240  0.927  0.000\n"
    )
    return p


def test_read_chemfiles_first_xyz(water_xyz: Path, tmp_path: Path):
    frame = readcon.read_chemfiles_first(str(water_xyz))
    assert len(frame.atoms) == 3
    out = tmp_path / "water.con"
    frame.write_con(str(out))
    assert out.is_file()
    back = readcon.read_first_frame(str(out))
    assert len(back.atoms) == 3


def test_read_chemfiles_all_and_memory(water_xyz: Path):
    frames = readcon.read_chemfiles(str(water_xyz))
    assert len(frames) == 1
    data = water_xyz.read_text()
    mem = readcon.read_chemfiles_memory(data, "XYZ")
    assert len(mem) == 1
    assert len(mem[0].atoms) == 3


def test_chemfiles_stamps_internal_units(water_xyz: Path):
    frame = readcon.read_chemfiles_first(str(water_xyz))
    units = frame.metadata["units"]
    assert units["length"] == "angstrom"
    assert units["time"] == "ps"
    assert units["mass"] == "amu"
    assert frame.metadata["chemfiles::unit_system"]["velocity"] == "angstrom/ps"


def test_read_chemfiles_nth_and_nsteps(tmp_path: Path):
    p = tmp_path / "three.xyz"
    p.write_text(
        "2\nframe0\nCu 0.0 0.0 0.0\nH 1.0 0.0 0.0\n"
        "2\nframe1\nCu 2.0 0.0 0.0\nH 3.0 0.0 0.0\n"
        "2\nframe2\nCu 4.0 0.0 0.0\nH 5.0 0.0 0.0\n"
    )
    assert readcon.read_chemfiles_nsteps(str(p)) == 3
    mid = readcon.read_chemfiles_nth(str(p), 1)
    xs = [a.x for a in mid.atoms if a.symbol == "Cu"]
    assert xs and xs[0] == pytest.approx(2.0)
    strided = readcon.read_chemfiles(str(p), skip=0, step=2)
    assert len(strided) == 2
    guessed = readcon.read_chemfiles_memory(
        "3\nwater\nO 0 0 0\nH 0.957 0 0\nH -0.240 0.927 0\n",
        "XYZ",
        guess_bonds=True,
    )
    assert guessed[0].has_bonds


def test_select_methods_on_frame_with_bonds():
    atoms = [
        readcon.Atom(
            "O", 0.0, 0.0, 0.0, fixed=[False, False, False], atom_id=0, mass=16.0
        ),
        readcon.Atom(
            "H", 1.0, 0.0, 0.0, fixed=[False, False, False], atom_id=1, mass=1.0
        ),
        readcon.Atom(
            "H", 0.0, 1.0, 0.0, fixed=[False, False, False], atom_id=2, mass=1.0
        ),
    ]
    frame = readcon.ConFrame(
        cell=[10.0, 10.0, 10.0],
        angles=[90.0, 90.0, 90.0],
        atoms=atoms,
        metadata={"con_spec_version": 2, "bonds": [[0, 1], [0, 2]]},
    )
    assert frame.has_bonds
    assert frame.select_atoms("name O") == [0]
    angles = frame.select("angles: all")
    assert angles["context_size"] == 3
    assert len(angles["matches"]) >= 1