diff --git a/.gitignore b/.gitignore
index 7a23598..670289c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ __pycache__
build
*egg-info
env
+doc/source/generated
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9107878..8e10193 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,4 +1,5 @@
ci:
+ autofix_prs: false
autoupdate_schedule: quarterly
repos:
diff --git a/.readthedocs.yml b/.readthedocs.yml
index bf974f1..ffc6124 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -3,7 +3,7 @@ version: 2
build:
os: ubuntu-24.04
tools:
- python: "3.9"
+ python: "3.13"
sphinx:
configuration: doc/source/conf.py
diff --git a/LICENSE b/LICENSE
index 4c295f6..14a0229 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2021-2024, Auburn University
+Copyright (c) 2021-2025, Auburn University
All rights reserved.
diff --git a/README.md b/README.md
index ae9c2da..c590439 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,10 @@
[](https://pypi.org/project/lammpsio)
[](https://anaconda.org/conda-forge/lammpsio)
-Tools for working with LAMMPS data and dump files.
+lammpsio provides a Python interface for reading and writing LAMMPS
+data and dump files. It also enables interconversion with HOOMD-blue's
+GSD format. This flexible package enables users to work with LAMMPS files
+in a variety of ways that improve workflow efficiency and reproducibility.
`lammpsio` is a pure Python package that can be installed using `pip`:
@@ -13,160 +16,3 @@ Tools for working with LAMMPS data and dump files.
or `conda`:
conda install -c conda-forge lammpsio
-
-## Snapshot
-
-The particle configuration is stored in a `Snapshot`. A `Snapshot` holds the
-data for *N* particles, the simulation `Box`, and the timestep. The `Box` follows
-the LAMMPS conventions for its shape and bounds. Here is a 3-particle
-configuration in an orthorhombic box centered at the origin at step 100:
-
- box = lammpsio.Box((-2,-3,-4), (2,3,4))
- snapshot = lammpsio.Snapshot(3, box, step=100)
-
-These constructor arguments are available as attributes:
-
-- `N`: number of particles (int)
-- `box`: bounding box (`Box`)
-- `step`: timestep counter (int)
-- `num_types`: number of particle types (int). If `num_types is None`, then the number of types is deduced from `typeid`.
-
-The data contained in a `Snapshot` per particle is:
-
-- `id`: (*N*,) array atom IDs (dtype: `int`, default: runs from 1 to *N*)
-- `position`: (*N*,3) array of coordinates (dtype: `float`, default: `(0,0,0)`)
-- `image`: (*N*,3) array of periodic image indexes (dtype: `int`, default: `(0,0,0)`)
-- `velocity`: (*N*,3) array of velocities (dtype: `float`, default: `(0,0,0)`)
-- `molecule`: (*N*,) array of molecule indexes (dtype: `int`, default: `0`)
-- `typeid`: (*N*,) array of type indexes (dtype: `int`, default: `1`)
-- `mass`: (*N*,) array of masses (dtype: `float`, default: `1`)
-- `charge`: (*N*,) array of charges (dtype: `float`, default: `0`)
-
-The optional topology data is:
-
-- `type_label`: Labels of particle typeids. (`LabelMap`, default: `None`)
-- `bonds`: Bond data (`Bonds`, default: `None`)
-- `angles`: Angle data (`Angles`, default: `None`)
-- `dihedrals`: Dihedral data (`Dihedrals`, default: `None`)
-- `impropers`: Improper data (`Impropers`, default: `None`)
-
-All values of indexes will follow the LAMMPS 1-indexed convention, but the
-arrays themselves are 0-indexed.
-
-The `Snapshot` will lazily initialize these per-particle arrays as they are
-accessed to save memory. Hence, accessing a per-particle property will allocate
-it to default values. If you want to check if an attribute has been set, use the
-corresponding `has_` method instead (e.g., `has_position()`):
-
- snapshot.position = [[0,0,0],[1,-1,1],[1.5,2.5,-3.5]]
- snapshot.typeid[2] = 2
- if not snapshot.has_mass():
- snapshot.mass = [2.,2.,10.]
-
-## Topology
-
-The topology (bond information) can be stored in `Bonds`, `Angles`, `Dihedrals`,
-and `Impropers` objects. All these objects function similarly, differing only in the
-number of particles that are included in a connection (2 for a bond, 3 for an angle,
-4 for a dihedral or improper). Each connection has an associated `id` and `typeid`.
-
-```py
-bonds = Bonds(N=3, num_types=2)
-angles = Angles(N=2, num_types=1)
-```
-These constructor arguments are available as attributes:
-
-- `N`: number of connections (int)
-- `num_types`: number of connection types (int). If `num_types is None`, then the number of types is deduced from `typeid`.
-
-The data contained per connection is:
-- `members`: (*N*, *M*) array of particles IDs in each topology (dtype: `int`, default: `1`),
-where *M* is the number of particles in a connection.
-- `id`: (*N*,) array topology IDs (dtype: `int`, default: runs from 1 to *N*)
-- `typeid`: (*N*,) array of type indexes (dtype: `int`, default: `1`)
-
-A label (type) can be associated with a connection's typeid using a `type_label`.
-- `type_label`: Labels of connection typeids. (`LabelMap`, default: `None`)
-
-All values of indexes will follow the LAMMPS 1-indexed convention, but the
-arrays themselves are 0-indexed. Lazy array initialization is used as for the `Snapshot`.
-
-## Label maps
-
-A `LabelMap` is effectively a dictionary associating a label (type) with a particle's
-or connection's typeid. These labels can be useful for tracking the meaning of
-typeids. They are also automatically used when interconverting with
-HOOMD GSD files that require such labels.
-
-The keys of the `LabelMap` are the typeids, and the values are the labels. A
-`LabelMap` additionally has the following attributes:
-
-- `types`: Types in label map. (dtype: `tuple`, default: `()`)
-- `typeids`: Typeids in label map. (dtype: `tuple`, default: `()`)
-
-## Data files
-
-A LAMMPS data file is represented by a `DataFile`. The file must be explicitly
-`read()` to get a `Snapshot`:
-
- f = lammpsio.DataFile("config.data")
- snapshot = f.read()
-
-The `atom_style` will be read from the comment in the Atoms section
-of the file. If it is not present, it must be specified in the `DataFile`.
-If `atom_style` is specified and also present in the file, the two must match
-or an error will be raised.
-
-There are many sections that can be stored in a data file, but `lammpsio` does
-not currently understand all of them. You can check `DataFile.known_headers`,
-`DataFile.unknown_headers`, `DataFile.known_bodies` and `DataFile.unknown_bodies`
-for lists of what is currently supported.
-
-A `Snapshot` can be written using the `create()` method:
-
- f = lammpsio.DataFile.create("config2.data", snapshot)
-
-A `DataFile` corresponding to the new file is returned by `create()`.
-
-## Dump files
-
-A LAMMPS dump file is represented by a `DumpFile`. The actual file format is
-very flexible, but by default embeds a schema that can be read:
-
- traj = lammpsio.DumpFile(filename="atoms.lammpstrj")
-
-If the schema does not exist for some reason, it can be manually specified as
-a dictionary. Valid keys for the schema match the names and shapes in the
-`Snapshot`. The keys requiring only 1 column index are: `id`, `typeid`,
-`molecule`, `charge`, and `mass`. The keys requiring 3 column indexes are
-`position`, `velocity`, and `image`.
-
-LAMMPS will dump particles in an unknown order unless you have used the
-`dump_modify sort` option. If you want particles to be ordered by `id` in the
-`Snapshot`, use `sort_ids=True` (default).
-
-A `DumpFile` is iterable, so you can use it to go through all the snapshots
-of a trajectory:
-
- for snap in traj:
- print(snap.step)
-
-You can also get the number of snapshots in the `DumpFile`, but this does
-require reading the entire file: so use with caution!
-
- num_frames = len(traj)
-
-Random access to snapshots is not currently implemented, but it may be added
-in future. If you want to randomly access snapshots, you should load the
-whole file into a list:
-
- snaps = [snap for snap in traj]
- print(snaps[3].step)
-
-Keep in the mind that the memory requirements for this can be huge!
-
-A `DumpFile` can be created from a list of snapshots:
-
- t = lammpsio.DumpFile.create("atoms.lammpstrj", schema, snaps)
-
-The object representing the new file is returned and can be used.
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 0000000..03a7aae
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,40 @@
+import numpy
+
+try:
+ import sybil
+ import sybil.parsers.rest
+
+except ImportError:
+ sybil = None
+
+import lammpsio
+
+try:
+ import gsd.hoomd
+
+ has_gsd = True
+except ModuleNotFoundError:
+ has_gsd = False
+
+
+def setup_sybil_tests(namespace):
+ """Sybil setup function."""
+ # Common imports.
+ namespace["numpy"] = numpy
+ namespace["lammpsio"] = lammpsio
+ if has_gsd:
+ namespace["frame"] = gsd.hoomd.Frame()
+ else:
+ namespace["frame"] = 0
+
+
+if sybil is not None:
+ pytest_collect_file = sybil.Sybil(
+ parsers=[
+ sybil.parsers.rest.PythonCodeBlockParser(),
+ sybil.parsers.rest.SkipParser(),
+ ],
+ pattern="*.py",
+ setup=setup_sybil_tests,
+ fixtures=["tmp_path"],
+ ).pytest()
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 1b2ca1d..ceca0a2 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,7 +1,7 @@
furo
-ipython==8.10.0
+ipython==9.3.0
MyST-Parser
-nbsphinx==0.8.12
-sphinx==6.1.3
-sphinx_design==0.4.1
+nbsphinx==0.9.7
+sphinx==8.1.3
+sphinx_design==0.6.1
sphinx_favicon==1.0.1
diff --git a/doc/source/_images/lammpsio_logo.svg b/doc/source/_images/lammpsio_logo.svg
new file mode 100644
index 0000000..7ae1d73
--- /dev/null
+++ b/doc/source/_images/lammpsio_logo.svg
@@ -0,0 +1,66 @@
+
+
+
diff --git a/doc/source/_templates/autosummary/class.rst b/doc/source/_templates/autosummary/class.rst
new file mode 100644
index 0000000..25ca004
--- /dev/null
+++ b/doc/source/_templates/autosummary/class.rst
@@ -0,0 +1,29 @@
+{{ fullname | escape | underline}}
+
+.. currentmodule:: {{ module }}
+
+.. autoclass:: {{ objname }}
+
+ {% block methods %}
+ {% if methods %}
+ .. rubric:: {{ _('Methods:') }}
+
+ .. autosummary::
+ {% for item in methods %}
+ {%- if not item in ['__init__'] %}
+ ~{{ name }}.{{ item }}
+ {%- endif -%}
+ {%- endfor %}
+ {% endif %}
+ {% endblock %}
+
+ {% block attributes %}
+ {% if attributes %}
+ .. rubric:: {{ _('Attributes:') }}
+
+ .. autosummary::
+ {% for item in attributes %}
+ ~{{ name }}.{{ item }}
+ {%- endfor %}
+ {% endif %}
+ {% endblock %}
diff --git a/doc/source/api.rst b/doc/source/api.rst
new file mode 100644
index 0000000..114248f
--- /dev/null
+++ b/doc/source/api.rst
@@ -0,0 +1,40 @@
+API
+---
+
+Particle data
+=============
+
+.. autosummary::
+ :nosignatures:
+ :toctree: generated/
+
+ lammpsio.Box
+ lammpsio.Snapshot
+ lammpsio.LabelMap
+
+Topology
+========
+
+The topology (bond information) can be stored in `Bonds`, `Angles`, `Dihedrals`,
+and `Impropers` objects. All these objects function similarly, differing only in
+the number of particles that are included in a connection (2 for a bond, 3 for
+an angle, 4 for a dihedral or improper).
+
+.. autosummary::
+ :nosignatures:
+ :toctree: generated/
+
+ lammpsio.Angles
+ lammpsio.Bonds
+ lammpsio.Dihedrals
+ lammpsio.Impropers
+
+File formats
+============
+
+.. autosummary::
+ :nosignatures:
+ :toctree: generated/
+
+ lammpsio.DataFile
+ lammpsio.DumpFile
diff --git a/doc/source/api/box.rst b/doc/source/api/box.rst
deleted file mode 100644
index e4b0c3e..0000000
--- a/doc/source/api/box.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-box
----
-
-.. automodule:: lammpsio.box
- :members:
- :show-inheritance:
diff --git a/doc/source/api/data.rst b/doc/source/api/data.rst
deleted file mode 100644
index 97b526c..0000000
--- a/doc/source/api/data.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-data
-----
-
-.. automodule:: lammpsio.data
- :members:
- :show-inheritance:
diff --git a/doc/source/api/dump.rst b/doc/source/api/dump.rst
deleted file mode 100644
index dc0874f..0000000
--- a/doc/source/api/dump.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-dump
-----
-
-.. automodule:: lammpsio.dump
- :members:
- :show-inheritance:
diff --git a/doc/source/api/snapshot.rst b/doc/source/api/snapshot.rst
deleted file mode 100644
index 09dcd73..0000000
--- a/doc/source/api/snapshot.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-snapshot
---------
-
-.. automodule:: lammpsio.snapshot
- :members:
- :show-inheritance:
diff --git a/doc/source/api/topology.rst b/doc/source/api/topology.rst
deleted file mode 100644
index 7e427db..0000000
--- a/doc/source/api/topology.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-topology
---------
-
-.. automodule:: lammpsio.topology
- :members:
- :show-inheritance:
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 49d2741..c12dd34 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -13,7 +13,7 @@
# -- Project information -----------------------------------------------------
-project = "lammmpsio"
+project = "lammpsio"
year = datetime.date.today().year
copyright = f"2021-{year}, Auburn University"
author = "Michael P. Howard"
@@ -60,11 +60,14 @@
"color-admonition-background": "#e86100",
},
}
+html_logo = "_images/lammpsio_logo.svg"
# -- Options for autodoc & autosummary ---------------------------------------
autosummary_generate = True
+autodoc_member_order = "bysource"
+
autodoc_default_options = {"inherited-members": None, "special-members": False}
# -- Options for intersphinx -------------------------------------------------
diff --git a/doc/source/guide/credits.rst b/doc/source/credits.rst
similarity index 82%
rename from doc/source/guide/credits.rst
rename to doc/source/credits.rst
index 166d5bb..01ec5b0 100644
--- a/doc/source/guide/credits.rst
+++ b/doc/source/credits.rst
@@ -5,4 +5,4 @@ Credits
* Michael P. Howard
* Mayukh Kundu
* Philipp Leclercq
-* C. Levi Petix
+* C\. Levi Petix
diff --git a/doc/source/guide/examples/index.rst b/doc/source/guide/examples/index.rst
deleted file mode 100644
index b90c0e1..0000000
--- a/doc/source/guide/examples/index.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-========
-Examples
-========
diff --git a/doc/source/guide/release.md b/doc/source/guide/release.md
deleted file mode 100644
index 14e66a7..0000000
--- a/doc/source/guide/release.md
+++ /dev/null
@@ -1,2 +0,0 @@
-```{include} ../../../CHANGELOG.md
-```
diff --git a/doc/source/guidelines.rst b/doc/source/guidelines.rst
new file mode 100644
index 0000000..f559727
--- /dev/null
+++ b/doc/source/guidelines.rst
@@ -0,0 +1,30 @@
+====================
+Community Guidelines
+====================
+
+We ask that you please review and adhere to our `Code of Conduct`_.
+
+
+Reporting Issues
+================
+
+If you encounter any bugs or issues while using ``lammpsio``, please report them
+on our `GitHub Issues page`_.
+
+Contributing
+=============
+
+We welcome contributions to ``lammpsio`` via GitHub pull requests. We ask that
+you please open an issue first to discuss your proposed changes before
+submitting a pull request. This helps us to understand the context of your
+changes and to ensure that they fit with the existing codebase.
+
+Seeking Help
+============
+
+If you have questions or need help using ``lammpsio``, please feel free to reach
+out to us via our `GitHub Discussions page`_.
+
+.. _Code of Conduct: http://github.com/mphowardlab/lammpsio/blob/main/CODE_OF_CONDUCT.md
+.. _GitHub Issues page: http://github.com/mphowardlab/lammpsio/issues
+.. _GitHub Discussions page: http://github.com/mphowardlab/lammpsio/discussions
diff --git a/doc/source/index.rst b/doc/source/index.rst
index db5c178..a5e7514 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -1,25 +1,30 @@
lammpsio documentation
======================
-Tools for working with LAMMPS data and dump files.
+lammpsio provides a Python interface for reading and writing LAMMPS
+data and dump files. It also enables interconversion with HOOMD-blue's
+GSD format. This flexible package enables users to work with LAMMPS files
+in a variety of ways that improve workflow efficiency and reproducibility.
.. toctree::
:maxdepth: 1
- :caption: User guide
+ :caption: Getting started
- guide/install
- guide/examples/index
- guide/release
- guide/license
- guide/credits
+ ./install
+ ./release
+ ./guidelines
.. toctree::
:maxdepth: 1
- :caption: API reference
+ :caption: Reference
- api/box
- api/data
- api/dump
- api/snapshot
- api/topology
+ ./api
+ ./tutorials
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Additional information
+
+ ./credits
+ ./license
diff --git a/doc/source/guide/install.rst b/doc/source/install.rst
similarity index 83%
rename from doc/source/guide/install.rst
rename to doc/source/install.rst
index e290b82..c6b1f18 100644
--- a/doc/source/guide/install.rst
+++ b/doc/source/install.rst
@@ -2,12 +2,17 @@
Installation
============
-The easiest way to get ``lammpsio`` is from PyPI using ``pip``:
+The easiest way to get ``lammpsio`` is from PyPI:
.. code:: bash
pip install lammpsio
+or conda-forge:
+
+.. code:: bash
+
+ conda install -c conda-forge lammpsio
Building from source
====================
diff --git a/doc/source/guide/license.rst b/doc/source/license.rst
similarity index 54%
rename from doc/source/guide/license.rst
rename to doc/source/license.rst
index f6a6f5a..ffd9466 100644
--- a/doc/source/guide/license.rst
+++ b/doc/source/license.rst
@@ -2,5 +2,5 @@
License
=======
-.. literalinclude:: ../../../LICENSE
+.. literalinclude:: ../../LICENSE
:language: none
diff --git a/doc/source/release.md b/doc/source/release.md
new file mode 100644
index 0000000..3139cd4
--- /dev/null
+++ b/doc/source/release.md
@@ -0,0 +1,2 @@
+```{include} ../../CHANGELOG.md
+```
diff --git a/doc/source/tutorials.rst b/doc/source/tutorials.rst
new file mode 100644
index 0000000..75decd3
--- /dev/null
+++ b/doc/source/tutorials.rst
@@ -0,0 +1,11 @@
+Tutorials
+---------
+
+Overview
+========
+
+
+.. toctree::
+ :maxdepth: 1
+
+ ./tutorials/dimer_lattice_tutorial
diff --git a/doc/source/tutorials/dimer_lattice_tutorial.ipynb b/doc/source/tutorials/dimer_lattice_tutorial.ipynb
new file mode 100644
index 0000000..446607d
--- /dev/null
+++ b/doc/source/tutorials/dimer_lattice_tutorial.ipynb
@@ -0,0 +1,361 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "56c2719c",
+ "metadata": {},
+ "source": [
+ "# Packing a Dimer on a Cubic Lattice\n",
+ "\n",
+ "In this tutorial, we’ll walk through filling a box with dimers using `lammpsio`.\n",
+ "We'll create a simple cubic lattice where each unit cell contains one dimer\n",
+ "made up of two particle (one of type A and one of type B) that are bonded to\n",
+ "each other. At the end, we will create a data file ready to be used by LAMMPS. \n",
+ "\n",
+ "First, we import `numpy` for making the simple cubic lattice and `lammpsio` to\n",
+ "handle creating the data file."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 37,
+ "id": "2ab58341",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import lammpsio\n",
+ "import numpy"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "82d4bea0",
+ "metadata": {},
+ "source": [
+ "## Creating the lattice\n",
+ "\n",
+ "We define the core parameters of our system, including particle diameters,\n",
+ "offset between bonded particles, and the number of repetitions of the unit cell.\n",
+ "We choose the particles to have a unit diameter $d$ and a bond length of $\\ell =\n",
+ "1.5 d$. We chose a lattice spacing of $2 d + \\ell$ so that each dimer is\n",
+ "separated by one diameter. We also choose to place 1000 dimers total (10 in each\n",
+ "direction)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 38,
+ "id": "cd4c3e84",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "diameter = 1.0\n",
+ "bond_length = 1.5\n",
+ "lattice_spacing = 2 * diameter + bond_length\n",
+ "num_repeat = [10, 10, 10]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a93991ec",
+ "metadata": {},
+ "source": [
+ "### Define the unit cell\n",
+ "\n",
+ "The unit cell is a volume containing particles that can be copied along the\n",
+ "vectors that define it in order to fill space. Our unit cell is a cube, so its\n",
+ "vectors are the 3 Cartesian axes, scaled by the lattice spacing. Each unit cell\n",
+ "contains two particles: type A at the origin and type B shifted along the *x*\n",
+ "axis to give the proper spacing. We also specify the type IDs (A is 1, B is 2)\n",
+ "and masses of the particles in the unit cell."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "id": "3ddbcaf9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "unit_cell_vectors = numpy.array([\n",
+ " [1, 0, 0],\n",
+ " [0, 1, 0],\n",
+ " [0, 0, 1]\n",
+ "]) * lattice_spacing\n",
+ "unit_cell_coords = numpy.array([\n",
+ " [0, 0, 0],\n",
+ " [bond_length, 0, 0]\n",
+ "])\n",
+ "unit_cell_typeids = [1, 2]\n",
+ "unit_cell_mass = [1.0, 1.5]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8ca906a5",
+ "metadata": {},
+ "source": [
+ "### Define the box\n",
+ "\n",
+ "The simulation box is the volume obtained by repeating the unit cell the desired\n",
+ "number of times. First, we repeat each vector by the number of repeats we\n",
+ "specified. Then, we transpose these vectors to form the matrix\n",
+ "[**a** **b** **c**] that defines a LAMMPS box. We use the `from_matrix` method to \n",
+ "create our box and choose to put the lower corner of the box at the origin `[0, 0, 0]`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
+ "id": "b79ec025",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "box_matrix = (unit_cell_vectors * num_repeat).T\n",
+ "box = lammpsio.Box.from_matrix(low=[0, 0,0], matrix=box_matrix)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f1371cbc",
+ "metadata": {},
+ "source": [
+ "## Creating the snapshot\n",
+ "\n",
+ "The `Snapshot` holds the data about the particle's configuration and properties,\n",
+ "the `Box`, and the timestep. First, we calculate the total number of particles\n",
+ "by multiplying the number of unit cells by the number of particles per cell."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "id": "a9358a71",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "num_cells = numpy.prod(num_repeat)\n",
+ "num_per_unit_cell = unit_cell_coords.shape[0]\n",
+ "snap = lammpsio.Snapshot(N=num_cells * num_per_unit_cell, box=box)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "48ea8e9b",
+ "metadata": {},
+ "source": [
+ "We then generate positions for all particles by iterating through each unit\n",
+ "cell in our lattice. The origin of the lattice (relative to the origin of the\n",
+ "box) is calculated and used to shift the unit cell coordinates. Finally, the\n",
+ "origin of the box is added to give the final particle positions.\n",
+ "\n",
+ "Note that `lammpsio` automatically allocates an array with the right data type\n",
+ "and shape for particle data, so particle data can be assigned directly to the\n",
+ "snapshot rather than using an intermediate array!"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 42,
+ "id": "1dbd74b0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "for i, unit_cell_idx in enumerate(numpy.ndindex(*num_repeat)):\n",
+ " first = i * num_per_unit_cell\n",
+ " last = first + num_per_unit_cell\n",
+ " snap.position[first:last] = (\n",
+ " numpy.array(unit_cell_idx) * lattice_spacing + unit_cell_coords\n",
+ " )\n",
+ "snap.position += snap.box.low"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5d91b59f",
+ "metadata": {},
+ "source": [
+ "We then create an array of type IDs by replicating our unit cell type IDs. We do\n",
+ "the same thing to give the particles their masses. \n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "id": "cecf14cb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "snap.typeid = numpy.tile(unit_cell_typeids, num_cells)\n",
+ "snap.mass = numpy.tile(unit_cell_mass, num_cells)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "46ac8215",
+ "metadata": {},
+ "source": [
+ "To specify the bond for each dimer, we also need to add a `Bonds` object to the\n",
+ "snapshot. We know that each cell contains one dimer and thus one bond. Since all\n",
+ "of our bonds in this system are the same, we assign them all type ID 1."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 44,
+ "id": "3ce97e21",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "snap.bonds = lammpsio.Bonds(N=num_cells, num_types=1)\n",
+ "snap.bonds.typeid = numpy.ones(snap.bonds.N)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dbe9bda1",
+ "metadata": {},
+ "source": [
+ "Each dimer consists of consecutive particle IDs (1-2, 3-4, etc.). We create\n",
+ "bonds by connecting these consecutive pairs."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 45,
+ "id": "ae6ecebf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "snap.bonds.members = [[2 * i + 1, 2 * i + 2] for i in range(snap.bonds.N)]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1112f225",
+ "metadata": {},
+ "source": [
+ "## Save to LAMMPS data file"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "09b878bd",
+ "metadata": {},
+ "source": [
+ "Finally, we save the configuration to a data file ready to be used in LAMMPS.\n",
+ "The data file is written in the molecular style since we have bonds. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ada7ffb6",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 46,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "lammpsio.DataFile.create(\n",
+ " filename=\"dimer_lattice.data\", snapshot=snap, atom_style=\"molecular\"\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "567bb70a",
+ "metadata": {},
+ "source": [
+ "## Save to HOOMD-blue GSD file"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5fdc51d0",
+ "metadata": {},
+ "source": [
+ "HOOMD-blue is another molecular simulation tool whose user community overlaps\n",
+ "with LAMMPS. There may come a time when you want to use HOOMD-blue or share your\n",
+ "LAMMPS data file with someone who is more familar with it. Manually converting a\n",
+ "LAMMPS data file to HOOMD-blue’s GSD format can be tedious — for example,\n",
+ "HOOMD-blue requires the box to be centered at `[0, 0, 0]`. Luckily, `lammpsio`\n",
+ "automatically handles this coordinate transformation and other format\n",
+ "differences for you!\n",
+ "\n",
+ "HOOMD-blue requires alphanumeric type names along with type IDs, so we have to\n",
+ "add those to our snapshot using a `LabelMap`. By default, if you do not specify\n",
+ "a `LabelMap`, `lammpsio` will convert type IDs to string types in the GSD file\n",
+ "(e.g., typeID `1` becomes `\"1\"`). Here, we're going to explicitly map particle\n",
+ "typeID `1 -> A` & `2 -> B` and bond typeID `1 -> dimer`. `lammpsio` will use the\n",
+ "`LabelMap` to set the alphanumeric types in the GSD file.\n",
+ "\n",
+ "Note: LAMMPS now also supports alphanumeric type labeling, but `lammpsio` \n",
+ "does not currently support for this feature. It is planned as a future addition."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 47,
+ "id": "890f5b1f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "snap.type_label = lammpsio.LabelMap({1: \"A\", 2: \"B\"})\n",
+ "snap.bonds.type_label = lammpsio.LabelMap({1: \"dimer\"})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "01b31b86",
+ "metadata": {},
+ "source": [
+ " We can write this out to file and have the same particle and bond data ready to \n",
+ "be used in a different simulation engine! You can similarly use\n",
+ "`Snapshot.from_hoomd_gsd` to convert a HOOMD-blue GSD frame into a `Snapshot`! "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "284768f5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import gsd.hoomd\n",
+ "\n",
+ "with gsd.hoomd.open(\"dimer_lattice.gsd\", \"w\") as f:\n",
+ " f.append(snap.to_hoomd_gsd())"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "lammpsio-dev",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/src/lammpsio/box.py b/src/lammpsio/box.py
index 3965c74..3a99d0a 100644
--- a/src/lammpsio/box.py
+++ b/src/lammpsio/box.py
@@ -4,22 +4,51 @@
class Box:
- """Triclinic simulation box.
+ r"""Simulation box.
- The convention for defining the bounds of the box is based on
- `LAMMPS `_. This
- means that the lower corner of the box is placed at ``low``, and
- the size and shape of the box is determined by ``high`` and ``tilt``.
+ In LAMMPS, the simulation box is specified by three parameters: `low`,
+ `high`, and `tilt`. `low` defines the origin (lower corner) of the box,
+ while `high` specifies how far the box extends along each axis. The
+ difference between `high` and `low` gives three lengths $L_x$, $L_y$, and
+ $L_z$. `tilt` has three factors ($L_{xy}$, $L_{xz}$, $L_{yz}$) that skew the
+ edges to create non-orthorhombic simulation boxes. These parameters define
+ a box matrix consisting of three lattice vectors **a**, **b**, and **c**:
+
+ .. math::
+
+ [\mathbf{a} \quad \mathbf{b} \quad \mathbf{c} ] =
+ \begin{bmatrix}
+ L_x & L_{xy} & L_{xz} \\
+ 0 & L_y & L_{yz} \\
+ 0 & 0 & L_z
+ \end{bmatrix}
+
+ For more details on how to convert between the LAMMPS parameters and box
+ matrix see the `LAMMPS documentation
+ `_.
+
+ .. warning::
+
+ `high` is the upper bound of the simulation box **only** when it is
+ orthorhombic.
Parameters
----------
low : list
Origin of the box
high : list
- "High" of the box, used to compute edge lengths.
+ High parameter used to compute edge lengths.
tilt : list
- Tilt factors ``xy``, ``xz``, and ``yz`` for a triclinic box.
- Default of ``None`` is a strictly orthorhombic box.
+ Tilt factors ``xy``, ``xz``, and ``yz`` for a triclinic box. Default of
+ ``None`` is a strictly orthorhombic box, implying all are zero.
+
+ Examples
+ --------
+ Construct a triclinic simulation box:
+
+ .. code-block:: python
+
+ box = lammpsio.Box([-5.0, -10.0, 0.0], [1.0, 10.0, 8.0], [1.0, -2.0, 0.5])
"""
@@ -30,7 +59,7 @@ def __init__(self, low, high, tilt=None):
@classmethod
def cast(cls, value):
- """Cast an array to a :class:`Box`.
+ """Cast from an array.
If ``value`` has 6 elements, it is unpacked as an orthorhombic box::
@@ -47,8 +76,16 @@ def cast(cls, value):
Returns
-------
- :class:`Box`
- A simulation box matching the array.
+ `Box`
+ A simulation box.
+
+ Examples
+ --------
+ Construct an orthorhombic simulation box by casting an array:
+
+ .. code-block:: python
+
+ box = lammpsio.Box.cast([-5.0, -10.0, 0.0, 1.0, 10.0, 8.0])
"""
if isinstance(value, Box):
@@ -65,36 +102,23 @@ def cast(cls, value):
@classmethod
def from_matrix(cls, low, matrix, force_triclinic=False):
- """Create a Box from low and matrix.
+ """Cast from an origin and matrix.
Parameters
----------
low : list
Origin of the box.
- matrix : :class:`numpy.ndarray`
- Upper triangular matrix in LAMMPS style::
-
- [[lx, xy, xz],
- [0, ly, yz],
- [0, 0, lz]]
+ matrix : `numpy.ndarray`
+ Box matrix.
force_triclinic : bool
If ``True``, forces the box to be triclinic even if the tilt
factors are zero. Default is ``False``.
Returns
-------
- :class:`Box`
+ `Box`
A simulation box.
- Raises
- ------
- TypeError
- If `low` is not length 3.
- TypeError
- If `matrix` is not a 3x3 array.
- ValueError
- If `matrix` is not upper triangular.
-
"""
low = numpy.array(low, dtype=float)
arr = numpy.array(matrix, dtype=float)
@@ -118,7 +142,10 @@ def from_matrix(cls, low, matrix, force_triclinic=False):
@property
def low(self):
- """:class:`numpy.ndarray`: Box low."""
+ """(3,) `numpy.ndarray` of `float`: Low parameter.
+
+ The low of the box is the origin.
+ """
return self._low
@low.setter
@@ -130,7 +157,11 @@ def low(self, value):
@property
def high(self):
- """:class:`numpy.ndarray`: Box high."""
+ """(3,) `numpy.ndarray` of `float`: High parameter.
+
+ The high of the box is used to compute the lengths $L_x$, $L_y$, and
+ $L_z$.
+ """
return self._high
@high.setter
@@ -142,7 +173,12 @@ def high(self, value):
@property
def tilt(self):
- """:class:`numpy.ndarray`: Box tilt factors."""
+ """(3,) `numpy.ndarray` of `float`: Tilt parameters.
+
+ The 3 tilt factors, $L_{xy}$, $L_{xz}$, and $L_{yz}$ define the
+ shape of the box. The default of ``None`` is strictly orthorhombic,
+ meaning all are zero.
+ """
return self._tilt
@tilt.setter
diff --git a/src/lammpsio/data.py b/src/lammpsio/data.py
index d2182fe..db227eb 100644
--- a/src/lammpsio/data.py
+++ b/src/lammpsio/data.py
@@ -16,13 +16,36 @@ def _readline(file_, require=False):
class DataFile:
"""LAMMPS data file.
+ LAMMPS can both read a data file for initialization and also write a data
+ file (e.g., for visualization or restart purposes). A `Snapshot` can be
+ written to a data file using `create`:
+
+ .. code-block:: python
+
+ box = lammpsio.Box([-5.0, -10.0, 0.0], [1.0, 10.0, 8.0], [1.0, -2.0, 0.5])
+
+ snap = lammpsio.Snapshot(3, box, 10)
+
+ data = lammpsio.DataFile.create(tmp_path / "atoms.data", snap)
+
+ A data file can also be read into a `Snapshot`:
+
+ .. code-block:: python
+
+ snap = data.read()
+
+ There are many sections that can be stored in a data file, but ``lammpsio``
+ does not currently understand all of them. You can check `known_headers`,
+ `unknown_headers`, `known_bodies` and `unknown_bodies` for lists of what is
+ currently supported.
+
Parameters
----------
filename : str
Path to data file.
atom_style : str
- Atom style to use for data file. Defaults to ``None``, which means the
- style should be read from the file.
+ LAMMPS atom style to use. Defaults to ``None``, which means the style
+ should be read from the file.
Attributes
----------
@@ -30,13 +53,13 @@ class DataFile:
Path to the file.
atom_style : str
Atom style for the file.
- known_headers : list
+ known_headers : list of str
Data file headers that can be processed.
- unknown_headers : list
+ unknown_headers : list of str
Data file headers that will be ignored.
- known_bodies : list
+ known_bodies : list of str
Data file body sections that can be processed.
- unknown_bodies : list
+ unknown_bodies : list of str
Data file body sections that will be ignored.
"""
@@ -119,7 +142,7 @@ def create(cls, filename, snapshot, atom_style=None):
----------
filename : str
Path to data file.
- snapshot : :class:`Snapshot`
+ snapshot : `Snapshot`
Snapshot to write to file.
atom_style : str
Atom style to use for data file. Defaults to ``None``, which means the
@@ -127,7 +150,7 @@ def create(cls, filename, snapshot, atom_style=None):
Returns
-------
- :class:`DataFile`
+ `DataFile`
The object representing the new data file.
Raises
@@ -324,20 +347,20 @@ def create(cls, filename, snapshot, atom_style=None):
return DataFile(filename)
def read(self):
- """Read the file.
+ """Read a LAMMPS data file into a snapshot.
+
+ The `atom_style` will be read from the comment in the Atoms section
+ of the file. If it is not present, it must be specified manually.
+ If `atom_style` is specified manually and also present in the file,
+ the two must match or an error will be raised.
+
+ Unknown headers and sections are silently ignored.
Returns
-------
- :class:`Snapshot`
+ `Snapshot`
Snapshot from the data file.
- Raises
- ------
- ValueError
- If :attr:`atom_style` is set but does not match file contents.
- ValueError
- If :attr:`atom_style` is not specified and not set in file.
-
"""
with open(self.filename) as f:
# initialize snapshot from header
diff --git a/src/lammpsio/dump.py b/src/lammpsio/dump.py
index 9800e90..1d82f01 100644
--- a/src/lammpsio/dump.py
+++ b/src/lammpsio/dump.py
@@ -16,30 +16,89 @@
class DumpFile:
"""LAMMPS dump file.
- The dump file is a flexible file format, so a ``schema`` can be given
- to parse the atom data. The ``schema`` is given as a dictionary of column
- indexes. Valid keys for the schema match the names and shapes in the `Snapshot`.
- The keys requiring only 1 column index are: ``id``, ``typeid``, ``molecule``,
- ``charge``, and ``mass``. The keys requiring 3 column indexes are ``position``,
- ``velocity``, and ``image``. If a ``schema`` is not specified, it will be deduced
- from the ``ITEM: ATOMS`` header.
-
- The vector-valued fields (``position``, ``velocity``, ``image``) must contain all
- three elements.
+ The LAMMPS dump file is a highly flexible file format. The ``schema`` of the
+ file is deduced from the ``ITEM: ATOMS`` header unless one is manually
+ specified. If specificed, the ``schema`` must be a dictionary mapping pieces
+ of data to column indexes (0-indexed). Valid keys for the schema match the
+ names and shapes in the `Snapshot`. The keys requiring only 1 column index
+ are:
+
+ - ``"id"``
+ - ``"typeid"``
+ - ``"molecule"``
+ - ``"charge"``
+ - ``"mass"``
+
+ The keys requiring 3 column indexes are:
+
+ - ``"position"``
+ - ``"velocity"``
+ - ``"image"``
+
+ LAMMPS dumps particles in an unknown order unless you have used the
+ ``dump_modify sort`` option. If you want particles to be ordered by ``id``
+ in the `Snapshot`, use ``sort_ids=True`` (default). Note that slightly
+ faster reading may be possible by setting this option to ``False``.
+
+ A `DumpFile` is iterable, so you can use it to go through all the snapshots
+ of a trajectory. Random access to snapshots is not currently implemented,
+ but it may be added in future. If you want to randomly access snapshots, you
+ should load the whole file into a list, but the memory requirements to do so
+ may be large.
+
+ The dump file may not contain certain information about your particles, for
+ example, topology, or you may choose not to write this information in the
+ dump file because it does not change frame-to-frame. The `copy_from` option
+ allows this information to be copied into a new snapshot from a reference
+ one, e.g, that was read from a `DataFile`.
Parameters
----------
filename : str
Path to dump file.
schema : dict
- Schema for the contents of the file. Defaults to ``None``, which means to read
- it from the file.
+ Schema for the contents of the file. Defaults to ``None``, which means
+ to read it from the file.
sort_ids : bool
- If true, sort the particles by ID in each snapshot.
- copy_from : :class:`Snapshot`
- If specified, copy fields that are missing in the dump file but are set in
- a reference :class:`Snapshot`. The fields that can be copied are ``typeid``,
- ``molecule``, ``charge``, and ``mass``.
+ If ``True``, sort the particles by ID in each snapshot.
+ copy_from : `Snapshot`
+ If specified, copy supported fields that are missing in the dump file
+ but are set in a reference `Snapshot`.
+
+ Example
+ -------
+ Create a dump file object:
+
+ .. code-block:: python
+
+ traj = lammpsio.DumpFile("atoms.lammpstrj")
+
+ Iterate snapshots:
+
+ .. skip: next
+
+ .. code-block:: python
+
+ for snapshot in traj:
+ print(snapshot.step)
+
+ You can also get the number of snapshots in the `DumpFile`, but this does
+ require reading the entire file: use with caution!
+
+ .. skip: next
+
+ .. code-block:: python
+
+ num_frames = len(traj)
+
+ Random access by creating a list:
+
+ .. skip: next
+
+ .. code-block:: python
+
+ snapshots = [snap for snap in traj]
+ print(snapshots[3].step)
"""
@@ -60,14 +119,24 @@ def create(cls, filename, schema, snapshots):
Path to dump file.
schema : dict
Schema for the contents of the file.
- snapshots : :class:`Snapshot` or list
+ snapshots : `Snapshot` or list
One or more snapshots to write to the dump file.
Returns
-------
- :class:`DumpFile`
+ `DumpFile`
The object representing the new dump file.
+ Example
+ -------
+ A `DumpFile` can be created from a list of snapshots:
+
+ .. skip: next
+
+ .. code-block:: python
+
+ lammpsio.DumpFile.create("atoms.lammpstrj", schema, snapshots)
+
"""
# map out the schema into a dump row
# each entry is a tuple: (column, (attribute, index))
@@ -184,6 +253,20 @@ def _compression_from_suffix(suffix):
@property
def copy_from(self):
+ """`Snapshot`: Copy fields that are missing in a dump file.
+
+ The fields that can be copied are:
+
+ - `Snapshot.angles`
+ - `Snapshot.bonds`
+ - `Snapshot.charge`
+ - `Snapshot.dihedrals`
+ - `Snapshot.impropers`
+ - `Snapshot.mass`
+ - `Snapshot.molecule`
+ - `Snapshot.typeid`
+
+ """
return self._copy_from
@copy_from.setter
diff --git a/src/lammpsio/snapshot.py b/src/lammpsio/snapshot.py
index 5d13738..c1c5b75 100644
--- a/src/lammpsio/snapshot.py
+++ b/src/lammpsio/snapshot.py
@@ -10,20 +10,49 @@
class Snapshot:
"""Particle configuration.
+ A `Snapshot` holds the data for `N` particles, the simulation `Box`, and the
+ timestep.
+
Parameters
----------
N : int
Number of particles in configuration.
- box : :class:`Box`
+ box : `Box`
Simulation box.
step : int
- Simulation time step counter. Default of ``None`` means
- time step is not specified.
+ Simulation time step. Default of ``None`` means time step is not
+ specified.
+ num_types : int
+ Number of particle types. If ``None``, the number of types is deduced
+ from `typeid`.
- Attributes
- ----------
- step : int
- Simulation time step counter.
+ Example
+ -------
+ Here is a 3-particle configuration in an triclinic box centered at the
+ origin at step 10:
+
+ .. code-block:: python
+
+ box = lammpsio.Box([-5.0, -10.0, 0.0], [1.0, 10.0, 8.0], [1.0, -2.0,
+ 0.5])
+
+ snapshot = lammpsio.Snapshot(3, box, 10, num_types=None)
+
+ All values of indexes follow the LAMMPS 1-indexed convention, but the arrays
+ themselves are 0-indexed. `Snapshot` will lazily initialize these
+ per-particle arrays as they are accessed to save memory. Hence, accessing a
+ per-particle property will allocate it to default values. If you want to
+ check if an attribute has been set, use the corresponding ``has_`` method
+ instead (e.g., `has_position` to check if the `position` data is allocated):
+
+ .. code-block:: python
+
+ snapshot.position = [[0,0,0],[1,-1,1],[1.5,2.5,-3.5]]
+
+ snapshot.typeid[2] = 2
+
+ if not snapshot.has_mass():
+ snapshot.mass = [2.,2.,10.]
"""
@@ -53,16 +82,30 @@ def from_hoomd_gsd(cls, frame):
Parameters
----------
- frame : :class:`gsd.hoomd.Frame`
+ frame : `gsd.hoomd.Frame`
HOOMD GSD frame to convert.
Returns
-------
- :class:`Snapshot`
+ `Snapshot`
Snapshot created from HOOMD GSD frame.
dict
A map from the :attr:`Snapshot.typeid` to the HOOMD type.
+ Example
+ -------
+ Create snapshot from a GSD file:
+
+ .. skip: next if(frame == 0, reason="gsd.hoomd not installed")
+
+ .. code-block:: python
+
+ frame.configuration.box = [4, 5, 6, 0.1, 0.2, 0.3]
+
+ frame.particles.N = 3
+
+ snap_2, type_map = lammpsio.Snapshot.from_hoomd_gsd(frame)
+
"""
# ensures frame is well formed and that we have NumPy arrays
frame.validate()
@@ -217,11 +260,25 @@ def to_hoomd_gsd(self, type_map=None):
A map from the :attr:`Snapshot.typeid` to a HOOMD type.
If not specified, the typeids are used as the types.
+ .. deprecated:: 0.7.0
+ Use `Snapshot.type_label` instead.
+
Returns
-------
- :class:`gsd.hoomd.Frame`
+ `gsd.hoomd.Frame`
Converted HOOMD GSD frame.
+ Example
+ -------
+
+ Convert snapshot to a GSD file:
+
+ .. skip: next if(frame == 0, reason="gsd.hoomd not installed")
+
+ .. code-block:: python
+
+ frame = snap_2.to_hoomd_gsd()
+
"""
if _compatibility.gsd_version is None:
raise ImportError("GSD package not found")
@@ -352,17 +409,54 @@ def to_hoomd_gsd(self, type_map=None):
@property
def N(self):
- """int: Number of particles."""
+ """int: Number of particles.
+
+ Example
+ -------
+ .. code-block:: python
+
+ num_particles = snapshot.N
+
+ """
return self._N
@property
def box(self):
- """:class:`Box`: Simulation box."""
+ """`Box`: Simulation box.
+
+ Example
+ -------
+ .. code-block:: python
+
+ snapshot.box
+
+ """
return self._box
+ @property
+ def step(self):
+ """int: Simulation time step."""
+ return self._step
+
+ @step.setter
+ def step(self, value):
+ if value is not None:
+ self._step = int(value)
+ else:
+ self._step = None
+
@property
def id(self):
- """:class:`numpy.ndarray`: Particle IDs."""
+ """(*N*,) `numpy.ndarray` of `int`: Particle IDs.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.id = [2, 0, 1]
+
+ """
if not self.has_id():
self._id = numpy.arange(1, self.N + 1)
return self._id
@@ -387,14 +481,29 @@ def has_id(self):
Returns
-------
bool
- True if particle IDs have been initialized.
+ ``True`` if particle IDs have been initialized.
+
+ Example
+ -------
+ .. code-block:: python
+
+ snapshot.has_id()
"""
return self._id is not None
@property
def position(self):
- """:class:`numpy.ndarray`: Positions."""
+ """(*N*, 3) `numpy.ndarray` of `float`: Positions.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.position = [[0.1, 0.2, 0.3], [-0.4, -0.5, -0.6], [0.7, 0.8, 0.9]]
+
+ """
if not self.has_position():
self._position = numpy.zeros((self.N, 3), dtype=float)
return self._position
@@ -419,14 +528,30 @@ def has_position(self):
Returns
-------
bool
- True if positions have been initialized.
+ ``True`` if positions have been initialized.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_position()
"""
return self._position is not None
@property
def image(self):
- """:class:`numpy.ndarray`: Images."""
+ """(*N*, 3) `numpy.ndarray` of `int`: Images.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.image = [[1, 2, 3], [-4, -5, -6], [7, 8, 9]]
+
+ """
if not self.has_image():
self._image = numpy.zeros((self.N, 3), dtype=int)
return self._image
@@ -451,14 +576,30 @@ def has_image(self):
Returns
-------
bool
- True if images have been initialized.
+ ``True`` if images have been initialized.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_image()
"""
return self._image is not None
@property
def velocity(self):
- """:class:`numpy.ndarray`: Velocities."""
+ """(*N*, 3) `numpy.ndarray` of `float`: Velocities.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.velocity = [[-3, -2, -1], [6, 5, 4], [9, 8, 7]]
+
+ """
if not self.has_velocity():
self._velocity = numpy.zeros((self.N, 3), dtype=float)
return self._velocity
@@ -483,14 +624,21 @@ def has_velocity(self):
Returns
-------
bool
- True if velocities have been initialized.
+ ``True`` if velocities have been initialized.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_velocity()
"""
return self._velocity is not None
@property
def molecule(self):
- """:class:`numpy.ndarray`: Molecule tags."""
+ """(*N*,) `numpy.ndarray` of `int`: Molecule tags."""
if not self.has_molecule():
self._molecule = numpy.zeros(self.N, dtype=int)
return self._molecule
@@ -515,7 +663,14 @@ def has_molecule(self):
Returns
-------
bool
- True if molecule tags have been initialized.
+ ``True`` if molecule tags have been initialized.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_molecule()
"""
return self._molecule is not None
@@ -540,7 +695,16 @@ def num_types(self, value):
@property
def typeid(self):
- """:class:`numpy.ndarray`: Types."""
+ """(*N*,) `numpy.ndarray` of `int`: Types.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.typeid = [2, 1, 2]
+
+ """
if not self.has_typeid():
self._typeid = numpy.ones(self.N, dtype=int)
return self._typeid
@@ -565,14 +729,30 @@ def has_typeid(self):
Returns
-------
bool
- True if types have been initialized.
+ ``True`` if types have been initialized.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_typeid()
"""
return self._typeid is not None
@property
def charge(self):
- """:class:`numpy.ndarray`: Charges."""
+ """(*N*,) `numpy.ndarray` of `float`: Charges.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.charge = [-1, 0, 1]
+
+ """
if not self.has_charge():
self._charge = numpy.zeros(self.N, dtype=float)
return self._charge
@@ -597,14 +777,30 @@ def has_charge(self):
Returns
-------
bool
- True if charges have been initialized.
+ ``True`` if charges have been initialized.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_charge()
"""
return self._charge is not None
@property
def mass(self):
- """:class:`numpy.ndarray`: Masses."""
+ """:(*N*,) `numpy.ndarray` of `float`: Masses.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.mass = [3, 2, 3]
+
+ """
if not self.has_mass():
self._mass = numpy.ones(self.N, dtype=float)
return self._mass
@@ -624,19 +820,35 @@ def mass(self, value):
self._mass = None
def has_mass(self):
- """Check if configuration has masses.
+ """`bool`: Check if configuration has masses.
Returns
-------
bool
- True if masses have been initialized.
+ ``True`` if masses have been initialized.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_mass()
"""
return self._mass is not None
@property
def type_label(self):
- """LabelMap: Labels for particle typeids."""
+ """`LabelMap`: Labels for `typeid`.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.type_label = lammpsio.topology.LabelMap({1: "A", 2: "B"})
+
+ """
return self._type_label
@type_label.setter
@@ -650,7 +862,16 @@ def type_label(self, value):
@property
def bonds(self):
- """Bonds: Bond data."""
+ """`Bonds`: Bond data.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.bonds = lammpsio.topology.Bonds(N=6, num_types=2)
+
+ """
return self._bonds
@bonds.setter
@@ -668,14 +889,30 @@ def has_bonds(self):
Returns
-------
bool
- True if bonds is initialized and there is at least one bond.
+ ``True`` if bonds is initialized and there is at least one bond.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_bonds()
"""
return self._bonds is not None and self._bonds.N > 0
@property
def angles(self):
- """Angles: Angle data."""
+ """`Angles`: Angle data.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.angles = lammpsio.topology.Angles(N=4, num_types=2)
+
+ """
return self._angles
@angles.setter
@@ -693,14 +930,30 @@ def has_angles(self):
Returns
-------
bool
- True if angles is initialized and there is at least one angle.
+ ``True`` if angles is initialized and there is at least one angle.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_angles()
"""
return self._angles is not None and self._angles.N > 0
@property
def dihedrals(self):
- """Dihedrals: Dihedral data."""
+ """`Dihedrals`: Dihedral data.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.dihedrals = lammpsio.topology.Dihedrals(N=2, num_types=2)
+
+ """
return self._dihedrals
@dihedrals.setter
@@ -718,14 +971,30 @@ def has_dihedrals(self):
Returns
-------
bool
- True if dihedrals is initialized and there is at least one dihedral.
+ ``True`` if dihedrals is initialized and there is at least one dihedral.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_dihedrals()
"""
return self._dihedrals is not None and self._dihedrals.N > 0
@property
def impropers(self):
- """Impropers: Improper data."""
+ """`Impropers`: Improper data.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.impropers = lammpsio.topology.Impropers(N=2, num_types=2)
+
+ """
return self._impropers
@impropers.setter
@@ -743,7 +1012,14 @@ def has_impropers(self):
Returns
-------
bool
- True if impropers is initialized and there is at least one improper.
+ ``True`` if impropers is initialized and there is at least one improper.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ snapshot.has_impropers()
"""
return self._impropers is not None and self._impropers.N > 0
@@ -756,7 +1032,16 @@ def reorder(self, order, check_order=True):
order : list
New order of indexes.
check_order : bool
- If true, validate the new ``order`` before applying it.
+ If ``True``, validate the new ``order`` before applying it.
+
+ Example
+ -------
+
+ .. code-block:: python
+
+ bond_id = [0, 3, 1, 4, 2, 5]
+
+ snapshot.bonds.reorder(numpy.sort(bond_id), check_order=True)
"""
# sanity check the sorting order before applying it
@@ -790,20 +1075,19 @@ def reorder(self, order, check_order=True):
def _set_type_id(lammps_typeid, gsd_typeid, label_map):
"""Maps LAMMPS typeids to HOOMD GSD typeids using a given label map.
- Parameters:
+ Parameters
----------
lammps_typeid : list
List of LAMMPS typeids to be mapped (one-indexed).
gsd_typeid : list
List of HOOMD GSD typeids to be updated (zero-indexed).
- label_map : :class:`LabelMap`
+ label_map : `LabelMap`
LabelMap for connection type mapping LAMMPS typeids to HOOMD GSD types.
- Returns:
+ Returns
-------
- :class:`LabelMap`
+ `LabelMap`
LabelMap mapping LAMMPS typeids to HOOMD GSD types.
- LabelMap is created mapping typeids to str(typeids) if not provided.
"""
if label_map is None:
sorted_typeids = numpy.sort(numpy.unique(lammps_typeid))
diff --git a/src/lammpsio/topology.py b/src/lammpsio/topology.py
index e237f61..ff4038f 100644
--- a/src/lammpsio/topology.py
+++ b/src/lammpsio/topology.py
@@ -1,3 +1,5 @@
+"""Topology (connection and type) information."""
+
import collections.abc
import numpy
@@ -15,8 +17,12 @@ class Topology:
num_members : int
Number of members in a connection.
num_types : int
- Number of connection types. Default of ``None`` means
- the number of types is determined from the unique typeids.
+ Number of connection types. Default of ``None`` means the number of
+ types is determined from the unique typeids.
+
+ All values of indexes follow the LAMMPS 1-indexed convention, but the
+ arrays themselves are 0-indexed. Lazy array initialization is used as for
+ the `Snapshot`.
"""
@@ -37,7 +43,11 @@ def N(self):
@property
def id(self):
- """:class:`numpy.ndarray`: IDs."""
+ """(*N*,) `numpy.ndarray` of `int`: Unique identifiers (IDs).
+
+ The default value on initialization runs from 1 to `N`.
+
+ """
if not self.has_id():
self._id = numpy.arange(1, self.N + 1)
return self._id
@@ -62,14 +72,18 @@ def has_id(self):
Returns
-------
bool
- True if connection IDs have been initialized.
+ ``True`` if connection IDs have been initialized.
"""
return self._id is not None
@property
def typeid(self):
- """:class:`numpy.ndarray`: Connection typeids."""
+ """(*N*,) `numpy.ndarray` of `int`: Connection type IDs.
+
+ The default value on initialization is 1 for all entries.
+
+ """
if not self.has_typeid():
self._typeid = numpy.ones(self.N, dtype=int)
return self._typeid
@@ -94,14 +108,19 @@ def has_typeid(self):
Returns
-------
bool
- True if connection typeids have been initialized.
+ ``True`` if connection typeids have been initialized.
"""
return self._typeid is not None
@property
def members(self):
- """:class:`numpy.ndarray`: Connection members."""
+ """(*N*, *M*) `numpy.ndarray` of `int`: Connection members.
+
+ The default value on initialization is 1 for all entries. *M* is the
+ number of members in the connection.
+
+ """
if not self.has_members():
self._members = numpy.ones((self.N, self._num_members), dtype=int)
return self._members
@@ -126,14 +145,14 @@ def has_members(self):
Returns
-------
bool
- True if particle members have been initialized.
+ ``True`` if particle members have been initialized.
"""
return self._members is not None
@property
def num_types(self):
- """int: Number of connection types"""
+ """int: Number of connection types."""
if self._num_types is not None:
return self._num_types
else:
@@ -151,7 +170,7 @@ def num_types(self, value):
@property
def type_label(self):
- """LabelMap: Labels of connection typeids."""
+ """LabelMap: Labels of connection type IDs."""
return self._type_label
@@ -172,7 +191,7 @@ def reorder(self, order, check_order=True):
order : list
New order of indexes.
check_order : bool
- If true, validate the new ``order`` before applying it.
+ If ``True``, validate the new ``order`` before applying it.
"""
# sanity check the sorting order before applying it
@@ -194,14 +213,26 @@ def reorder(self, order, check_order=True):
class Bonds(Topology):
- """Particle bonds.
+ """Bond connections between particles.
+
+ All values of indexes follow the LAMMPS 1-indexed convention, but the
+ arrays themselves are 0-indexed.
Parameters
----------
N : int
Number of bonds.
num_types : int
- Number of bond types.
+ Number of bond types. Default of ``None`` means the number of types is
+ determined from the unique typeids.
+
+ Example
+ -------
+ Create bonds:
+
+ .. code-block:: python
+
+ bonds = lammpsio.topology.Bonds(N=2, num_types=2)
"""
@@ -210,14 +241,26 @@ def __init__(self, N, num_types=None):
class Angles(Topology):
- """Particle angles.
+ """Angle connections between particles.
+
+ All values of indexes follow the LAMMPS 1-indexed convention, but the
+ arrays themselves are 0-indexed.
Parameters
----------
N : int
Number of angles.
num_types : int
- Number of angle types.
+ Number of angle types. Default of ``None`` means the number of types is
+ determined from the unique typeids.
+
+ Example
+ -------
+ Create angles:
+
+ .. code-block:: python
+
+ angles = lammpsio.topology.Angles(N=2, num_types=2)
"""
@@ -226,14 +269,26 @@ def __init__(self, N, num_types=None):
class Dihedrals(Topology):
- """Particle dihedrals.
+ """Dihedral connections between particles.
+
+ All values of indexes follow the LAMMPS 1-indexed convention, but the
+ arrays themselves are 0-indexed.
Parameters
----------
N : int
- Number of diehdrals.
+ Number of dihedrals.
num_types : int
- Number of dihedral types.
+ Number of dihedral types. Default of ``None`` means the number of types
+ is determined from the unique typeids.
+
+ Example
+ -------
+ Create dihedrals:
+
+ .. code-block:: python
+
+ dihedrals = lammpsio.topology.Dihedrals(N=2, num_types=2)
"""
@@ -242,14 +297,26 @@ def __init__(self, N, num_types=None):
class Impropers(Topology):
- """Particle improper dihedrals.
+ """Improper dihedral connections between particles.
+
+ All values of indexes follow the LAMMPS 1-indexed convention, but the
+ arrays themselves are 0-indexed.
Parameters
----------
N : int
Number of improper dihedrals.
num_types : int
- Number of improper dihedral types.
+ Number of improper dihedral types. Default of ``None`` means the number
+ of types is determined from the unique typeids.
+
+ Example
+ -------
+ Create dihedrals:
+
+ .. code-block:: python
+
+ impropers = lammpsio.topology.Impropers(N=2, num_types=2)
"""
@@ -258,13 +325,26 @@ def __init__(self, N, num_types=None):
class LabelMap(collections.abc.MutableMapping):
- """Label map between typeids and types.
+ """Map between integer type IDs and string type names.
+
+ A `LabelMap` is effectively a dictionary associating a label (type) with a
+ particle's or connection's typeid. These labels can be useful for tracking
+ the meaning of typeids. They are also automatically used when interconverting
+ with HOOMD GSD files that require such labels.
Parameters
----------
map : dict
Map of typeids to types.
+ Example
+ -------
+ Create `LabelMap`:
+
+ .. code-block:: python
+
+ label = lammpsio.topology.LabelMap({1: "A", 2: "B"})
+
"""
def __init__(self, map=None):
@@ -289,10 +369,10 @@ def __len__(self):
@property
def types(self):
- """tuple: Types in label map."""
+ """tuple of str: Types in map."""
return tuple(self._map.values())
@property
def typeid(self):
- """tuple: Typeids in label map."""
+ """tuple of int: Type IDs in map."""
return tuple(self._map.keys())
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 69a6b01..72bd04e 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,2 +1,3 @@
pytest>=8
pytest-lazy-fixtures>=1.1.1
+sybil