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 @@ [![PyPI downloads](https://img.shields.io/pypi/dm/lammpsio)](https://pypi.org/project/lammpsio) [![Conda](https://img.shields.io/conda/dn/conda-forge/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