Skip to content

load

Loading structures and ensembles, with clear errors about what is supported.

Crystal.load and Molecule.load are separate entry points that raise a bare KeyError on anything they do not recognise, and the caller has to know in advance which one to use. Often you do not - a file named geometry.in may be either - so this module dispatches on its own and always returns the same thing: a list of frames.

One file may hold several structures - a trajectory, a multi-block CIF - and several files may be viewed as one ensemble, so a single structure is just the one-frame case rather than a separate path through the code.

Frame dataclass

One structure out of a possibly larger ensemble.

source is the file it came from. The other two are both "which one is this", but they answer different questions and so cannot share a field: a CIF data block names a structure, while a trajectory comment describes a state of one structure. Only the former belongs in a title.

Source code in chmpy/fmt/load.py
@dataclass
class Frame:
    """One structure out of a possibly larger ensemble.

    `source` is the file it came from. The other two are both "which one is
    this", but they answer different questions and so cannot share a field: a
    CIF data block *names a structure*, while a trajectory comment *describes
    a state* of one structure. Only the former belongs in a title.
    """

    structure: object
    source: str
    label: str = ""  # what this frame is a snapshot of: a trajectory comment
    block: str = ""  # what names this structure in the file: a CIF data block

    @property
    def is_crystal(self):
        return isinstance(self.structure, Crystal)

    @property
    def title(self):
        """What the file calls this structure.

        Preference order: the name the structure carries, then the block that
        names it in the file, then the file name. A multi-block CIF should
        show the block, since the file name is the same for every structure
        in it.
        """
        name = getattr(self.structure, "titl", None) or getattr(
            self.structure, "name", None
        )
        if not name or name in GENERIC_NAMES:
            name = self.block or Path(self.source).stem
        return str(name)

title property

What the file calls this structure.

Preference order: the name the structure carries, then the block that names it in the file, then the file name. A multi-block CIF should show the block, since the file name is the same for every structure in it.

UnknownFormat

Bases: ValueError

Raised when nothing knows how to read a file.

Source code in chmpy/fmt/load.py
class UnknownFormat(ValueError):
    """Raised when nothing knows how to read a file."""

    def __init__(self, path, reason=""):
        detail = f" ({reason})" if reason else ""
        super().__init__(
            f"don't know how to read {Path(path).name}{detail}\n"
            f"supported formats:\n{supported()}"
        )

load_file(path)

Every structure in one file.

Source code in chmpy/fmt/load.py
def load_file(path) -> list[Frame]:
    """Every structure in one file."""
    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"no such file: {path}")
    suffix, name = path.suffix.lower(), path.name

    # the other multi-frame formats go to ASE, which already reads them as one
    if suffix == ".xyz":
        return _from_trajectory(path)
    if name in CRYSTAL_NAMES or suffix in CRYSTAL_SUFFIXES:
        return _from_crystal_file(path)
    if name in EITHER_NAMES or suffix in EITHER_SUFFIXES:
        return _from_aims(path)
    if suffix in MOLECULE_SUFFIXES:
        return [Frame(Molecule.load(str(path)), path.name)]
    return _from_ase(path)

load_frames(sources)

Every structure across several files, viewed as one ensemble.

A directory contributes every file in it that can be read, sorted by name, which is the usual shape of an optimisation or a set of candidates.

Source code in chmpy/fmt/load.py
def load_frames(sources) -> list[Frame]:
    """Every structure across several files, viewed as one ensemble.

    A directory contributes every file in it that can be read, sorted by name,
    which is the usual shape of an optimisation or a set of candidates.
    """
    if isinstance(sources, str | Path):
        sources = [sources]

    paths = []
    for source in sources:
        path = Path(source)
        if path.is_dir():
            paths.extend(sorted(p for p in path.iterdir() if p.is_file()))
        else:
            paths.append(path)

    frames, failures = [], []
    for path in paths:
        try:
            frames.extend(load_file(path))
        except (UnknownFormat, FileNotFoundError, ValueError) as exc:
            failures.append((path, exc))

    if not frames:
        if len(failures) == 1:
            raise failures[0][1]
        detail = "\n".join(f"  {p.name}: {e}".split("\n")[0] for p, e in failures)
        raise UnknownFormat(paths[0] if paths else "?", f"nothing readable\n{detail}")
    return frames

load_stdin(text)

Read an ensemble from piped text, guessing the format from content.

Source code in chmpy/fmt/load.py
def load_stdin(text) -> list[Frame]:
    """Read an ensemble from piped text, guessing the format from content."""
    import tempfile

    name = sniff(text)
    # every reader here takes a path, and a temporary file is a great deal
    # simpler than threading an optional string through all of them
    with tempfile.TemporaryDirectory() as tmp:
        path = Path(tmp) / name
        path.write_text(text)
        frames = load_file(path)
    for frame in frames:
        frame.source = "<stdin>"
    return frames

sniff(text)

Guess a format from file contents, for input arriving without a name.

Source code in chmpy/fmt/load.py
def sniff(text):
    """Guess a format from file contents, for input arriving without a name."""
    # aims declares its atoms with a keyword, so it is recognisable whether or
    # not the geometry happens to be periodic
    if any(
        line.lstrip().startswith(("lattice_vector", "atom ", "atom_frac"))
        for line in text.splitlines()
    ):
        return "geometry.in"
    if "data_" in text or "_cell_length_a" in text or "loop_" in text:
        return "structure.cif"
    first = text.lstrip().split("\n", 1)[0].strip()
    if first.isdigit():
        return "structure.xyz"
    raise UnknownFormat("<stdin>", "cannot tell what format this is")

supported()

A human-readable summary of what can be loaded.

Source code in chmpy/fmt/load.py
def supported() -> str:
    """A human-readable summary of what can be loaded."""
    groups = [
        ("periodic", {**CRYSTAL_SUFFIXES, **CRYSTAL_NAMES}),
        ("molecular", MOLECULE_SUFFIXES),
        ("either", {**EITHER_SUFFIXES, **EITHER_NAMES}),
        ("multi-frame", TRAJECTORY_SUFFIXES),
    ]
    lines = []
    for name, table in groups:
        entries = sorted({f"{k} ({v})" for k, v in table.items()})
        lines.append(f"  {name:12s} {', '.join(entries)}")
    lines.append("  anything else is passed to ASE, if it is installed")
    return "\n".join(lines)