Skip to content

Systems

A System is one atomic configuration: a table with one row per atom, a simulation box, global information such as the energy, and for molecules an optional topology of bonds, angles, dihedrals and impropers. Every analysis, builder, reader and calculator of MDAPY takes and returns systems.

import numpy as np
import polars as pl
import mdapy as mp

water = mp.System(
    positions=[[0.0, 0.0, 0.0], [0.96, 0.0, 0.0], [-0.24, 0.93, 0.0]],
    box=mp.Box(10.0),
    elements=["O", "H", "H"],
    info={"energy": -14.2},
)
print(water)
# System: 3 atoms (H2 O)
#   box:      10 × 10 × 10 Å, orthogonal, periodic x y z
#   columns:  positions (3), element
#   info:     energy = -14.2
water["positions"]  # (3, 3) NumPy array of the positions, read-only
water.table  # the per-atom table, a polars DataFrame

A system never changes. Methods such as with_columns, filter and sort return a new system and leave the old one as it is; see Changing a system and Selecting and ordering atoms.

Creating a system

Name Type Default Unit Meaning
positions (N, 3) float64 Å Positions. Give either positions or table.
box Box or lattice vectors non-periodic box around the atoms Å Simulation box; a number or three numbers give an orthogonal periodic box.
table polars DataFrame Per-atom table with a positions column of 3 numbers per atom.
elements str or N str Chemical symbols; one symbol applies to all atoms.
types int32 or (N,) int32 Atom types, from 1; one value applies to all atoms.
info mapping {} Global information, for example energy (eV), virial (eV), timestep.
topology Topology None Bonds, angles, dihedrals and impropers, see Topology.

From positions, as above, or from a table that already holds the columns:

table = pl.DataFrame(
    {
        "positions": pl.Series(
            [[0.0, 0.0, 0.0], [1.8, 1.8, 0.0]], dtype=pl.Array(pl.Float64, 3)
        ),
        "element": ["Cu", "Ni"],
        "velocities": [[0.1, 0.0, 0.0], [-0.1, 0.0, 0.0]],
    }
)
pair = mp.System(table=table, box=mp.Box(3.6))

Most systems come from files with mdapy.io.read or from the structure builders, which return them the same way.

The arrays given to a system are copied, so changing them afterwards does not change the system. A box that is not given encloses the atoms and is not periodic.

Columns

The table holds one row per atom and any number of columns. Columns with a standard name have a fixed type. They are converted to it whenever they are set, when a system is made and by with_columns and with_table, so the rest of MDAPY can rely on it:

Column Type Unit Meaning
positions 3 float64 Å positions, required
velocities 3 float64 Å/ps velocities
forces 3 float64 eV/Å forces
element element chemical symbol
type int32 atom type, from 1
id int64 atom identifier
molecule int64 molecule identifier
mass float64 amu mass
charge float64 e charge
image 3 int32 periodic image flags
movable (3,) bool whether the atom may move along x, y and z

A conversion that would change a value raises an error. Small int64 image flags become int32, and 1.0 becomes type 1, but 1.5 as a type, 2**40 as an image flag, or 2 in movable do not fit, and neither does the wrong number of values per atom. Standard columns must not have missing values, positions, velocities, forces, masses and charges must be finite, masses positive and types at least 1. Every other column is kept as it is: numbers, booleans, text, and arrays of numbers such as the ptm_orientation of polyhedral template matching. The Glossary lists the columns that MDAPY itself writes.

The element column holds chemical symbols; underneath they are atomic numbers, so elements compare as text and sort by atomic number:

water.table["element"].to_list()  # ['O', 'H', 'H']
water.table["element"].to_physical().to_list()  # [8, 1, 1]

Reading data

Name Meaning
system["name"] The values of a column, a component or an expression, one per atom, as a read-only NumPy array; system["positions"] are the (N, 3) positions in Å, without a copy.
table The per-atom table as a polars DataFrame.
box The Box.
info Global information, a read-only mapping.
topology The Topology, or None.
len(system) The number of atoms.

Values by name

system["name"] gives the values of each atom as a NumPy array. The name is a column, a component of an array column written column.component, or an expression over the columns, written as in the conditions of filter:

from mdapy.build import build_crystal

alloy = build_crystal("fcc", 3.6, elements="Cu", repeat=(3, 3, 3))
alloy = alloy.with_columns(
    element=np.where(np.arange(len(alloy)) % 4 == 0, "Ni", "Cu"),
    charge=np.linspace(-1.0, 1.0, len(alloy)),
)
alloy["charge"]  # (108,) float64
alloy["positions.z"].max()  # the largest z
alloy["positions"]  # (108, 3)
alloy["sqrt(positions.x**2 + positions.y**2)"]  # distance from the z axis
alloy["element"]  # ['Ni', 'Cu', 'Cu', ...]
Name Example Gives
a column "charge", "element", "positions" its values, (N,) or (N, k) for an array column
a component "positions.z", "velocities.x", "group.1" one component, (N,)
an expression "positions.x**2 + positions.y**2", "charge > 0" the value of each atom, (N,)

Components are named as in conditions: x, y, z for 3 values, x, y, z, w for 4, xx to zz by rows for 9, and the index from 0 for any number. A column or a component is read where it is, without a copy, for numbers without missing values; an expression is computed. The arrays are read-only, so they cannot change the system.

Every column is read this way, the positions too: a system has no attribute for a column, so that each is read and changed the same way, by system["name"] and with_columns. A system is indexed only by names: atoms are chosen with filter, gather and sort, and a system is not a sequence of atoms to loop over.

The table

Everything else is read from the table, a polars DataFrame on which every polars operation works, for example counts and averages by element:

alloy.table.group_by("element").agg(pl.len(), pl.col("charge").mean())
alloy.table["positions"].arr.get(2).max()  # the largest z, as polars writes it

The table is a copy of the system's own: changing it does not change the system. To make a changed system, see Changing a system.

Printing a system

print(system) gives a summary of a few lines:

System: 108 atoms (Cu81 Ni27)
  box:      10.8 × 10.8 × 10.8 Å, orthogonal, periodic x y z
  columns:  positions (3), element, charge
  info:     energy = -3.54, timestep = 1000
  topology: 2 bonds, 1 angle
  • the number of atoms and the elements, the most frequent first;
  • the box: its lengths for an orthogonal box, with its origin when that is not zero, or for a triclinic box the lengths of a, b, c and the angles between them; and its periodic directions;
  • the columns, with the number of components of array columns in parentheses;
  • the global information, if any: numbers and short text with their values, arrays with their shape;
  • the topology, if any: the number of terms of each kind.

Errors

The constructor raises InvalidParameter for inconsistent input: neither or both of positions and table, positions not of shape (N, 3) or not finite, unknown chemical symbols, types below 1, missing values in standard columns, values that do not fit the type of a standard column, or topology indices beyond the number of atoms.