diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 60b1bcfc0a..696a1b8cdd 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -662,7 +662,7 @@ if(BUILD_SHARED_LIBS) add_custom_target( install-python ${Python_EXECUTABLE} install.py -v ${LAMMPS_SOURCE_DIR}/version.h - -m ${LAMMPS_PYTHON_DIR}/lammps.py + -p ${LAMMPS_PYTHON_DIR}/lammps -l ${CMAKE_BINARY_DIR}/liblammps${CMAKE_SHARED_LIBRARY_SUFFIX} WORKING_DIRECTORY ${LAMMPS_PYTHON_DIR} COMMENT "Installing LAMMPS Python module") @@ -692,11 +692,8 @@ if(BUILD_SHARED_LIBS OR PKG_PYTHON) find_package(Python COMPONENTS Interpreter) endif() if (Python_EXECUTABLE) - execute_process(COMMAND ${Python_EXECUTABLE} - -c "import distutils.sysconfig as cg; print(cg.get_python_lib(1,0,prefix='${CMAKE_INSTALL_PREFIX}'))" - OUTPUT_VARIABLE PYTHON_DEFAULT_INSTDIR OUTPUT_STRIP_TRAILING_WHITESPACE) - set(PYTHON_INSTDIR ${PYTHON_DEFAULT_INSTDIR} CACHE PATH "Installation folder for LAMMPS Python module") - install(FILES ${LAMMPS_PYTHON_DIR}/lammps.py DESTINATION ${PYTHON_INSTDIR}) + file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/python) + install(CODE "execute_process(COMMAND ${Python_EXECUTABLE} setup.py build -b ${CMAKE_BINARY_DIR}/python install --prefix=${CMAKE_INSTALL_PREFIX} --root=\$ENV{DESTDIR}/ WORKING_DIRECTORY ${LAMMPS_PYTHON_DIR})") endif() endif() diff --git a/doc/src/Howto_pylammps.rst b/doc/src/Howto_pylammps.rst index 89119e89af..e6d3d7b91d 100644 --- a/doc/src/Howto_pylammps.rst +++ b/doc/src/Howto_pylammps.rst @@ -9,8 +9,8 @@ Overview ``PyLammps`` is a Python wrapper class for LAMMPS which can be created on its own or use an existing lammps Python object. It creates a simpler, more "pythonic" interface to common LAMMPS functionality, in contrast to -the ``lammps.py`` wrapper for the C-style LAMMPS library interface which -is written using `Python ctypes `_. The ``lammps.py`` wrapper +the ``lammps`` wrapper for the C-style LAMMPS library interface which +is written using `Python ctypes `_. The ``lammps`` wrapper is discussed on the :doc:`Python_head` doc page. Unlike the flat ``ctypes`` interface, PyLammps exposes a discoverable diff --git a/doc/src/Python_ext.rst b/doc/src/Python_ext.rst index 6b6d1ab715..8966ffcb2a 100644 --- a/doc/src/Python_ext.rst +++ b/doc/src/Python_ext.rst @@ -9,7 +9,7 @@ This means you can extend the Python wrapper by following these steps: * Add a new interface function to ``src/library.cpp`` and ``src/library.h``. * Rebuild LAMMPS as a shared library. -* Add a wrapper method to ``python/lammps.py`` for this interface +* Add a wrapper method to ``python/lammps/core.py`` for this interface function. * Define the corresponding ``argtypes`` list and ``restype`` in the ``lammps.__init__()`` function. diff --git a/doc/src/Python_install.rst b/doc/src/Python_install.rst index 88d32895a3..c12644bf4a 100644 --- a/doc/src/Python_install.rst +++ b/doc/src/Python_install.rst @@ -8,9 +8,9 @@ module. Because of the dynamic loading, it is required that LAMMPS is compiled in :ref:`"shared" mode `. It is also recommended to compile LAMMPS with :ref:`C++ exceptions ` enabled. -Two files are necessary for Python to be able to invoke LAMMPS code: +Two components are necessary for Python to be able to invoke LAMMPS code: -* The LAMMPS Python Module (``lammps.py``) from the ``python`` folder +* The LAMMPS Python Package (``lammps``) from the ``python`` folder * The LAMMPS Shared Library (``liblammps.so``, ``liblammps.dylib`` or ``liblammps.dll``) from the folder where you compiled LAMMPS. @@ -25,10 +25,10 @@ Installing the LAMMPS Python Module and Shared Library ====================================================== Making LAMMPS usable within Python and vice versa requires putting the -LAMMPS Python module file (``lammps.py``) into a location where the +LAMMPS Python package (``lammps``) into a location where the Python interpreter can find it and installing the LAMMPS shared library -into a folder that the dynamic loader searches or into the same folder -where the ``lammps.py`` file is. There are multiple ways to achieve +into a folder that the dynamic loader searches or inside of the installed +``lammps`` package folder. There are multiple ways to achieve this. #. Do a full LAMMPS installation of libraries, executables, selected @@ -36,13 +36,13 @@ this. available via CMake), which can also be either system-wide or into user specific folders. -#. Install both files into a Python ``site-packages`` folder, either +#. Install both components into a Python ``site-packages`` folder, either system-wide or in the corresponding user-specific folder. This way no additional environment variables need to be set, but the shared library is otherwise not accessible. -#. Do an installation into a virtual environment. This can either be - an installation of the python module only or a full installation. +#. Do an installation into a virtual environment. This can either be an + installation of the Python package only or a full installation of LAMMPS. #. Leave the files where they are in the source/development tree and adjust some environment variables. @@ -81,19 +81,19 @@ this. This leads to an installation to the following locations: - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+===========================================================+=============================================================+ - | LAMMPS Python Module | * ``$HOME/.local/lib/pythonX.Y/site-packages/`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$HOME/.local/lib64/pythonX.Y/site-packages/`` (64bit) | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``$HOME/.local/lib/`` (32bit) | | - | | * ``$HOME/.local/lib64/`` (64bit) | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS executable | * ``$HOME/.local/bin/`` | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS potential files | * ``$HOME/.local/share/lammps/potentials/`` | Set ``LAMMPS_POTENTIALS`` environment variable to this path | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+=================================================================+=============================================================+ + | LAMMPS Python package | * ``$HOME/.local/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | + | | * ``$HOME/.local/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``$HOME/.local/lib/`` (32bit) | Set shared loader environment variable to this path | + | | * ``$HOME/.local/lib64/`` (64bit) | (see below for more info on this) | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS executable | * ``$HOME/.local/bin/`` | | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS potential files | * ``$HOME/.local/share/lammps/potentials/`` | Set ``LAMMPS_POTENTIALS`` environment variable to this path | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ For a system-wide installation you need to set ``CMAKE_INSTALL_PREFIX`` to a system folder like ``/usr`` (or @@ -102,19 +102,19 @@ this. privilege, e.g. by using ``sudo cmake --install .``. The installation folders will then by changed to: - +------------------------+---------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+===================================================+=============================================================+ - | LAMMPS Python Module | * ``/usr/lib/pythonX.Y/site-packages/`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``/usr/lib64/pythonX.Y/site-packages/`` (64bit) | | - +------------------------+---------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``/usr/lib/`` (32bit) | | - | | * ``/usr/lib64/`` (64bit) | | - +------------------------+---------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS executable | * ``/usr/bin/`` | | - +------------------------+---------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS potential files | * ``/usr/share/lammps/potentials/`` | | - +------------------------+---------------------------------------------------+-------------------------------------------------------------+ + +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+=========================================================+=============================================================+ + | LAMMPS Python package | * ``/usr/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | + | | * ``/usr/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | + +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``/usr/lib/`` (32bit) | | + | | * ``/usr/lib64/`` (64bit) | | + +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS executable | * ``/usr/bin/`` | | + +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS potential files | * ``/usr/share/lammps/potentials/`` | | + +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ To be able to use the "user" installation you have to ensure that the folder containing the LAMMPS shared library is either included @@ -146,7 +146,7 @@ this. necessary due to files installed in system folders that are loaded automatically when a login shell is started. - .. tab:: Python module only + .. tab:: Python package only Compile LAMMPS with either :doc:`CMake ` or the :doc:`traditional make ` procedure in :ref:`shared @@ -157,37 +157,37 @@ this. make install-python - This will try to install (only) the shared library and the python - module into a system folder and if that fails (due to missing + This will try to install (only) the shared library and the Python + package into a system folder and if that fails (due to missing write permissions) will instead do the installation to a user folder under ``$HOME/.local``. For a system-wide installation you would have to gain superuser privilege, e.g. though ``sudo`` - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+===========================================================+=============================================================+ - | LAMMPS Python Module | * ``$HOME/.local/lib/pythonX.Y/site-packages/`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$HOME/.local/lib64/pythonX.Y/site-packages/`` (64bit) | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``$HOME/.local/lib/pythonX.Y/site-packages/`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$HOME/.local/lib64/pythonX.Y/site-packages/`` (64bit) | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+=================================================================+=============================================================+ + | LAMMPS Python package | * ``$HOME/.local/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | + | | * ``$HOME/.local/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``$HOME/.local/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | + | | * ``$HOME/.local/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ For a system-wide installation those folders would then become. - +------------------------+---------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+===================================================+=============================================================+ - | LAMMPS Python Module | * ``/usr/lib/pythonX.Y/site-packages/`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``/usr/lib64/pythonX.Y/site-packages/`` (64bit) | | - +------------------------+---------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``/usr/lib/pythonX.Y/site-packages/`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``/usr/lib64/pythonX.Y/site-packages/`` (64bit) | | - +------------------------+---------------------------------------------------+-------------------------------------------------------------+ + +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+=========================================================+=============================================================+ + | LAMMPS Python package | * ``/usr/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | + | | * ``/usr/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | + +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``/usr/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | + | | * ``/usr/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | + +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ No environment variables need to be set for those, as those folders are searched by default by Python or the LAMMPS Python - module. + package. For the traditional make process you can override the python version to version x.y when calling ``make`` with @@ -199,9 +199,9 @@ this. .. code-block:: bash - $ python install.py -m -l -v [-d ] + $ python install.py -p -l -v [-d ] - * The ``-m`` flag points to the ``lammps.py`` python module file to be installed, + * The ``-p`` flag points to the ``lammps`` Python package folder to be installed, * the ``-l`` flag points to the LAMMPS shared library file to be installed, * the ``-v`` flag points to the ``version.h`` file in the LAMMPS source * and the optional ``-d`` flag to a custom (legacy) installation folder @@ -249,38 +249,38 @@ this. When using CMake to build LAMMPS, you need to set ``CMAKE_INSTALL_PREFIX`` to the value of the ``$VIRTUAL_ENV`` environment variable during the configuration step. For the - traditional make procedure, not additional steps are needed. - After compiling LAMMPS you can do a "Python module only" + traditional make procedure, no additional steps are needed. + After compiling LAMMPS you can do a "Python package only" installation with ``make install-python`` and the LAMMPS Python - module and the shared library file are installed into the + package and the shared library file are installed into the following locations: - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+===========================================================+=============================================================+ - | LAMMPS Python Module | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$VIRTUAL_ENV/lib64/pythonX.Y/site-packages/`` (64bit) | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$VIRTUAL_ENV/lib64/pythonX.Y/site-packages/`` (64bit) | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+=================================================================+=============================================================+ + | LAMMPS Python Module | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | + | | * ``$VIRTUAL_ENV/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | + | | * ``$VIRTUAL_ENV/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ If you do a full installation (CMake only) with "install", this leads to the following installation locations: - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+===========================================================+=============================================================+ - | LAMMPS Python Module | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$VIRTUAL_ENV/lib64/pythonX.Y/site-packages/`` (64bit) | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``$VIRTUAL_ENV/lib/`` (32bit) | | - | | * ``$VIRTUAL_ENV/lib64/`` (64bit) | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS executable | * ``$VIRTUAL_ENV/bin/`` | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS potential files | * ``$VIRTUAL_ENV/share/lammps/potentials/`` | | - +------------------------+-----------------------------------------------------------+-------------------------------------------------------------+ + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+=================================================================+=============================================================+ + | LAMMPS Python Module | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | + | | * ``$VIRTUAL_ENV/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``$VIRTUAL_ENV/lib/`` (32bit) | Set shared loader environment variable to this path | + | | * ``$VIRTUAL_ENV/lib64/`` (64bit) | (see below for more info on this) | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS executable | * ``$VIRTUAL_ENV/bin/`` | | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS potential files | * ``$VIRTUAL_ENV/share/lammps/potentials/`` | Set ``LAMMPS_POTENTIALS`` environment variable to this path | + +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ In that case you need to modify the ``$HOME/myenv/bin/activate`` script in a similar fashion you need to update your @@ -296,15 +296,15 @@ this. echo 'export LD_LIBRARY_PATH=$VIRTUAL_ENV/lib:$LD_LIBRARY_PATH' >> $HOME/myenv/bin/activate # MacOS - echo 'export DYLD_LIBRARY_PATH=$VIRTUAL_ENV/lib:$LD_LIBRARY_PATH' >> $HOME/myenv/bin/activate + echo 'export DYLD_LIBRARY_PATH=$VIRTUAL_ENV/lib:$DYLD_LIBRARY_PATH' >> $HOME/myenv/bin/activate .. tab:: In place usage You can also :doc:`compile LAMMPS ` as usual in :ref:`"shared" mode ` leave the shared library and Python - module files inside the source/compilation folders. Instead of + package inside the source/compilation folders. Instead of copying the files where they can be found, you need to set the environment - variables ``PYTHONPATH`` (for the Python module) and + variables ``PYTHONPATH`` (for the Python package) and ``LD_LIBRARY_PATH`` (or ``DYLD_LIBRARY_PATH`` on MacOS For Bourne shells (bash, ksh and similar) the commands are: @@ -325,6 +325,10 @@ this. You can make those changes permanent by editing your ``$HOME/.bashrc`` or ``$HOME/.login`` files, respectively. + .. note:: + + The ``PYTHONPATH`` needs to point to the parent folder that contains the ``lammps`` package! + To verify if LAMMPS can be successfully started from Python, start the Python interpreter, load the ``lammps`` Python module and create a @@ -346,7 +350,7 @@ output similar to the following: .. note:: Unless you opted for "In place use", you will have to rerun the installation - any time you recompile LAMMPS to ensure the latest Python module and shared + any time you recompile LAMMPS to ensure the latest Python package and shared library are installed and used. .. note:: diff --git a/doc/src/Python_module.rst b/doc/src/Python_module.rst index 04bc3f2c5b..8b4fbe1c2e 100644 --- a/doc/src/Python_module.rst +++ b/doc/src/Python_module.rst @@ -3,12 +3,12 @@ The ``lammps`` Python module .. py:module:: lammps -The LAMMPS Python interface is implemented as a module called -:py:mod:`lammps` in the ``lammps.py`` file in the ``python`` folder of -the LAMMPS source code distribution. After compilation of LAMMPS, the -module can be installed into a Python system folder or a user folder -with ``make install-python``. Components of the module can then loaded -into a Python session with the ``import`` command. +The LAMMPS Python interface is implemented as a module called :py:mod:`lammps` +which is defined in the ``lammps`` package in the ``python`` folder of the +LAMMPS source code distribution. After compilation of LAMMPS, the module can +be installed into a Python system folder or a user folder with ``make +install-python``. Components of the module can then loaded into a Python +session with the ``import`` command. There are multiple Python interface classes in the :py:mod:`lammps` module: @@ -44,7 +44,7 @@ functions. Below is a detailed documentation of the API. .. autoclass:: lammps.lammps :members: -.. autoclass:: lammps.numpy_wrapper +.. autoclass:: lammps.numpy::numpy_wrapper :members: ---------- @@ -117,8 +117,8 @@ Style Constants to request from computes or fixes. See :cpp:enum:`_LMP_STYLE_CONST` for the equivalent constants in the C library interface. Used in :py:func:`lammps.extract_compute`, :py:func:`lammps.extract_fix`, and their NumPy variants - :py:func:`lammps.numpy.extract_compute() ` and - :py:func:`lammps.numpy.extract_fix() `. + :py:func:`lammps.numpy.extract_compute() ` and + :py:func:`lammps.numpy.extract_fix() `. .. _py_type_constants: @@ -132,8 +132,8 @@ Type Constants to request from computes or fixes. See :cpp:enum:`_LMP_TYPE_CONST` for the equivalent constants in the C library interface. Used in :py:func:`lammps.extract_compute`, :py:func:`lammps.extract_fix`, and their NumPy variants - :py:func:`lammps.numpy.extract_compute() ` and - :py:func:`lammps.numpy.extract_fix() `. + :py:func:`lammps.numpy.extract_compute() ` and + :py:func:`lammps.numpy.extract_fix() `. .. _py_vartype_constants: @@ -153,6 +153,6 @@ Classes representing internal objects :members: :no-undoc-members: -.. autoclass:: lammps.NumPyNeighList +.. autoclass:: lammps.numpy::NumPyNeighList :members: :no-undoc-members: diff --git a/doc/src/Python_overview.rst b/doc/src/Python_overview.rst index a90ea171d5..2dfc193f8d 100644 --- a/doc/src/Python_overview.rst +++ b/doc/src/Python_overview.rst @@ -2,9 +2,9 @@ Overview ======== The LAMMPS distribution includes a ``python`` directory with the Python -code needed to run LAMMPS from Python. The ``python/lammps.py`` -contains :doc:`the "lammps" Python ` that wraps the -LAMMPS C-library interface. This file makes it is possible to do the +code needed to run LAMMPS from Python. The ``python/lammps`` package +contains :doc:`the "lammps" Python module ` that wraps the +LAMMPS C-library interface. This module makes it is possible to do the following either from a Python script, or interactively from a Python prompt: @@ -20,8 +20,8 @@ have a version of Python that extends Python to enable multiple instances of Python to read what you type. To do all of this, you must build LAMMPS in :ref:`"shared" mode ` -and make certain that your Python interpreter can find the ``lammps.py`` -file and the LAMMPS shared library file. +and make certain that your Python interpreter can find the ``lammps`` +Python package and the LAMMPS shared library file. .. _ctypes: https://docs.python.org/3/library/ctypes.html diff --git a/doc/src/Python_trouble.rst b/doc/src/Python_trouble.rst index 3ef7dacf34..b94a043a6a 100644 --- a/doc/src/Python_trouble.rst +++ b/doc/src/Python_trouble.rst @@ -33,7 +33,7 @@ the constructor call as follows (see :ref:`python_create_lammps` for more detail >>> lmp = lammps(name='mpi') You can also test the load directly in Python as follows, without -first importing from the lammps.py file: +first importing from the ``lammps`` module: .. code-block:: python diff --git a/doc/src/python.rst b/doc/src/python.rst index f38e756232..a4c3d7097c 100644 --- a/doc/src/python.rst +++ b/doc/src/python.rst @@ -323,8 +323,8 @@ Python function is as follows: The function definition must include a variable (lmpptr in this case) which corresponds to SELF in the python command. The first line of the -function imports the Python module lammps.py in the python directory of -the distribution. The second line creates a Python object "lmp" which +function imports the :doc:`"lammps" Python module `. +The second line creates a Python object ``lmp`` which wraps the instance of LAMMPS that called the function. The "ptr=lmpptr" argument is what makes that happen. The third line invokes the command() function in the LAMMPS library interface. It takes a single @@ -502,18 +502,16 @@ Python library on your system. Settings to enable this are in the lib/python/Makefile.lammps file. See the lib/python/README file for information on those settings. -If you use Python code which calls back to LAMMPS, via the SELF input -argument explained above, there is an extra step required when -building LAMMPS. LAMMPS must also be built as a shared library and -your Python function must be able to load the Python module in -python/lammps.py that wraps the LAMMPS library interface. These are -the same steps required to use Python by itself to wrap LAMMPS. -Details on these steps are explained on the :doc:`Python ` -doc page. Note that it is important that the stand-alone LAMMPS -executable and the LAMMPS shared library be consistent (built from the -same source code files) in order for this to work. If the two have -been built at different times using different source files, problems -may occur. +If you use Python code which calls back to LAMMPS, via the SELF input argument +explained above, there is an extra step required when building LAMMPS. LAMMPS +must also be built as a shared library and your Python function must be able to +load the :doc:`"lammps" Python module ` that wraps the LAMMPS +library interface. These are the same steps required to use Python by itself +to wrap LAMMPS. Details on these steps are explained on the :doc:`Python +` doc page. Note that it is important that the stand-alone LAMMPS +executable and the LAMMPS shared library be consistent (built from the same +source code files) in order for this to work. If the two have been built at +different times using different source files, problems may occur. Related commands """""""""""""""" diff --git a/examples/COUPLE/python/example.py b/examples/COUPLE/python/example.py index ea268fe1ca..5ddb9fb587 100644 --- a/examples/COUPLE/python/example.py +++ b/examples/COUPLE/python/example.py @@ -1,4 +1,4 @@ -# this example requires the LAMMPS Python package (lammps.py) to be installed +# this example requires the LAMMPS Python package (python/lammps) to be installed # and LAMMPS to be loadable as shared library in LD_LIBRARY_PATH import lammps diff --git a/lib/python/Makefile.lammps b/lib/python/Makefile.lammps index 4289674e99..e4afa70456 100644 --- a/lib/python/Makefile.lammps +++ b/lib/python/Makefile.lammps @@ -2,6 +2,6 @@ # See the README file for more explanation python_SYSINC = $(shell which python-config > /dev/null 2>&1 && python-config --includes || :) -python_SYSLIB = $(shell which python-config > /dev/null 2>&1 && python-config --ldflags || :) +python_SYSLIB = $(shell which python-config > /dev/null 2>&1 && python-config --ldflags --embed > /dev/null 2>&1 && python-config --ldflags --embed || (which python-config > /dev/null 2>&1 && python-config --ldflags || :) ) python_SYSPATH = PYTHON=python diff --git a/lib/python/Makefile.lammps.python3 b/lib/python/Makefile.lammps.python3 index 5c43b45ff6..d37e822327 100644 --- a/lib/python/Makefile.lammps.python3 +++ b/lib/python/Makefile.lammps.python3 @@ -2,6 +2,6 @@ # See the README file for more explanation python_SYSINC = $(shell which python3-config > /dev/null 2>&1 && python3-config --includes || (which python-config > /dev/null 2>&1 && python-config --includes || :)) -python_SYSLIB = $(shell which python3-config > /dev/null 2>&1 && python3-config --ldflags || (which python-config > /dev/null 2>&1 && python-config --ldflags || :)) +python_SYSLIB = $(shell which python3-config > /dev/null 2>&1 && python3-config --ldflags --embed > /dev/null 2>&1 && python3-config --ldflags --embed || (which python3-config > /dev/null 2>&1 && python3-config --ldflags || (which python-config > /dev/null 2>&1 && python-config --ldflags || :) ) ) python_SYSPATH = PYTHON=$(shell which python3 > /dev/null 2>&1 && echo python3 || echo python) diff --git a/python/install.py b/python/install.py index 7f7062103a..a6b69c1ee6 100644 --- a/python/install.py +++ b/python/install.py @@ -1,42 +1,42 @@ #!/usr/bin/env python """ -Installer script to install the LAMMPS python module and the corresponding +Installer script to install the LAMMPS python package and the corresponding shared library into either the system-wide site-packages tree, or - failing that - into the corresponding user tree. Called from the 'install-python' build target in the conventional and CMake based build systems """ -# copy LAMMPS shared library and lammps.py to system dirs +# copy LAMMPS shared library and lammps package to system dirs from __future__ import print_function import sys,os,shutil from argparse import ArgumentParser parser = ArgumentParser(prog='install.py', - description='LAMMPS python module installer script') + description='LAMMPS python package installer script') -parser.add_argument("-m", "--module", required=True, - help="path to the source of the LAMMPS Python module") +parser.add_argument("-p", "--package", required=True, + help="path to the LAMMPS Python package") parser.add_argument("-l", "--lib", required=True, help="path to the compiled LAMMPS shared library") parser.add_argument("-v", "--version", required=True, help="path to the LAMMPS version.h header file") parser.add_argument("-d","--dir", - help="Legacy custom installation folder selection for module and library") + help="Legacy custom installation folder selection for package and library") args = parser.parse_args() # validate arguments and make paths absolute -if args.module: - if not os.path.exists(args.module): - print( "ERROR: LAMMPS module file %s does not exist" % args.module) +if args.package: + if not os.path.exists(args.package): + print( "ERROR: LAMMPS package %s does not exist" % args.package) parser.print_help() sys.exit(1) else: - args.module = os.path.abspath(args.module) + args.package = os.path.abspath(args.package) if args.lib: if not os.path.exists(args.lib): @@ -66,9 +66,9 @@ if args.dir: # without any special processing or additional steps to that folder if args.dir: - print("Copying LAMMPS Python module to custom folder %s" % args.dir) + print("Copying LAMMPS Python package to custom folder %s" % args.dir) try: - shutil.copyfile(args.module, os.path.join(args.dir,'lammps.py')) + shutil.copytree(args.package, os.path.join(args.dir,'lammps')) except shutil.Error: pass # fail silently @@ -81,15 +81,19 @@ if args.dir: sys.exit() # extract version string from header -fp = open(args.version,'r') -txt=fp.read().split('"')[1].split() -verstr=txt[0]+txt[1]+txt[2] -fp.close() +def get_lammps_version(header): + with open(header, 'r') as f: + line = f.readline() + start_pos = line.find('"')+1 + end_pos = line.find('"', start_pos) + return "".join(line[start_pos:end_pos].split()) -print("Installing LAMMPS Python module version %s into site-packages folder" % verstr) +verstr = get_lammps_version(args.version) -# we need to switch to the folder of the python module -os.chdir(os.path.dirname(args.module)) +print("Installing LAMMPS Python package version %s into site-packages folder" % verstr) + +# we need to switch to the folder of the python package +os.chdir(os.path.dirname(args.package)) from distutils.core import setup from distutils.sysconfig import get_python_lib @@ -103,10 +107,10 @@ try: author = "Steve Plimpton", author_email = "sjplimp@sandia.gov", url = "https://lammps.sandia.gov", - description = "LAMMPS Molecular Dynamics Python module", + description = "LAMMPS Molecular Dynamics Python package", license = "GPL", - py_modules = ["lammps"], - data_files = [(get_python_lib(), [args.lib])]) + packages=['lammps'], + data_files = [(os.path.join(get_python_lib(), 'lammps'), [args.lib])]) except: tryuser=True print ("Installation into global site-packages folder failed.\nTrying user folder %s now." % site.USER_SITE) @@ -119,9 +123,9 @@ if tryuser: author = "Steve Plimpton", author_email = "sjplimp@sandia.gov", url = "https://lammps.sandia.gov", - description = "LAMMPS Molecular Dynamics Python module", + description = "LAMMPS Molecular Dynamics Python package", license = "GPL", - py_modules = ["lammps"], - data_files = [(site.USER_SITE, [args.lib])]) + packages=['lammps'], + data_files = [(os.path.join(site.USER_SITE, 'lammps'), [args.lib])]) except: print("Installation into user site package folder failed.") diff --git a/python/lammps/__init__.py b/python/lammps/__init__.py new file mode 100644 index 0000000000..b1c8306617 --- /dev/null +++ b/python/lammps/__init__.py @@ -0,0 +1,4 @@ +from .constants import * +from .core import * +from .data import * +from .pylammps import * diff --git a/python/lammps/constants.py b/python/lammps/constants.py new file mode 100644 index 0000000000..e5504691fe --- /dev/null +++ b/python/lammps/constants.py @@ -0,0 +1,49 @@ +# ---------------------------------------------------------------------- +# LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator +# http://lammps.sandia.gov, Sandia National Laboratories +# Steve Plimpton, sjplimp@sandia.gov +# +# Copyright (2003) Sandia Corporation. Under the terms of Contract +# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains +# certain rights in this software. This software is distributed under +# the GNU General Public License. +# +# See the README file in the top-level LAMMPS directory. +# ------------------------------------------------------------------------- + +from ctypes import c_int, c_int32, c_int64 + +# various symbolic constants to be used +# in certain calls to select data formats +LAMMPS_AUTODETECT = None +LAMMPS_INT = 0 +LAMMPS_INT_2D = 1 +LAMMPS_DOUBLE = 2 +LAMMPS_DOUBLE_2D = 3 +LAMMPS_INT64 = 4 +LAMMPS_INT64_2D = 5 +LAMMPS_STRING = 6 + +# these must be kept in sync with the enums in library.h +LMP_STYLE_GLOBAL = 0 +LMP_STYLE_ATOM = 1 +LMP_STYLE_LOCAL = 2 + +LMP_TYPE_SCALAR = 0 +LMP_TYPE_VECTOR = 1 +LMP_TYPE_ARRAY = 2 +LMP_SIZE_VECTOR = 3 +LMP_SIZE_ROWS = 4 +LMP_SIZE_COLS = 5 + +LMP_VAR_EQUAL = 0 +LMP_VAR_ATOM = 1 + +# ------------------------------------------------------------------------- + +def get_ctypes_int(size): + if size == 4: + return c_int32 + elif size == 8: + return c_int64 + return c_int diff --git a/python/lammps.py b/python/lammps/core.py similarity index 59% rename from python/lammps.py rename to python/lammps/core.py index b74e111dc6..161583b78c 100644 --- a/python/lammps.py +++ b/python/lammps/core.py @@ -10,62 +10,23 @@ # # See the README file in the top-level LAMMPS directory. # ------------------------------------------------------------------------- -# Python wrappers for the LAMMPS library via ctypes +# Python wrapper for the LAMMPS library via ctypes # for python2/3 compatibility from __future__ import print_function -# imports for simple LAMMPS python wrapper module "lammps" - -import sys,traceback,types +import os +import sys +import traceback +import types import warnings from ctypes import * from os.path import dirname,abspath,join from inspect import getsourcefile -# imports for advanced LAMMPS python wrapper modules "PyLammps" and "IPyLammps" - -from collections import namedtuple -import os -import select -import re -import sys - -# various symbolic constants to be used -# in certain calls to select data formats -LAMMPS_AUTODETECT = None -LAMMPS_INT = 0 -LAMMPS_INT_2D = 1 -LAMMPS_DOUBLE = 2 -LAMMPS_DOUBLE_2D = 3 -LAMMPS_INT64 = 4 -LAMMPS_INT64_2D = 5 -LAMMPS_STRING = 6 - -# these must be kept in sync with the enums in library.h -LMP_STYLE_GLOBAL = 0 -LMP_STYLE_ATOM = 1 -LMP_STYLE_LOCAL = 2 - -LMP_TYPE_SCALAR = 0 -LMP_TYPE_VECTOR = 1 -LMP_TYPE_ARRAY = 2 -LMP_SIZE_VECTOR = 3 -LMP_SIZE_ROWS = 4 -LMP_SIZE_COLS = 5 - -LMP_VAR_EQUAL = 0 -LMP_VAR_ATOM = 1 - -# ------------------------------------------------------------------------- - -def get_ctypes_int(size): - if size == 4: - return c_int32 - elif size == 8: - return c_int64 - return c_int +from .constants import * +from .data import * # ------------------------------------------------------------------------- @@ -78,94 +39,6 @@ class MPIAbortException(Exception): # ------------------------------------------------------------------------- -class NeighList: - """This is a wrapper class that exposes the contents of a neighbor list. - - It can be used like a regular Python list. Each element is a tuple of: - - * the atom local index - * its number of neighbors - * and a pointer to an c_int array containing local atom indices of its - neighbors - - Internally it uses the lower-level LAMMPS C-library interface. - - :param lmp: reference to instance of :py:class:`lammps` - :type lmp: lammps - :param idx: neighbor list index - :type idx: int - """ - def __init__(self, lmp, idx): - self.lmp = lmp - self.idx = idx - - def __str__(self): - return "Neighbor List ({} atoms)".format(self.size) - - def __repr__(self): - return self.__str__() - - @property - def size(self): - """ - :return: number of elements in neighbor list - """ - return self.lmp.get_neighlist_size(self.idx) - - def get(self, element): - """ - :return: tuple with atom local index, numpy array of neighbor local atom indices - :rtype: (int, int, ctypes.POINTER(c_int)) - """ - iatom, numneigh, neighbors = self.lmp.get_neighlist_element_neighbors(self.idx, element) - return iatom, numneigh, neighbors - - # the methods below implement the iterator interface, so NeighList can be used like a regular Python list - - def __getitem__(self, element): - return self.get(element) - - def __len__(self): - return self.size - - def __iter__(self): - inum = self.size - - for ii in range(inum): - yield self.get(ii) - -# ------------------------------------------------------------------------- - -class NumPyNeighList(NeighList): - """This is a wrapper class that exposes the contents of a neighbor list. - - It can be used like a regular Python list. Each element is a tuple of: - - * the atom local index - * a NumPy array containing the local atom indices of its neighbors - - Internally it uses the lower-level LAMMPS C-library interface. - - :param lmp: reference to instance of :py:class:`lammps` - :type lmp: lammps - :param idx: neighbor list index - :type idx: int - """ - def __init__(self, lmp, idx): - super(NumPyNeighList, self).__init__(lmp, idx) - - def get(self, element): - """ - :return: tuple with atom local index, numpy array of neighbor local atom indices - :rtype: (int, numpy.array) - """ - iatom, neighbors = self.lmp.numpy.get_neighlist_element_neighbors(self.idx, element) - return iatom, neighbors - - -# ------------------------------------------------------------------------- -# ------------------------------------------------------------------------- - class lammps(object): """Create an instance of the LAMMPS Python class. @@ -222,7 +95,7 @@ class lammps(object): # load liblammps.so unless name is given # if name = "g++", load liblammps_g++.so # try loading the LAMMPS shared object from the location - # of lammps.py with an absolute path, + # of the lammps package with an absolute path, # so that LD_LIBRARY_PATH does not need to be set for regular install # fall back to loading with a relative path, # typically requires LD_LIBRARY_PATH to be set appropriately @@ -446,7 +319,7 @@ class lammps(object): narg = 0 cargs = None if cmdargs: - cmdargs.insert(0,"lammps.py") + cmdargs.insert(0,"lammps") narg = len(cmdargs) for i in range(narg): if type(cmdargs[i]) is str: @@ -468,7 +341,7 @@ class lammps(object): self.comm = self.MPI.COMM_WORLD self.opened = 1 if cmdargs: - cmdargs.insert(0,"lammps.py") + cmdargs.insert(0,"lammps") narg = len(cmdargs) for i in range(narg): if type(cmdargs[i]) is str: @@ -528,6 +401,7 @@ class lammps(object): :rtype: numpy_wrapper """ if not self._numpy: + from .numpy import numpy_wrapper self._numpy = numpy_wrapper(self) return self._numpy @@ -1796,1131 +1670,3 @@ class lammps(object): computeid = computeid.encode() idx = self.lib.lammps_find_compute_neighlist(self.lmp, computeid, request) return idx - -# ------------------------------------------------------------------------- - -class numpy_wrapper: - """lammps API NumPy Wrapper - - This is a wrapper class that provides additional methods on top of an - existing :py:class:`lammps` instance. The methods transform raw ctypes - pointers into NumPy arrays, which give direct access to the - original data while protecting against out-of-bounds accesses. - - There is no need to explicitly instantiate this class. Each instance - of :py:class:`lammps` has a :py:attr:`numpy ` property - that returns an instance. - - :param lmp: instance of the :py:class:`lammps` class - :type lmp: lammps - """ - def __init__(self, lmp): - self.lmp = lmp - - # ------------------------------------------------------------------------- - - def _ctype_to_numpy_int(self, ctype_int): - import numpy as np - if ctype_int == c_int32: - return np.int32 - elif ctype_int == c_int64: - return np.int64 - return np.intc - - # ------------------------------------------------------------------------- - - def extract_atom(self, name, dtype=LAMMPS_AUTODETECT, nelem=LAMMPS_AUTODETECT, dim=LAMMPS_AUTODETECT): - """Retrieve per-atom properties from LAMMPS as NumPy arrays - - This is a wrapper around the :py:meth:`lammps.extract_atom()` method. - It behaves the same as the original method, but returns NumPy arrays - instead of ``ctypes`` pointers. - - .. note:: - - While the returned arrays of per-atom data are dimensioned - for the range [0:nmax] - as is the underlying storage - - the data is usually only valid for the range of [0:nlocal], - unless the property of interest is also updated for ghost - atoms. In some cases, this depends on a LAMMPS setting, see - for example :doc:`comm_modify vel yes `. - - :param name: name of the property - :type name: string - :param dtype: type of the returned data (see :ref:`py_datatype_constants`) - :type dtype: int, optional - :param nelem: number of elements in array - :type nelem: int, optional - :param dim: dimension of each element - :type dim: int, optional - :return: requested data as NumPy array with direct access to C data or None - :rtype: numpy.array or NoneType - """ - if dtype == LAMMPS_AUTODETECT: - dtype = self.lmp.extract_atom_datatype(name) - - if nelem == LAMMPS_AUTODETECT: - if name == "mass": - nelem = self.lmp.extract_global("ntypes") + 1 - else: - nelem = self.lmp.extract_global("nlocal") - if dim == LAMMPS_AUTODETECT: - if dtype in (LAMMPS_INT_2D, LAMMPS_DOUBLE_2D, LAMMPS_INT64_2D): - # TODO add other fields - if name in ("x", "v", "f", "angmom", "torque", "csforce", "vforce"): - dim = 3 - else: - dim = 2 - else: - dim = 1 - - raw_ptr = self.lmp.extract_atom(name, dtype) - - if dtype in (LAMMPS_DOUBLE, LAMMPS_DOUBLE_2D): - return self.darray(raw_ptr, nelem, dim) - elif dtype in (LAMMPS_INT, LAMMPS_INT_2D): - return self.iarray(c_int32, raw_ptr, nelem, dim) - elif dtype in (LAMMPS_INT64, LAMMPS_INT64_2D): - return self.iarray(c_int64, raw_ptr, nelem, dim) - return raw_ptr - - # ------------------------------------------------------------------------- - - def extract_atom_iarray(self, name, nelem, dim=1): - warnings.warn("deprecated, use extract_atom instead", DeprecationWarning) - - if name in ['id', 'molecule']: - c_int_type = self.lmp.c_tagint - elif name in ['image']: - c_int_type = self.lmp.c_imageint - else: - c_int_type = c_int - - if dim == 1: - raw_ptr = self.lmp.extract_atom(name, LAMMPS_INT) - else: - raw_ptr = self.lmp.extract_atom(name, LAMMPS_INT_2D) - - return self.iarray(c_int_type, raw_ptr, nelem, dim) - - # ------------------------------------------------------------------------- - - def extract_atom_darray(self, name, nelem, dim=1): - warnings.warn("deprecated, use extract_atom instead", DeprecationWarning) - - if dim == 1: - raw_ptr = self.lmp.extract_atom(name, LAMMPS_DOUBLE) - else: - raw_ptr = self.lmp.extract_atom(name, LAMMPS_DOUBLE_2D) - - return self.darray(raw_ptr, nelem, dim) - - # ------------------------------------------------------------------------- - - def extract_compute(self, cid, style, type): - """Retrieve data from a LAMMPS compute - - This is a wrapper around the - :py:meth:`lammps.extract_compute() ` method. - It behaves the same as the original method, but returns NumPy arrays - instead of ``ctypes`` pointers. - - :param id: compute ID - :type id: string - :param style: style of the data retrieve (global, atom, or local), see :ref:`py_style_constants` - :type style: int - :param type: type of the returned data (scalar, vector, or array), see :ref:`py_type_constants` - :type type: int - :return: requested data either as float, as NumPy array with direct access to C data, or None - :rtype: float, numpy.array, or NoneType - """ - value = self.lmp.extract_compute(cid, style, type) - - if style in (LMP_STYLE_GLOBAL, LMP_STYLE_LOCAL): - if type == LMP_TYPE_VECTOR: - nrows = self.lmp.extract_compute(cid, style, LMP_SIZE_VECTOR) - return self.darray(value, nrows) - elif type == LMP_TYPE_ARRAY: - nrows = self.lmp.extract_compute(cid, style, LMP_SIZE_ROWS) - ncols = self.lmp.extract_compute(cid, style, LMP_SIZE_COLS) - return self.darray(value, nrows, ncols) - elif style == LMP_STYLE_ATOM: - if type == LMP_TYPE_VECTOR: - nlocal = self.lmp.extract_global("nlocal") - return self.darray(value, nlocal) - elif type == LMP_TYPE_ARRAY: - nlocal = self.lmp.extract_global("nlocal") - ncols = self.lmp.extract_compute(cid, style, LMP_SIZE_COLS) - return self.darray(value, nlocal, ncols) - return value - - # ------------------------------------------------------------------------- - - def extract_fix(self, fid, style, type, nrow=0, ncol=0): - """Retrieve data from a LAMMPS fix - - This is a wrapper around the :py:meth:`lammps.extract_fix() ` method. - It behaves the same as the original method, but returns NumPy arrays - instead of ``ctypes`` pointers. - - :param id: fix ID - :type id: string - :param style: style of the data retrieve (global, atom, or local), see :ref:`py_style_constants` - :type style: int - :param type: type or size of the returned data (scalar, vector, or array), see :ref:`py_type_constants` - :type type: int - :param nrow: index of global vector element or row index of global array element - :type nrow: int - :param ncol: column index of global array element - :type ncol: int - :return: requested data - :rtype: integer or double value, pointer to 1d or 2d double array or None - - """ - value = self.lmp.extract_fix(fid, style, type, nrow, ncol) - if style == LMP_STYLE_ATOM: - if type == LMP_TYPE_VECTOR: - nlocal = self.lmp.extract_global("nlocal") - return self.darray(value, nlocal) - elif type == LMP_TYPE_ARRAY: - nlocal = self.lmp.extract_global("nlocal") - ncols = self.lmp.extract_fix(fid, style, LMP_SIZE_COLS, 0, 0) - return self.darray(value, nlocal, ncols) - elif style == LMP_STYLE_LOCAL: - if type == LMP_TYPE_VECTOR: - nrows = self.lmp.extract_fix(fid, style, LMP_SIZE_ROWS, 0, 0) - return self.darray(value, nrows) - elif type == LMP_TYPE_ARRAY: - nrows = self.lmp.extract_fix(fid, style, LMP_SIZE_ROWS, 0, 0) - ncols = self.lmp.extract_fix(fid, style, LMP_SIZE_COLS, 0, 0) - return self.darray(value, nrows, ncols) - return value - - # ------------------------------------------------------------------------- - - def extract_variable(self, name, group=None, vartype=LMP_VAR_EQUAL): - """ Evaluate a LAMMPS variable and return its data - - This function is a wrapper around the function - :py:meth:`lammps.extract_variable() ` - method. It behaves the same as the original method, but returns NumPy arrays - instead of ``ctypes`` pointers. - - :param name: name of the variable to execute - :type name: string - :param group: name of group for atom-style variable (ignored for equal-style variables) - :type group: string - :param vartype: type of variable, see :ref:`py_vartype_constants` - :type vartype: int - :return: the requested data or None - :rtype: c_double, numpy.array, or NoneType - """ - import numpy as np - value = self.lmp.extract_variable(name, group, vartype) - if vartype == LMP_VAR_ATOM: - return np.ctypeslib.as_array(value) - return value - - # ------------------------------------------------------------------------- - - def get_neighlist(self, idx): - """Returns an instance of :class:`NumPyNeighList` which wraps access to the neighbor list with the given index - - :param idx: index of neighbor list - :type idx: int - :return: an instance of :class:`NumPyNeighList` wrapping access to neighbor list data - :rtype: NumPyNeighList - """ - if idx < 0: - return None - return NumPyNeighList(self.lmp, idx) - - # ------------------------------------------------------------------------- - - def get_neighlist_element_neighbors(self, idx, element): - """Return data of neighbor list entry - - This function is a wrapper around the function - :py:meth:`lammps.get_neighlist_element_neighbors() ` - method. It behaves the same as the original method, but returns a NumPy array containing the neighbors - instead of a ``ctypes`` pointer. - - :param element: neighbor list index - :type element: int - :param element: neighbor list element index - :type element: int - :return: tuple with atom local index and numpy array of neighbor local atom indices - :rtype: (int, numpy.array) - """ - iatom, numneigh, c_neighbors = self.lmp.get_neighlist_element_neighbors(idx, element) - neighbors = self.iarray(c_int, c_neighbors, numneigh, 1) - return iatom, neighbors - - # ------------------------------------------------------------------------- - - def iarray(self, c_int_type, raw_ptr, nelem, dim=1): - import numpy as np - np_int_type = self._ctype_to_numpy_int(c_int_type) - - if dim == 1: - ptr = cast(raw_ptr, POINTER(c_int_type * nelem)) - else: - ptr = cast(raw_ptr[0], POINTER(c_int_type * nelem * dim)) - - a = np.frombuffer(ptr.contents, dtype=np_int_type) - a.shape = (nelem, dim) - return a - - # ------------------------------------------------------------------------- - - def darray(self, raw_ptr, nelem, dim=1): - import numpy as np - if dim == 1: - ptr = cast(raw_ptr, POINTER(c_double * nelem)) - else: - ptr = cast(raw_ptr[0], POINTER(c_double * nelem * dim)) - - a = np.frombuffer(ptr.contents) - a.shape = (nelem, dim) - return a - - -# ------------------------------------------------------------------------- -# ------------------------------------------------------------------------- -# ------------------------------------------------------------------------- - -################################################################################ -# Alternative Python Wrapper -# Written by Richard Berger -################################################################################ - -class OutputCapture(object): - """ Utility class to capture LAMMPS library output """ - - def __init__(self): - self.stdout_pipe_read, self.stdout_pipe_write = os.pipe() - self.stdout_fd = 1 - - def __enter__(self): - self.stdout = os.dup(self.stdout_fd) - os.dup2(self.stdout_pipe_write, self.stdout_fd) - return self - - def __exit__(self, type, value, tracebac): - os.dup2(self.stdout, self.stdout_fd) - os.close(self.stdout) - os.close(self.stdout_pipe_read) - os.close(self.stdout_pipe_write) - - # check if we have more to read from the pipe - def more_data(self, pipe): - r, _, _ = select.select([pipe], [], [], 0) - return bool(r) - - # read the whole pipe - def read_pipe(self, pipe): - out = "" - while self.more_data(pipe): - out += os.read(pipe, 1024).decode() - return out - - @property - def output(self): - return self.read_pipe(self.stdout_pipe_read) - -# ------------------------------------------------------------------------- - -class Variable(object): - def __init__(self, pylammps_instance, name, style, definition): - self._pylmp = pylammps_instance - self.name = name - self.style = style - self.definition = definition.split() - - @property - def value(self): - if self.style == 'atom': - return list(self._pylmp.lmp.extract_variable(self.name, "all", 1)) - else: - value = self._pylmp.lmp_print('"${%s}"' % self.name).strip() - try: - return float(value) - except ValueError: - return value - -# ------------------------------------------------------------------------- - -class AtomList(object): - """ - A dynamic list of atoms that returns either an :py:class:`Atom` or - :py:class:`Atom2D` instance for each atom. Instances are only allocated - when accessed. - - :ivar natoms: total number of atoms - :ivar dimensions: number of dimensions in system - """ - def __init__(self, pylammps_instance): - self._pylmp = pylammps_instance - self.natoms = self._pylmp.system.natoms - self.dimensions = self._pylmp.system.dimensions - self._loaded = {} - - def __getitem__(self, index): - """ - Return Atom with given local index - - :param index: Local index of atom - :type index: int - :rtype: Atom or Atom2D - """ - if index not in self._loaded: - if self.dimensions == 2: - atom = Atom2D(self._pylmp, index + 1) - else: - atom = Atom(self._pylmp, index + 1) - self._loaded[index] = atom - return self._loaded[index] - - def __len__(self): - return self.natoms - - -# ------------------------------------------------------------------------- - -class Atom(object): - """ - A wrapper class then represents a single atom inside of LAMMPS - - It provides access to properties of the atom and allows you to change some of them. - """ - def __init__(self, pylammps_instance, index): - self._pylmp = pylammps_instance - self.index = index - - @property - def id(self): - """ - Return the atom ID - - :type: int - """ - return int(self._pylmp.eval("id[%d]" % self.index)) - - @property - def type(self): - """ - Return the atom type - - :type: int - """ - return int(self._pylmp.eval("type[%d]" % self.index)) - - @property - def mol(self): - """ - Return the atom molecule index - - :type: int - """ - return self._pylmp.eval("mol[%d]" % self.index) - - @property - def mass(self): - """ - Return the atom mass - - :type: float - """ - return self._pylmp.eval("mass[%d]" % self.index) - - @property - def position(self): - """ - :getter: Return position of atom - :setter: Set position of atom - :type: tuple (float, float, float) - """ - return (self._pylmp.eval("x[%d]" % self.index), - self._pylmp.eval("y[%d]" % self.index), - self._pylmp.eval("z[%d]" % self.index)) - - @position.setter - def position(self, value): - """ - :getter: Return velocity of atom - :setter: Set velocity of atom - :type: tuple (float, float, float) - """ - self._pylmp.set("atom", self.index, "x", value[0]) - self._pylmp.set("atom", self.index, "y", value[1]) - self._pylmp.set("atom", self.index, "z", value[2]) - - @property - def velocity(self): - return (self._pylmp.eval("vx[%d]" % self.index), - self._pylmp.eval("vy[%d]" % self.index), - self._pylmp.eval("vz[%d]" % self.index)) - - @velocity.setter - def velocity(self, value): - self._pylmp.set("atom", self.index, "vx", value[0]) - self._pylmp.set("atom", self.index, "vy", value[1]) - self._pylmp.set("atom", self.index, "vz", value[2]) - - @property - def force(self): - """ - Return the total force acting on the atom - - :type: tuple (float, float, float) - """ - return (self._pylmp.eval("fx[%d]" % self.index), - self._pylmp.eval("fy[%d]" % self.index), - self._pylmp.eval("fz[%d]" % self.index)) - - @property - def charge(self): - """ - Return the atom charge - - :type: float - """ - return self._pylmp.eval("q[%d]" % self.index) - -# ------------------------------------------------------------------------- - -class Atom2D(Atom): - """ - A wrapper class then represents a single 2D atom inside of LAMMPS - - Inherits all properties from the :py:class:`Atom` class, but returns 2D versions - of position, velocity, and force. - - It provides access to properties of the atom and allows you to change some of them. - """ - def __init__(self, pylammps_instance, index): - super(Atom2D, self).__init__(pylammps_instance, index) - - @property - def position(self): - """ - :getter: Return position of atom - :setter: Set position of atom - :type: tuple (float, float) - """ - return (self._pylmp.eval("x[%d]" % self.index), - self._pylmp.eval("y[%d]" % self.index)) - - @position.setter - def position(self, value): - self._pylmp.set("atom", self.index, "x", value[0]) - self._pylmp.set("atom", self.index, "y", value[1]) - - @property - def velocity(self): - """ - :getter: Return velocity of atom - :setter: Set velocity of atom - :type: tuple (float, float) - """ - return (self._pylmp.eval("vx[%d]" % self.index), - self._pylmp.eval("vy[%d]" % self.index)) - - @velocity.setter - def velocity(self, value): - self._pylmp.set("atom", self.index, "vx", value[0]) - self._pylmp.set("atom", self.index, "vy", value[1]) - - @property - def force(self): - """ - Return the total force acting on the atom - - :type: tuple (float, float) - """ - return (self._pylmp.eval("fx[%d]" % self.index), - self._pylmp.eval("fy[%d]" % self.index)) - -# ------------------------------------------------------------------------- - -class variable_set: - def __init__(self, name, variable_dict): - self._name = name - array_pattern = re.compile(r"(?P.+)\[(?P[0-9]+)\]") - - for key, value in variable_dict.items(): - m = array_pattern.match(key) - if m: - g = m.groupdict() - varname = g['arr'] - idx = int(g['index']) - if varname not in self.__dict__: - self.__dict__[varname] = {} - self.__dict__[varname][idx] = value - else: - self.__dict__[key] = value - - def __str__(self): - return "{}({})".format(self._name, ','.join(["{}={}".format(k, self.__dict__[k]) for k in self.__dict__.keys() if not k.startswith('_')])) - - def __repr__(self): - return self.__str__() - -# ------------------------------------------------------------------------- - -def get_thermo_data(output): - """ traverse output of runs and extract thermo data columns """ - if isinstance(output, str): - lines = output.splitlines() - else: - lines = output - - runs = [] - columns = [] - in_run = False - current_run = {} - - for line in lines: - if line.startswith("Per MPI rank memory allocation"): - in_run = True - elif in_run and len(columns) == 0: - # first line after memory usage are column names - columns = line.split() - - current_run = {} - - for col in columns: - current_run[col] = [] - - elif line.startswith("Loop time of "): - in_run = False - columns = None - thermo_data = variable_set('ThermoData', current_run) - r = {'thermo' : thermo_data } - runs.append(namedtuple('Run', list(r.keys()))(*list(r.values()))) - elif in_run and len(columns) > 0: - items = line.split() - # Convert thermo output and store it. - # It must have the same number of columns and - # all of them must be convertible to floats. - # Otherwise we ignore the line - if len(items) == len(columns): - try: - values = [float(x) for x in items] - for i, col in enumerate(columns): - current_run[col].append(values[i]) - except ValueError: - pass - - return runs - -# ------------------------------------------------------------------------- -# ------------------------------------------------------------------------- - -class PyLammps(object): - """ - This is a Python wrapper class around the lower-level - :py:class:`lammps` class, exposing a more Python-like, - object-oriented interface for prototyping system inside of IPython and - Jupyter notebooks. - - It either creates its own instance of :py:class:`lammps` or can be - initialized with an existing instance. The arguments are the same of the - lower-level interface. The original interface can still be accessed via - :py:attr:`PyLammps.lmp`. - - :param name: "machine" name of the shared LAMMPS library ("mpi" loads ``liblammps_mpi.so``, "" loads ``liblammps.so``) - :type name: string - :param cmdargs: list of command line arguments to be passed to the :cpp:func:`lammps_open` function. The executable name is automatically added. - :type cmdargs: list - :param ptr: pointer to a LAMMPS C++ class instance when called from an embedded Python interpreter. None means load symbols from shared library. - :type ptr: pointer - :param comm: MPI communicator (as provided by `mpi4py `_). ``None`` means use ``MPI_COMM_WORLD`` implicitly. - :type comm: MPI_Comm - - :ivar lmp: instance of original LAMMPS Python interface - :vartype lmp: :py:class:`lammps` - - :ivar runs: list of completed runs, each storing the thermo output - :vartype run: list - """ - - def __init__(self, name="", cmdargs=None, ptr=None, comm=None): - self.has_echo = False - - if cmdargs: - if '-echo' in cmdargs: - idx = cmdargs.index('-echo') - # ensures that echo line is ignored during output capture - self.has_echo = idx+1 < len(cmdargs) and cmdargs[idx+1] in ('screen', 'both') - - if ptr: - if isinstance(ptr,PyLammps): - self.lmp = ptr.lmp - elif isinstance(ptr,lammps): - self.lmp = ptr - else: - self.lmp = lammps(name=name,cmdargs=cmdargs,ptr=ptr,comm=comm) - else: - self.lmp = lammps(name=name,cmdargs=cmdargs,ptr=None,comm=comm) - print("LAMMPS output is captured by PyLammps wrapper") - self._cmd_history = [] - self.runs = [] - - def __del__(self): - if self.lmp: self.lmp.close() - self.lmp = None - - def close(self): - """Explicitly delete a LAMMPS instance - - This is a wrapper around the :py:meth:`lammps.close` of the Python interface. - """ - if self.lmp: self.lmp.close() - self.lmp = None - - def version(self): - """Return a numerical representation of the LAMMPS version in use. - - This is a wrapper around the :py:meth:`lammps.version` function of the Python interface. - - :return: version number - :rtype: int - """ - return self.lmp.version() - - def file(self, file): - """Read LAMMPS commands from a file. - - This is a wrapper around the :py:meth:`lammps.file` function of the Python interface. - - :param path: Name of the file/path with LAMMPS commands - :type path: string - """ - self.lmp.file(file) - - def write_script(self, filepath): - """ - Write LAMMPS script file containing all commands executed up until now - - :param filepath: path to script file that should be written - :type filepath: string - """ - with open(filepath, "w") as f: - for cmd in self._cmd_history: - print(cmd, file=f) - - def command(self, cmd): - """ - Execute LAMMPS command - - All commands executed will be stored in a command history which can be - written to a file using :py:meth:`PyLammps.write_script()` - - :param cmd: command string that should be executed - :type: cmd: string - """ - self.lmp.command(cmd) - self._cmd_history.append(cmd) - - def run(self, *args, **kwargs): - """ - Execute LAMMPS run command with given arguments - - All thermo output during the run is captured and saved as new entry in - :py:attr:`PyLammps.runs`. The latest run can be retrieved by - :py:attr:`PyLammps.last_run`. - """ - output = self.__getattr__('run')(*args, **kwargs) - - comm = self.lmp.get_mpi_comm() - if comm: - output = self.lmp.comm.bcast(output, root=0) - - self.runs += get_thermo_data(output) - return output - - @property - def last_run(self): - """ - Return data produced of last completed run command - - :getter: Returns an object containing information about the last run command - :type: dict - """ - if len(self.runs) > 0: - return self.runs[-1] - return None - - @property - def atoms(self): - """ - All atoms of this LAMMPS instance - - :getter: Returns a list of atoms currently in the system - :type: AtomList - """ - return AtomList(self) - - @property - def system(self): - """ - The system state of this LAMMPS instance - - :getter: Returns an object with properties storing the current system state - :type: namedtuple - """ - output = self.info("system") - d = self._parse_info_system(output) - return namedtuple('System', d.keys())(*d.values()) - - @property - def communication(self): - """ - The communication state of this LAMMPS instance - - :getter: Returns an object with properties storing the current communication state - :type: namedtuple - """ - output = self.info("communication") - d = self._parse_info_communication(output) - return namedtuple('Communication', d.keys())(*d.values()) - - @property - def computes(self): - """ - The list of active computes of this LAMMPS instance - - :getter: Returns a list of computes that are currently active in this LAMMPS instance - :type: list - """ - output = self.info("computes") - return self._parse_element_list(output) - - @property - def dumps(self): - """ - The list of active dumps of this LAMMPS instance - - :getter: Returns a list of dumps that are currently active in this LAMMPS instance - :type: list - """ - output = self.info("dumps") - return self._parse_element_list(output) - - @property - def fixes(self): - """ - The list of active fixes of this LAMMPS instance - - :getter: Returns a list of fixes that are currently active in this LAMMPS instance - :type: list - """ - output = self.info("fixes") - return self._parse_element_list(output) - - @property - def groups(self): - """ - The list of active atom groups of this LAMMPS instance - - :getter: Returns a list of atom groups that are currently active in this LAMMPS instance - :type: list - """ - output = self.info("groups") - return self._parse_groups(output) - - @property - def variables(self): - """ - Returns a dictionary of all variables defined in the current LAMMPS instance - - :getter: Returns a dictionary of all variables that are defined in this LAMMPS instance - :type: dict - """ - output = self.info("variables") - vars = {} - for v in self._parse_element_list(output): - vars[v['name']] = Variable(self, v['name'], v['style'], v['def']) - return vars - - def eval(self, expr): - """ - Evaluate expression - - :param expr: the expression string that should be evaluated inside of LAMMPS - :type expr: string - - :return: the value of the evaluated expression - :rtype: float if numeric, string otherwise - """ - value = self.lmp_print('"$(%s)"' % expr).strip() - try: - return float(value) - except ValueError: - return value - - def _split_values(self, line): - return [x.strip() for x in line.split(',')] - - def _get_pair(self, value): - return [x.strip() for x in value.split('=')] - - def _parse_info_system(self, output): - lines = output[6:-2] - system = {} - - for line in lines: - if line.startswith("Units"): - system['units'] = self._get_pair(line)[1] - elif line.startswith("Atom style"): - system['atom_style'] = self._get_pair(line)[1] - elif line.startswith("Atom map"): - system['atom_map'] = self._get_pair(line)[1] - elif line.startswith("Atoms"): - parts = self._split_values(line) - system['natoms'] = int(self._get_pair(parts[0])[1]) - system['ntypes'] = int(self._get_pair(parts[1])[1]) - system['style'] = self._get_pair(parts[2])[1] - elif line.startswith("Kspace style"): - system['kspace_style'] = self._get_pair(line)[1] - elif line.startswith("Dimensions"): - system['dimensions'] = int(self._get_pair(line)[1]) - elif line.startswith("Orthogonal box"): - system['orthogonal_box'] = [float(x) for x in self._get_pair(line)[1].split('x')] - elif line.startswith("Boundaries"): - system['boundaries'] = self._get_pair(line)[1] - elif line.startswith("xlo"): - keys, values = [self._split_values(x) for x in self._get_pair(line)] - for key, value in zip(keys, values): - system[key] = float(value) - elif line.startswith("ylo"): - keys, values = [self._split_values(x) for x in self._get_pair(line)] - for key, value in zip(keys, values): - system[key] = float(value) - elif line.startswith("zlo"): - keys, values = [self._split_values(x) for x in self._get_pair(line)] - for key, value in zip(keys, values): - system[key] = float(value) - elif line.startswith("Molecule type"): - system['molecule_type'] = self._get_pair(line)[1] - elif line.startswith("Bonds"): - parts = self._split_values(line) - system['nbonds'] = int(self._get_pair(parts[0])[1]) - system['nbondtypes'] = int(self._get_pair(parts[1])[1]) - system['bond_style'] = self._get_pair(parts[2])[1] - elif line.startswith("Angles"): - parts = self._split_values(line) - system['nangles'] = int(self._get_pair(parts[0])[1]) - system['nangletypes'] = int(self._get_pair(parts[1])[1]) - system['angle_style'] = self._get_pair(parts[2])[1] - elif line.startswith("Dihedrals"): - parts = self._split_values(line) - system['ndihedrals'] = int(self._get_pair(parts[0])[1]) - system['ndihedraltypes'] = int(self._get_pair(parts[1])[1]) - system['dihedral_style'] = self._get_pair(parts[2])[1] - elif line.startswith("Impropers"): - parts = self._split_values(line) - system['nimpropers'] = int(self._get_pair(parts[0])[1]) - system['nimpropertypes'] = int(self._get_pair(parts[1])[1]) - system['improper_style'] = self._get_pair(parts[2])[1] - - return system - - def _parse_info_communication(self, output): - lines = output[6:-3] - comm = {} - - for line in lines: - if line.startswith("MPI library"): - comm['mpi_version'] = line.split(':')[1].strip() - elif line.startswith("Comm style"): - parts = self._split_values(line) - comm['comm_style'] = self._get_pair(parts[0])[1] - comm['comm_layout'] = self._get_pair(parts[1])[1] - elif line.startswith("Processor grid"): - comm['proc_grid'] = [int(x) for x in self._get_pair(line)[1].split('x')] - elif line.startswith("Communicate velocities for ghost atoms"): - comm['ghost_velocity'] = (self._get_pair(line)[1] == "yes") - elif line.startswith("Nprocs"): - parts = self._split_values(line) - comm['nprocs'] = int(self._get_pair(parts[0])[1]) - comm['nthreads'] = int(self._get_pair(parts[1])[1]) - return comm - - def _parse_element_list(self, output): - lines = output[6:-3] - elements = [] - - for line in lines: - element_info = self._split_values(line.split(':')[1].strip()) - element = {'name': element_info[0]} - for key, value in [self._get_pair(x) for x in element_info[1:]]: - element[key] = value - elements.append(element) - return elements - - def _parse_groups(self, output): - lines = output[6:-3] - groups = [] - group_pattern = re.compile(r"(?P.+) \((?P.+)\)") - - for line in lines: - m = group_pattern.match(line.split(':')[1].strip()) - group = {'name': m.group('name'), 'type': m.group('type')} - groups.append(group) - return groups - - def lmp_print(self, s): - """ needed for Python2 compatibility, since print is a reserved keyword """ - return self.__getattr__("print")(s) - - def __dir__(self): - return ['angle_coeff', 'angle_style', 'atom_modify', 'atom_style', 'atom_style', - 'bond_coeff', 'bond_style', 'boundary', 'change_box', 'communicate', 'compute', - 'create_atoms', 'create_box', 'delete_atoms', 'delete_bonds', 'dielectric', - 'dihedral_coeff', 'dihedral_style', 'dimension', 'dump', 'fix', 'fix_modify', - 'group', 'improper_coeff', 'improper_style', 'include', 'kspace_modify', - 'kspace_style', 'lattice', 'mass', 'minimize', 'min_style', 'neighbor', - 'neigh_modify', 'newton', 'nthreads', 'pair_coeff', 'pair_modify', - 'pair_style', 'processors', 'read', 'read_data', 'read_restart', 'region', - 'replicate', 'reset_timestep', 'restart', 'run', 'run_style', 'thermo', - 'thermo_modify', 'thermo_style', 'timestep', 'undump', 'unfix', 'units', - 'variable', 'velocity', 'write_restart'] - - def __getattr__(self, name): - """ - This method is where the Python 'magic' happens. If a method is not - defined by the class PyLammps, it assumes it is a LAMMPS command. It takes - all the arguments, concatinates them to a single string, and executes it using - :py:meth:`lammps.PyLammps.command()`. - - :param verbose: Print output of command - :type verbose: bool - :return: line or list of lines of output, None if no output - :rtype: list or string - """ - def handler(*args, **kwargs): - cmd_args = [name] + [str(x) for x in args] - - with OutputCapture() as capture: - cmd = ' '.join(cmd_args) - self.command(cmd) - output = capture.output - - if 'verbose' in kwargs and kwargs['verbose']: - print(output) - - lines = output.splitlines() - - if self.has_echo: - lines = lines[1:] - - if len(lines) > 1: - return lines - elif len(lines) == 1: - return lines[0] - return None - - return handler - - -class IPyLammps(PyLammps): - """ - IPython wrapper for LAMMPS which adds embedded graphics capabilities to PyLammmps interface - - It either creates its own instance of :py:class:`lammps` or can be - initialized with an existing instance. The arguments are the same of the - lower-level interface. The original interface can still be accessed via - :py:attr:`PyLammps.lmp`. - - :param name: "machine" name of the shared LAMMPS library ("mpi" loads ``liblammps_mpi.so``, "" loads ``liblammps.so``) - :type name: string - :param cmdargs: list of command line arguments to be passed to the :cpp:func:`lammps_open` function. The executable name is automatically added. - :type cmdargs: list - :param ptr: pointer to a LAMMPS C++ class instance when called from an embedded Python interpreter. None means load symbols from shared library. - :type ptr: pointer - :param comm: MPI communicator (as provided by `mpi4py `_). ``None`` means use ``MPI_COMM_WORLD`` implicitly. - :type comm: MPI_Comm - """ - - def __init__(self,name="",cmdargs=None,ptr=None,comm=None): - super(IPyLammps, self).__init__(name=name,cmdargs=cmdargs,ptr=ptr,comm=comm) - - def image(self, filename="snapshot.png", group="all", color="type", diameter="type", - size=None, view=None, center=None, up=None, zoom=1.0, background_color="white"): - """ Generate image using write_dump command and display it - - See :doc:`dump image ` for more information. - - :param filename: Name of the image file that should be generated. The extension determines whether it is PNG or JPEG - :type filename: string - :param group: the group of atoms write_image should use - :type group: string - :param color: name of property used to determine color - :type color: string - :param diameter: name of property used to determine atom diameter - :type diameter: string - :param size: dimensions of image - :type size: tuple (width, height) - :param view: view parameters - :type view: tuple (theta, phi) - :param center: center parameters - :type center: tuple (flag, center_x, center_y, center_z) - :param up: vector pointing to up direction - :type up: tuple (up_x, up_y, up_z) - :param zoom: zoom factor - :type zoom: float - :param background_color: background color of scene - :type background_color: string - - :return: Image instance used to display image in notebook - :rtype: :py:class:`IPython.core.display.Image` - """ - cmd_args = [group, "image", filename, color, diameter] - - if size: - width = size[0] - height = size[1] - cmd_args += ["size", width, height] - - if view: - theta = view[0] - phi = view[1] - cmd_args += ["view", theta, phi] - - if center: - flag = center[0] - Cx = center[1] - Cy = center[2] - Cz = center[3] - cmd_args += ["center", flag, Cx, Cy, Cz] - - if up: - Ux = up[0] - Uy = up[1] - Uz = up[2] - cmd_args += ["up", Ux, Uy, Uz] - - if zoom: - cmd_args += ["zoom", zoom] - - cmd_args.append("modify backcolor " + background_color) - - self.write_dump(*cmd_args) - from IPython.core.display import Image - return Image(filename) - - def video(self, filename): - """ - Load video from file - - Can be used to visualize videos from :doc:`dump movie `. - - :param filename: Path to video file - :type filename: string - :return: HTML Video Tag used by notebook to embed a video - :rtype: :py:class:`IPython.display.HTML` - """ - from IPython.display import HTML - return HTML("") diff --git a/python/lammps/data.py b/python/lammps/data.py new file mode 100644 index 0000000000..2cf100ed82 --- /dev/null +++ b/python/lammps/data.py @@ -0,0 +1,73 @@ +# ---------------------------------------------------------------------- +# LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator +# http://lammps.sandia.gov, Sandia National Laboratories +# Steve Plimpton, sjplimp@sandia.gov +# +# Copyright (2003) Sandia Corporation. Under the terms of Contract +# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains +# certain rights in this software. This software is distributed under +# the GNU General Public License. +# +# See the README file in the top-level LAMMPS directory. +# ------------------------------------------------------------------------- + +################################################################################ +# LAMMPS data structures +# Written by Richard Berger +################################################################################ + +class NeighList: + """This is a wrapper class that exposes the contents of a neighbor list. + + It can be used like a regular Python list. Each element is a tuple of: + + * the atom local index + * its number of neighbors + * and a pointer to an c_int array containing local atom indices of its + neighbors + + Internally it uses the lower-level LAMMPS C-library interface. + + :param lmp: reference to instance of :py:class:`lammps` + :type lmp: lammps + :param idx: neighbor list index + :type idx: int + """ + def __init__(self, lmp, idx): + self.lmp = lmp + self.idx = idx + + def __str__(self): + return "Neighbor List ({} atoms)".format(self.size) + + def __repr__(self): + return self.__str__() + + @property + def size(self): + """ + :return: number of elements in neighbor list + """ + return self.lmp.get_neighlist_size(self.idx) + + def get(self, element): + """ + :return: tuple with atom local index, numpy array of neighbor local atom indices + :rtype: (int, int, ctypes.POINTER(c_int)) + """ + iatom, numneigh, neighbors = self.lmp.get_neighlist_element_neighbors(self.idx, element) + return iatom, numneigh, neighbors + + # the methods below implement the iterator interface, so NeighList can be used like a regular Python list + + def __getitem__(self, element): + return self.get(element) + + def __len__(self): + return self.size + + def __iter__(self): + inum = self.size + + for ii in range(inum): + yield self.get(ii) diff --git a/python/lammps/numpy.py b/python/lammps/numpy.py new file mode 100644 index 0000000000..ce64d68c90 --- /dev/null +++ b/python/lammps/numpy.py @@ -0,0 +1,338 @@ +# ---------------------------------------------------------------------- +# LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator +# http://lammps.sandia.gov, Sandia National Laboratories +# Steve Plimpton, sjplimp@sandia.gov +# +# Copyright (2003) Sandia Corporation. Under the terms of Contract +# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains +# certain rights in this software. This software is distributed under +# the GNU General Public License. +# +# See the README file in the top-level LAMMPS directory. +# ------------------------------------------------------------------------- + +################################################################################ +# NumPy additions +# Written by Richard Berger +################################################################################ + +import warnings +from ctypes import POINTER, c_double, c_int, c_int32, c_int64, cast + + +from .constants import * +from .data import NeighList + + +class numpy_wrapper: + """lammps API NumPy Wrapper + + This is a wrapper class that provides additional methods on top of an + existing :py:class:`lammps` instance. The methods transform raw ctypes + pointers into NumPy arrays, which give direct access to the + original data while protecting against out-of-bounds accesses. + + There is no need to explicitly instantiate this class. Each instance + of :py:class:`lammps` has a :py:attr:`numpy ` property + that returns an instance. + + :param lmp: instance of the :py:class:`lammps` class + :type lmp: lammps + """ + def __init__(self, lmp): + self.lmp = lmp + + # ------------------------------------------------------------------------- + + def _ctype_to_numpy_int(self, ctype_int): + import numpy as np + if ctype_int == c_int32: + return np.int32 + elif ctype_int == c_int64: + return np.int64 + return np.intc + + # ------------------------------------------------------------------------- + + def extract_atom(self, name, dtype=LAMMPS_AUTODETECT, nelem=LAMMPS_AUTODETECT, dim=LAMMPS_AUTODETECT): + """Retrieve per-atom properties from LAMMPS as NumPy arrays + + This is a wrapper around the :py:meth:`lammps.extract_atom()` method. + It behaves the same as the original method, but returns NumPy arrays + instead of ``ctypes`` pointers. + + .. note:: + + While the returned arrays of per-atom data are dimensioned + for the range [0:nmax] - as is the underlying storage - + the data is usually only valid for the range of [0:nlocal], + unless the property of interest is also updated for ghost + atoms. In some cases, this depends on a LAMMPS setting, see + for example :doc:`comm_modify vel yes `. + + :param name: name of the property + :type name: string + :param dtype: type of the returned data (see :ref:`py_datatype_constants`) + :type dtype: int, optional + :param nelem: number of elements in array + :type nelem: int, optional + :param dim: dimension of each element + :type dim: int, optional + :return: requested data as NumPy array with direct access to C data or None + :rtype: numpy.array or NoneType + """ + if dtype == LAMMPS_AUTODETECT: + dtype = self.lmp.extract_atom_datatype(name) + + if nelem == LAMMPS_AUTODETECT: + if name == "mass": + nelem = self.lmp.extract_global("ntypes") + 1 + else: + nelem = self.lmp.extract_global("nlocal") + if dim == LAMMPS_AUTODETECT: + if dtype in (LAMMPS_INT_2D, LAMMPS_DOUBLE_2D, LAMMPS_INT64_2D): + # TODO add other fields + if name in ("x", "v", "f", "angmom", "torque", "csforce", "vforce"): + dim = 3 + else: + dim = 2 + else: + dim = 1 + + raw_ptr = self.lmp.extract_atom(name, dtype) + + if dtype in (LAMMPS_DOUBLE, LAMMPS_DOUBLE_2D): + return self.darray(raw_ptr, nelem, dim) + elif dtype in (LAMMPS_INT, LAMMPS_INT_2D): + return self.iarray(c_int32, raw_ptr, nelem, dim) + elif dtype in (LAMMPS_INT64, LAMMPS_INT64_2D): + return self.iarray(c_int64, raw_ptr, nelem, dim) + return raw_ptr + + # ------------------------------------------------------------------------- + + def extract_atom_iarray(self, name, nelem, dim=1): + warnings.warn("deprecated, use extract_atom instead", DeprecationWarning) + + if name in ['id', 'molecule']: + c_int_type = self.lmp.c_tagint + elif name in ['image']: + c_int_type = self.lmp.c_imageint + else: + c_int_type = c_int + + if dim == 1: + raw_ptr = self.lmp.extract_atom(name, LAMMPS_INT) + else: + raw_ptr = self.lmp.extract_atom(name, LAMMPS_INT_2D) + + return self.iarray(c_int_type, raw_ptr, nelem, dim) + + # ------------------------------------------------------------------------- + + def extract_atom_darray(self, name, nelem, dim=1): + warnings.warn("deprecated, use extract_atom instead", DeprecationWarning) + + if dim == 1: + raw_ptr = self.lmp.extract_atom(name, LAMMPS_DOUBLE) + else: + raw_ptr = self.lmp.extract_atom(name, LAMMPS_DOUBLE_2D) + + return self.darray(raw_ptr, nelem, dim) + + # ------------------------------------------------------------------------- + + def extract_compute(self, cid, style, type): + """Retrieve data from a LAMMPS compute + + This is a wrapper around the + :py:meth:`lammps.extract_compute() ` method. + It behaves the same as the original method, but returns NumPy arrays + instead of ``ctypes`` pointers. + + :param id: compute ID + :type id: string + :param style: style of the data retrieve (global, atom, or local), see :ref:`py_style_constants` + :type style: int + :param type: type of the returned data (scalar, vector, or array), see :ref:`py_type_constants` + :type type: int + :return: requested data either as float, as NumPy array with direct access to C data, or None + :rtype: float, numpy.array, or NoneType + """ + value = self.lmp.extract_compute(cid, style, type) + + if style in (LMP_STYLE_GLOBAL, LMP_STYLE_LOCAL): + if type == LMP_TYPE_VECTOR: + nrows = self.lmp.extract_compute(cid, style, LMP_SIZE_VECTOR) + return self.darray(value, nrows) + elif type == LMP_TYPE_ARRAY: + nrows = self.lmp.extract_compute(cid, style, LMP_SIZE_ROWS) + ncols = self.lmp.extract_compute(cid, style, LMP_SIZE_COLS) + return self.darray(value, nrows, ncols) + elif style == LMP_STYLE_ATOM: + if type == LMP_TYPE_VECTOR: + nlocal = self.lmp.extract_global("nlocal") + return self.darray(value, nlocal) + elif type == LMP_TYPE_ARRAY: + nlocal = self.lmp.extract_global("nlocal") + ncols = self.lmp.extract_compute(cid, style, LMP_SIZE_COLS) + return self.darray(value, nlocal, ncols) + return value + + # ------------------------------------------------------------------------- + + def extract_fix(self, fid, style, type, nrow=0, ncol=0): + """Retrieve data from a LAMMPS fix + + This is a wrapper around the :py:meth:`lammps.extract_fix() ` method. + It behaves the same as the original method, but returns NumPy arrays + instead of ``ctypes`` pointers. + + :param id: fix ID + :type id: string + :param style: style of the data retrieve (global, atom, or local), see :ref:`py_style_constants` + :type style: int + :param type: type or size of the returned data (scalar, vector, or array), see :ref:`py_type_constants` + :type type: int + :param nrow: index of global vector element or row index of global array element + :type nrow: int + :param ncol: column index of global array element + :type ncol: int + :return: requested data + :rtype: integer or double value, pointer to 1d or 2d double array or None + + """ + value = self.lmp.extract_fix(fid, style, type, nrow, ncol) + if style == LMP_STYLE_ATOM: + if type == LMP_TYPE_VECTOR: + nlocal = self.lmp.extract_global("nlocal") + return self.darray(value, nlocal) + elif type == LMP_TYPE_ARRAY: + nlocal = self.lmp.extract_global("nlocal") + ncols = self.lmp.extract_fix(fid, style, LMP_SIZE_COLS, 0, 0) + return self.darray(value, nlocal, ncols) + elif style == LMP_STYLE_LOCAL: + if type == LMP_TYPE_VECTOR: + nrows = self.lmp.extract_fix(fid, style, LMP_SIZE_ROWS, 0, 0) + return self.darray(value, nrows) + elif type == LMP_TYPE_ARRAY: + nrows = self.lmp.extract_fix(fid, style, LMP_SIZE_ROWS, 0, 0) + ncols = self.lmp.extract_fix(fid, style, LMP_SIZE_COLS, 0, 0) + return self.darray(value, nrows, ncols) + return value + + # ------------------------------------------------------------------------- + + def extract_variable(self, name, group=None, vartype=LMP_VAR_EQUAL): + """ Evaluate a LAMMPS variable and return its data + + This function is a wrapper around the function + :py:meth:`lammps.extract_variable() ` + method. It behaves the same as the original method, but returns NumPy arrays + instead of ``ctypes`` pointers. + + :param name: name of the variable to execute + :type name: string + :param group: name of group for atom-style variable (ignored for equal-style variables) + :type group: string + :param vartype: type of variable, see :ref:`py_vartype_constants` + :type vartype: int + :return: the requested data or None + :rtype: c_double, numpy.array, or NoneType + """ + import numpy as np + value = self.lmp.extract_variable(name, group, vartype) + if vartype == LMP_VAR_ATOM: + return np.ctypeslib.as_array(value) + return value + + # ------------------------------------------------------------------------- + + def get_neighlist(self, idx): + """Returns an instance of :class:`NumPyNeighList` which wraps access to the neighbor list with the given index + + :param idx: index of neighbor list + :type idx: int + :return: an instance of :class:`NumPyNeighList` wrapping access to neighbor list data + :rtype: NumPyNeighList + """ + if idx < 0: + return None + return NumPyNeighList(self.lmp, idx) + + # ------------------------------------------------------------------------- + + def get_neighlist_element_neighbors(self, idx, element): + """Return data of neighbor list entry + + This function is a wrapper around the function + :py:meth:`lammps.get_neighlist_element_neighbors() ` + method. It behaves the same as the original method, but returns a NumPy array containing the neighbors + instead of a ``ctypes`` pointer. + + :param element: neighbor list index + :type element: int + :param element: neighbor list element index + :type element: int + :return: tuple with atom local index and numpy array of neighbor local atom indices + :rtype: (int, numpy.array) + """ + iatom, numneigh, c_neighbors = self.lmp.get_neighlist_element_neighbors(idx, element) + neighbors = self.iarray(c_int, c_neighbors, numneigh, 1) + return iatom, neighbors + + # ------------------------------------------------------------------------- + + def iarray(self, c_int_type, raw_ptr, nelem, dim=1): + import numpy as np + np_int_type = self._ctype_to_numpy_int(c_int_type) + + if dim == 1: + ptr = cast(raw_ptr, POINTER(c_int_type * nelem)) + else: + ptr = cast(raw_ptr[0], POINTER(c_int_type * nelem * dim)) + + a = np.frombuffer(ptr.contents, dtype=np_int_type) + a.shape = (nelem, dim) + return a + + # ------------------------------------------------------------------------- + + def darray(self, raw_ptr, nelem, dim=1): + import numpy as np + if dim == 1: + ptr = cast(raw_ptr, POINTER(c_double * nelem)) + else: + ptr = cast(raw_ptr[0], POINTER(c_double * nelem * dim)) + + a = np.frombuffer(ptr.contents) + a.shape = (nelem, dim) + return a + +# ------------------------------------------------------------------------- + +class NumPyNeighList(NeighList): + """This is a wrapper class that exposes the contents of a neighbor list. + + It can be used like a regular Python list. Each element is a tuple of: + + * the atom local index + * a NumPy array containing the local atom indices of its neighbors + + Internally it uses the lower-level LAMMPS C-library interface. + + :param lmp: reference to instance of :py:class:`lammps` + :type lmp: lammps + :param idx: neighbor list index + :type idx: int + """ + def __init__(self, lmp, idx): + super(NumPyNeighList, self).__init__(lmp, idx) + + def get(self, element): + """ + :return: tuple with atom local index, numpy array of neighbor local atom indices + :rtype: (int, numpy.array) + """ + iatom, neighbors = self.lmp.numpy.get_neighlist_element_neighbors(self.idx, element) + return iatom, neighbors diff --git a/python/lammps/pylammps.py b/python/lammps/pylammps.py new file mode 100644 index 0000000000..1ec45d43b5 --- /dev/null +++ b/python/lammps/pylammps.py @@ -0,0 +1,861 @@ +# ---------------------------------------------------------------------- +# LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator +# http://lammps.sandia.gov, Sandia National Laboratories +# Steve Plimpton, sjplimp@sandia.gov +# +# Copyright (2003) Sandia Corporation. Under the terms of Contract +# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains +# certain rights in this software. This software is distributed under +# the GNU General Public License. +# +# See the README file in the top-level LAMMPS directory. +# ------------------------------------------------------------------------- + +################################################################################ +# Alternative Python Wrapper +# Written by Richard Berger +################################################################################ + +# for python2/3 compatibility + +from __future__ import print_function + +import os +import re +import select +import sys +from collections import namedtuple + +from .core import lammps + + +class OutputCapture(object): + """ Utility class to capture LAMMPS library output """ + + def __init__(self): + self.stdout_pipe_read, self.stdout_pipe_write = os.pipe() + self.stdout_fd = 1 + + def __enter__(self): + self.stdout = os.dup(self.stdout_fd) + os.dup2(self.stdout_pipe_write, self.stdout_fd) + return self + + def __exit__(self, type, value, tracebac): + os.dup2(self.stdout, self.stdout_fd) + os.close(self.stdout) + os.close(self.stdout_pipe_read) + os.close(self.stdout_pipe_write) + + # check if we have more to read from the pipe + def more_data(self, pipe): + r, _, _ = select.select([pipe], [], [], 0) + return bool(r) + + # read the whole pipe + def read_pipe(self, pipe): + out = "" + while self.more_data(pipe): + out += os.read(pipe, 1024).decode() + return out + + @property + def output(self): + return self.read_pipe(self.stdout_pipe_read) + +# ------------------------------------------------------------------------- + +class Variable(object): + def __init__(self, pylammps_instance, name, style, definition): + self._pylmp = pylammps_instance + self.name = name + self.style = style + self.definition = definition.split() + + @property + def value(self): + if self.style == 'atom': + return list(self._pylmp.lmp.extract_variable(self.name, "all", 1)) + else: + value = self._pylmp.lmp_print('"${%s}"' % self.name).strip() + try: + return float(value) + except ValueError: + return value + +# ------------------------------------------------------------------------- + +class AtomList(object): + """ + A dynamic list of atoms that returns either an :py:class:`Atom` or + :py:class:`Atom2D` instance for each atom. Instances are only allocated + when accessed. + + :ivar natoms: total number of atoms + :ivar dimensions: number of dimensions in system + """ + def __init__(self, pylammps_instance): + self._pylmp = pylammps_instance + self.natoms = self._pylmp.system.natoms + self.dimensions = self._pylmp.system.dimensions + self._loaded = {} + + def __getitem__(self, index): + """ + Return Atom with given local index + + :param index: Local index of atom + :type index: int + :rtype: Atom or Atom2D + """ + if index not in self._loaded: + if self.dimensions == 2: + atom = Atom2D(self._pylmp, index + 1) + else: + atom = Atom(self._pylmp, index + 1) + self._loaded[index] = atom + return self._loaded[index] + + def __len__(self): + return self.natoms + + +# ------------------------------------------------------------------------- + +class Atom(object): + """ + A wrapper class then represents a single atom inside of LAMMPS + + It provides access to properties of the atom and allows you to change some of them. + """ + def __init__(self, pylammps_instance, index): + self._pylmp = pylammps_instance + self.index = index + + @property + def id(self): + """ + Return the atom ID + + :type: int + """ + return int(self._pylmp.eval("id[%d]" % self.index)) + + @property + def type(self): + """ + Return the atom type + + :type: int + """ + return int(self._pylmp.eval("type[%d]" % self.index)) + + @property + def mol(self): + """ + Return the atom molecule index + + :type: int + """ + return self._pylmp.eval("mol[%d]" % self.index) + + @property + def mass(self): + """ + Return the atom mass + + :type: float + """ + return self._pylmp.eval("mass[%d]" % self.index) + + @property + def position(self): + """ + :getter: Return position of atom + :setter: Set position of atom + :type: tuple (float, float, float) + """ + return (self._pylmp.eval("x[%d]" % self.index), + self._pylmp.eval("y[%d]" % self.index), + self._pylmp.eval("z[%d]" % self.index)) + + @position.setter + def position(self, value): + """ + :getter: Return velocity of atom + :setter: Set velocity of atom + :type: tuple (float, float, float) + """ + self._pylmp.set("atom", self.index, "x", value[0]) + self._pylmp.set("atom", self.index, "y", value[1]) + self._pylmp.set("atom", self.index, "z", value[2]) + + @property + def velocity(self): + return (self._pylmp.eval("vx[%d]" % self.index), + self._pylmp.eval("vy[%d]" % self.index), + self._pylmp.eval("vz[%d]" % self.index)) + + @velocity.setter + def velocity(self, value): + self._pylmp.set("atom", self.index, "vx", value[0]) + self._pylmp.set("atom", self.index, "vy", value[1]) + self._pylmp.set("atom", self.index, "vz", value[2]) + + @property + def force(self): + """ + Return the total force acting on the atom + + :type: tuple (float, float, float) + """ + return (self._pylmp.eval("fx[%d]" % self.index), + self._pylmp.eval("fy[%d]" % self.index), + self._pylmp.eval("fz[%d]" % self.index)) + + @property + def charge(self): + """ + Return the atom charge + + :type: float + """ + return self._pylmp.eval("q[%d]" % self.index) + +# ------------------------------------------------------------------------- + +class Atom2D(Atom): + """ + A wrapper class then represents a single 2D atom inside of LAMMPS + + Inherits all properties from the :py:class:`Atom` class, but returns 2D versions + of position, velocity, and force. + + It provides access to properties of the atom and allows you to change some of them. + """ + def __init__(self, pylammps_instance, index): + super(Atom2D, self).__init__(pylammps_instance, index) + + @property + def position(self): + """ + :getter: Return position of atom + :setter: Set position of atom + :type: tuple (float, float) + """ + return (self._pylmp.eval("x[%d]" % self.index), + self._pylmp.eval("y[%d]" % self.index)) + + @position.setter + def position(self, value): + self._pylmp.set("atom", self.index, "x", value[0]) + self._pylmp.set("atom", self.index, "y", value[1]) + + @property + def velocity(self): + """ + :getter: Return velocity of atom + :setter: Set velocity of atom + :type: tuple (float, float) + """ + return (self._pylmp.eval("vx[%d]" % self.index), + self._pylmp.eval("vy[%d]" % self.index)) + + @velocity.setter + def velocity(self, value): + self._pylmp.set("atom", self.index, "vx", value[0]) + self._pylmp.set("atom", self.index, "vy", value[1]) + + @property + def force(self): + """ + Return the total force acting on the atom + + :type: tuple (float, float) + """ + return (self._pylmp.eval("fx[%d]" % self.index), + self._pylmp.eval("fy[%d]" % self.index)) + +# ------------------------------------------------------------------------- + +class variable_set: + def __init__(self, name, variable_dict): + self._name = name + array_pattern = re.compile(r"(?P.+)\[(?P[0-9]+)\]") + + for key, value in variable_dict.items(): + m = array_pattern.match(key) + if m: + g = m.groupdict() + varname = g['arr'] + idx = int(g['index']) + if varname not in self.__dict__: + self.__dict__[varname] = {} + self.__dict__[varname][idx] = value + else: + self.__dict__[key] = value + + def __str__(self): + return "{}({})".format(self._name, ','.join(["{}={}".format(k, self.__dict__[k]) for k in self.__dict__.keys() if not k.startswith('_')])) + + def __repr__(self): + return self.__str__() + +# ------------------------------------------------------------------------- + +def get_thermo_data(output): + """ traverse output of runs and extract thermo data columns """ + if isinstance(output, str): + lines = output.splitlines() + else: + lines = output + + runs = [] + columns = [] + in_run = False + current_run = {} + + for line in lines: + if line.startswith("Per MPI rank memory allocation"): + in_run = True + elif in_run and len(columns) == 0: + # first line after memory usage are column names + columns = line.split() + + current_run = {} + + for col in columns: + current_run[col] = [] + + elif line.startswith("Loop time of "): + in_run = False + columns = None + thermo_data = variable_set('ThermoData', current_run) + r = {'thermo' : thermo_data } + runs.append(namedtuple('Run', list(r.keys()))(*list(r.values()))) + elif in_run and len(columns) > 0: + items = line.split() + # Convert thermo output and store it. + # It must have the same number of columns and + # all of them must be convertible to floats. + # Otherwise we ignore the line + if len(items) == len(columns): + try: + values = [float(x) for x in items] + for i, col in enumerate(columns): + current_run[col].append(values[i]) + except ValueError: + pass + + return runs + +# ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- + +class PyLammps(object): + """ + This is a Python wrapper class around the lower-level + :py:class:`lammps` class, exposing a more Python-like, + object-oriented interface for prototyping system inside of IPython and + Jupyter notebooks. + + It either creates its own instance of :py:class:`lammps` or can be + initialized with an existing instance. The arguments are the same of the + lower-level interface. The original interface can still be accessed via + :py:attr:`PyLammps.lmp`. + + :param name: "machine" name of the shared LAMMPS library ("mpi" loads ``liblammps_mpi.so``, "" loads ``liblammps.so``) + :type name: string + :param cmdargs: list of command line arguments to be passed to the :cpp:func:`lammps_open` function. The executable name is automatically added. + :type cmdargs: list + :param ptr: pointer to a LAMMPS C++ class instance when called from an embedded Python interpreter. None means load symbols from shared library. + :type ptr: pointer + :param comm: MPI communicator (as provided by `mpi4py `_). ``None`` means use ``MPI_COMM_WORLD`` implicitly. + :type comm: MPI_Comm + + :ivar lmp: instance of original LAMMPS Python interface + :vartype lmp: :py:class:`lammps` + + :ivar runs: list of completed runs, each storing the thermo output + :vartype run: list + """ + + def __init__(self, name="", cmdargs=None, ptr=None, comm=None): + self.has_echo = False + + if cmdargs: + if '-echo' in cmdargs: + idx = cmdargs.index('-echo') + # ensures that echo line is ignored during output capture + self.has_echo = idx+1 < len(cmdargs) and cmdargs[idx+1] in ('screen', 'both') + + if ptr: + if isinstance(ptr,PyLammps): + self.lmp = ptr.lmp + elif isinstance(ptr,lammps): + self.lmp = ptr + else: + self.lmp = lammps(name=name,cmdargs=cmdargs,ptr=ptr,comm=comm) + else: + self.lmp = lammps(name=name,cmdargs=cmdargs,ptr=None,comm=comm) + print("LAMMPS output is captured by PyLammps wrapper") + self._cmd_history = [] + self.runs = [] + + def __del__(self): + if self.lmp: self.lmp.close() + self.lmp = None + + def close(self): + """Explicitly delete a LAMMPS instance + + This is a wrapper around the :py:meth:`lammps.close` of the Python interface. + """ + if self.lmp: self.lmp.close() + self.lmp = None + + def version(self): + """Return a numerical representation of the LAMMPS version in use. + + This is a wrapper around the :py:meth:`lammps.version` function of the Python interface. + + :return: version number + :rtype: int + """ + return self.lmp.version() + + def file(self, file): + """Read LAMMPS commands from a file. + + This is a wrapper around the :py:meth:`lammps.file` function of the Python interface. + + :param path: Name of the file/path with LAMMPS commands + :type path: string + """ + self.lmp.file(file) + + def write_script(self, filepath): + """ + Write LAMMPS script file containing all commands executed up until now + + :param filepath: path to script file that should be written + :type filepath: string + """ + with open(filepath, "w") as f: + for cmd in self._cmd_history: + print(cmd, file=f) + + def command(self, cmd): + """ + Execute LAMMPS command + + All commands executed will be stored in a command history which can be + written to a file using :py:meth:`PyLammps.write_script()` + + :param cmd: command string that should be executed + :type: cmd: string + """ + self.lmp.command(cmd) + self._cmd_history.append(cmd) + + def run(self, *args, **kwargs): + """ + Execute LAMMPS run command with given arguments + + All thermo output during the run is captured and saved as new entry in + :py:attr:`PyLammps.runs`. The latest run can be retrieved by + :py:attr:`PyLammps.last_run`. + """ + output = self.__getattr__('run')(*args, **kwargs) + + comm = self.lmp.get_mpi_comm() + if comm: + output = self.lmp.comm.bcast(output, root=0) + + self.runs += get_thermo_data(output) + return output + + @property + def last_run(self): + """ + Return data produced of last completed run command + + :getter: Returns an object containing information about the last run command + :type: dict + """ + if len(self.runs) > 0: + return self.runs[-1] + return None + + @property + def atoms(self): + """ + All atoms of this LAMMPS instance + + :getter: Returns a list of atoms currently in the system + :type: AtomList + """ + return AtomList(self) + + @property + def system(self): + """ + The system state of this LAMMPS instance + + :getter: Returns an object with properties storing the current system state + :type: namedtuple + """ + output = self.info("system") + d = self._parse_info_system(output) + return namedtuple('System', d.keys())(*d.values()) + + @property + def communication(self): + """ + The communication state of this LAMMPS instance + + :getter: Returns an object with properties storing the current communication state + :type: namedtuple + """ + output = self.info("communication") + d = self._parse_info_communication(output) + return namedtuple('Communication', d.keys())(*d.values()) + + @property + def computes(self): + """ + The list of active computes of this LAMMPS instance + + :getter: Returns a list of computes that are currently active in this LAMMPS instance + :type: list + """ + output = self.info("computes") + return self._parse_element_list(output) + + @property + def dumps(self): + """ + The list of active dumps of this LAMMPS instance + + :getter: Returns a list of dumps that are currently active in this LAMMPS instance + :type: list + """ + output = self.info("dumps") + return self._parse_element_list(output) + + @property + def fixes(self): + """ + The list of active fixes of this LAMMPS instance + + :getter: Returns a list of fixes that are currently active in this LAMMPS instance + :type: list + """ + output = self.info("fixes") + return self._parse_element_list(output) + + @property + def groups(self): + """ + The list of active atom groups of this LAMMPS instance + + :getter: Returns a list of atom groups that are currently active in this LAMMPS instance + :type: list + """ + output = self.info("groups") + return self._parse_groups(output) + + @property + def variables(self): + """ + Returns a dictionary of all variables defined in the current LAMMPS instance + + :getter: Returns a dictionary of all variables that are defined in this LAMMPS instance + :type: dict + """ + output = self.info("variables") + vars = {} + for v in self._parse_element_list(output): + vars[v['name']] = Variable(self, v['name'], v['style'], v['def']) + return vars + + def eval(self, expr): + """ + Evaluate expression + + :param expr: the expression string that should be evaluated inside of LAMMPS + :type expr: string + + :return: the value of the evaluated expression + :rtype: float if numeric, string otherwise + """ + value = self.lmp_print('"$(%s)"' % expr).strip() + try: + return float(value) + except ValueError: + return value + + def _split_values(self, line): + return [x.strip() for x in line.split(',')] + + def _get_pair(self, value): + return [x.strip() for x in value.split('=')] + + def _parse_info_system(self, output): + lines = output[6:-2] + system = {} + + for line in lines: + if line.startswith("Units"): + system['units'] = self._get_pair(line)[1] + elif line.startswith("Atom style"): + system['atom_style'] = self._get_pair(line)[1] + elif line.startswith("Atom map"): + system['atom_map'] = self._get_pair(line)[1] + elif line.startswith("Atoms"): + parts = self._split_values(line) + system['natoms'] = int(self._get_pair(parts[0])[1]) + system['ntypes'] = int(self._get_pair(parts[1])[1]) + system['style'] = self._get_pair(parts[2])[1] + elif line.startswith("Kspace style"): + system['kspace_style'] = self._get_pair(line)[1] + elif line.startswith("Dimensions"): + system['dimensions'] = int(self._get_pair(line)[1]) + elif line.startswith("Orthogonal box"): + system['orthogonal_box'] = [float(x) for x in self._get_pair(line)[1].split('x')] + elif line.startswith("Boundaries"): + system['boundaries'] = self._get_pair(line)[1] + elif line.startswith("xlo"): + keys, values = [self._split_values(x) for x in self._get_pair(line)] + for key, value in zip(keys, values): + system[key] = float(value) + elif line.startswith("ylo"): + keys, values = [self._split_values(x) for x in self._get_pair(line)] + for key, value in zip(keys, values): + system[key] = float(value) + elif line.startswith("zlo"): + keys, values = [self._split_values(x) for x in self._get_pair(line)] + for key, value in zip(keys, values): + system[key] = float(value) + elif line.startswith("Molecule type"): + system['molecule_type'] = self._get_pair(line)[1] + elif line.startswith("Bonds"): + parts = self._split_values(line) + system['nbonds'] = int(self._get_pair(parts[0])[1]) + system['nbondtypes'] = int(self._get_pair(parts[1])[1]) + system['bond_style'] = self._get_pair(parts[2])[1] + elif line.startswith("Angles"): + parts = self._split_values(line) + system['nangles'] = int(self._get_pair(parts[0])[1]) + system['nangletypes'] = int(self._get_pair(parts[1])[1]) + system['angle_style'] = self._get_pair(parts[2])[1] + elif line.startswith("Dihedrals"): + parts = self._split_values(line) + system['ndihedrals'] = int(self._get_pair(parts[0])[1]) + system['ndihedraltypes'] = int(self._get_pair(parts[1])[1]) + system['dihedral_style'] = self._get_pair(parts[2])[1] + elif line.startswith("Impropers"): + parts = self._split_values(line) + system['nimpropers'] = int(self._get_pair(parts[0])[1]) + system['nimpropertypes'] = int(self._get_pair(parts[1])[1]) + system['improper_style'] = self._get_pair(parts[2])[1] + + return system + + def _parse_info_communication(self, output): + lines = output[6:-3] + comm = {} + + for line in lines: + if line.startswith("MPI library"): + comm['mpi_version'] = line.split(':')[1].strip() + elif line.startswith("Comm style"): + parts = self._split_values(line) + comm['comm_style'] = self._get_pair(parts[0])[1] + comm['comm_layout'] = self._get_pair(parts[1])[1] + elif line.startswith("Processor grid"): + comm['proc_grid'] = [int(x) for x in self._get_pair(line)[1].split('x')] + elif line.startswith("Communicate velocities for ghost atoms"): + comm['ghost_velocity'] = (self._get_pair(line)[1] == "yes") + elif line.startswith("Nprocs"): + parts = self._split_values(line) + comm['nprocs'] = int(self._get_pair(parts[0])[1]) + comm['nthreads'] = int(self._get_pair(parts[1])[1]) + return comm + + def _parse_element_list(self, output): + lines = output[6:-3] + elements = [] + + for line in lines: + element_info = self._split_values(line.split(':')[1].strip()) + element = {'name': element_info[0]} + for key, value in [self._get_pair(x) for x in element_info[1:]]: + element[key] = value + elements.append(element) + return elements + + def _parse_groups(self, output): + lines = output[6:-3] + groups = [] + group_pattern = re.compile(r"(?P.+) \((?P.+)\)") + + for line in lines: + m = group_pattern.match(line.split(':')[1].strip()) + group = {'name': m.group('name'), 'type': m.group('type')} + groups.append(group) + return groups + + def lmp_print(self, s): + """ needed for Python2 compatibility, since print is a reserved keyword """ + return self.__getattr__("print")(s) + + def __dir__(self): + return ['angle_coeff', 'angle_style', 'atom_modify', 'atom_style', 'atom_style', + 'bond_coeff', 'bond_style', 'boundary', 'change_box', 'communicate', 'compute', + 'create_atoms', 'create_box', 'delete_atoms', 'delete_bonds', 'dielectric', + 'dihedral_coeff', 'dihedral_style', 'dimension', 'dump', 'fix', 'fix_modify', + 'group', 'improper_coeff', 'improper_style', 'include', 'kspace_modify', + 'kspace_style', 'lattice', 'mass', 'minimize', 'min_style', 'neighbor', + 'neigh_modify', 'newton', 'nthreads', 'pair_coeff', 'pair_modify', + 'pair_style', 'processors', 'read', 'read_data', 'read_restart', 'region', + 'replicate', 'reset_timestep', 'restart', 'run', 'run_style', 'thermo', + 'thermo_modify', 'thermo_style', 'timestep', 'undump', 'unfix', 'units', + 'variable', 'velocity', 'write_restart'] + + def __getattr__(self, name): + """ + This method is where the Python 'magic' happens. If a method is not + defined by the class PyLammps, it assumes it is a LAMMPS command. It takes + all the arguments, concatinates them to a single string, and executes it using + :py:meth:`lammps.PyLammps.command()`. + + :param verbose: Print output of command + :type verbose: bool + :return: line or list of lines of output, None if no output + :rtype: list or string + """ + def handler(*args, **kwargs): + cmd_args = [name] + [str(x) for x in args] + + with OutputCapture() as capture: + cmd = ' '.join(cmd_args) + self.command(cmd) + output = capture.output + + if 'verbose' in kwargs and kwargs['verbose']: + print(output) + + lines = output.splitlines() + + if self.has_echo: + lines = lines[1:] + + if len(lines) > 1: + return lines + elif len(lines) == 1: + return lines[0] + return None + + return handler + + +class IPyLammps(PyLammps): + """ + IPython wrapper for LAMMPS which adds embedded graphics capabilities to PyLammmps interface + + It either creates its own instance of :py:class:`lammps` or can be + initialized with an existing instance. The arguments are the same of the + lower-level interface. The original interface can still be accessed via + :py:attr:`PyLammps.lmp`. + + :param name: "machine" name of the shared LAMMPS library ("mpi" loads ``liblammps_mpi.so``, "" loads ``liblammps.so``) + :type name: string + :param cmdargs: list of command line arguments to be passed to the :cpp:func:`lammps_open` function. The executable name is automatically added. + :type cmdargs: list + :param ptr: pointer to a LAMMPS C++ class instance when called from an embedded Python interpreter. None means load symbols from shared library. + :type ptr: pointer + :param comm: MPI communicator (as provided by `mpi4py `_). ``None`` means use ``MPI_COMM_WORLD`` implicitly. + :type comm: MPI_Comm + """ + + def __init__(self,name="",cmdargs=None,ptr=None,comm=None): + super(IPyLammps, self).__init__(name=name,cmdargs=cmdargs,ptr=ptr,comm=comm) + + def image(self, filename="snapshot.png", group="all", color="type", diameter="type", + size=None, view=None, center=None, up=None, zoom=1.0, background_color="white"): + """ Generate image using write_dump command and display it + + See :doc:`dump image ` for more information. + + :param filename: Name of the image file that should be generated. The extension determines whether it is PNG or JPEG + :type filename: string + :param group: the group of atoms write_image should use + :type group: string + :param color: name of property used to determine color + :type color: string + :param diameter: name of property used to determine atom diameter + :type diameter: string + :param size: dimensions of image + :type size: tuple (width, height) + :param view: view parameters + :type view: tuple (theta, phi) + :param center: center parameters + :type center: tuple (flag, center_x, center_y, center_z) + :param up: vector pointing to up direction + :type up: tuple (up_x, up_y, up_z) + :param zoom: zoom factor + :type zoom: float + :param background_color: background color of scene + :type background_color: string + + :return: Image instance used to display image in notebook + :rtype: :py:class:`IPython.core.display.Image` + """ + cmd_args = [group, "image", filename, color, diameter] + + if size: + width = size[0] + height = size[1] + cmd_args += ["size", width, height] + + if view: + theta = view[0] + phi = view[1] + cmd_args += ["view", theta, phi] + + if center: + flag = center[0] + Cx = center[1] + Cy = center[2] + Cz = center[3] + cmd_args += ["center", flag, Cx, Cy, Cz] + + if up: + Ux = up[0] + Uy = up[1] + Uz = up[2] + cmd_args += ["up", Ux, Uy, Uz] + + if zoom: + cmd_args += ["zoom", zoom] + + cmd_args.append("modify backcolor " + background_color) + + self.write_dump(*cmd_args) + from IPython.core.display import Image + return Image(filename) + + def video(self, filename): + """ + Load video from file + + Can be used to visualize videos from :doc:`dump movie `. + + :param filename: Path to video file + :type filename: string + :return: HTML Video Tag used by notebook to embed a video + :rtype: :py:class:`IPython.display.HTML` + """ + from IPython.display import HTML + return HTML("") diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000000..9be04138d5 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,26 @@ +# this only installs the LAMMPS python package +# it assumes the LAMMPS shared library is already installed +from distutils.core import setup +import os + +LAMMPS_PYTHON_DIR = os.path.dirname(os.path.realpath(__file__)) +LAMMPS_DIR = os.path.dirname(LAMMPS_PYTHON_DIR) +LAMMPS_SOURCE_DIR = os.path.join(LAMMPS_DIR, 'src') + +def get_lammps_version(): + with open(os.path.join(LAMMPS_SOURCE_DIR, 'version.h'), 'r') as f: + line = f.readline() + start_pos = line.find('"')+1 + end_pos = line.find('"', start_pos) + return "".join(line[start_pos:end_pos].split()) + +setup( + name = "lammps", + version = get_lammps_version(), + author = "Steve Plimpton", + author_email = "sjplimp@sandia.gov", + url = "https://lammps.sandia.gov", + description = "LAMMPS Molecular Dynamics Python package", + license = "GPL", + packages=["lammps"] +) diff --git a/src/Makefile b/src/Makefile index 149dedd35b..7a5e1aa728 100644 --- a/src/Makefile +++ b/src/Makefile @@ -278,7 +278,7 @@ mpi-stubs: sinclude ../lib/python/Makefile.lammps install-python: @$(PYTHON) ../python/install.py -v ../src/version.h \ - -m ../python/lammps.py -l ../src/liblammps.so + -p ../python/lammps -l ../src/liblammps.so # Create a tarball of src dir and packages diff --git a/src/PYTHON/python_impl.cpp b/src/PYTHON/python_impl.cpp index 22bf8e77fb..1b3fabfa62 100644 --- a/src/PYTHON/python_impl.cpp +++ b/src/PYTHON/python_impl.cpp @@ -53,7 +53,15 @@ PythonImpl::PythonImpl(LAMMPS *lmp) : Pointers(lmp) external_interpreter = Py_IsInitialized(); Py_Initialize(); - PyEval_InitThreads(); + + // only needed for Python 2.x and Python 3 < 3.7 + // With Python 3.7 this function is now called by Py_Initialize() + // Deprecated since version 3.9, will be removed in version 3.11 +#if PY_MAJOR_VERSION < 3 || PY_MINOR_VERSION < 7 + if(!PyEval_ThreadsInitialized()) { + PyEval_InitThreads(); + } +#endif PyGILState_STATE gstate = PyGILState_Ensure(); diff --git a/src/library.h b/src/library.h index 7806903e49..14be4064ea 100644 --- a/src/library.h +++ b/src/library.h @@ -42,7 +42,7 @@ /** Data type constants for extracting data from atoms, computes and fixes * - * Must be kept in sync with the equivalent constants in lammps.py */ + * Must be kept in sync with the equivalent constants in lammps/constants.py */ enum _LMP_DATATYPE_CONST { LAMMPS_INT = 0, /*!< 32-bit integer (array) */ @@ -56,7 +56,7 @@ enum _LMP_DATATYPE_CONST { /** Style constants for extracting data from computes and fixes. * - * Must be kept in sync with the equivalent constants in lammps.py */ + * Must be kept in sync with the equivalent constants in lammps/constants.py */ enum _LMP_STYLE_CONST { LMP_STYLE_GLOBAL=0, /*!< return global data */ @@ -66,7 +66,7 @@ enum _LMP_STYLE_CONST { /** Type and size constants for extracting data from computes and fixes. * - * Must be kept in sync with the equivalent constants in lammps.py */ + * Must be kept in sync with the equivalent constants in lammps/constants.py */ enum _LMP_TYPE_CONST { LMP_TYPE_SCALAR=0, /*!< return scalar */ diff --git a/unittest/python/CMakeLists.txt b/unittest/python/CMakeLists.txt index 575b64252b..640edf2733 100644 --- a/unittest/python/CMakeLists.txt +++ b/unittest/python/CMakeLists.txt @@ -44,7 +44,7 @@ if (Python_EXECUTABLE) find_package_handle_standard_args(COVERAGE DEFAULT_MSG COVERAGE_BINARY) if(COVERAGE_FOUND) - set(PYTHON_TEST_RUNNER ${Python_EXECUTABLE} -u ${COVERAGE_BINARY} run --parallel-mode --include=${LAMMPS_PYTHON_DIR}/lammps.py --omit=${LAMMPS_PYTHON_DIR}/install.py) + set(PYTHON_TEST_RUNNER ${Python_EXECUTABLE} -u ${COVERAGE_BINARY} run --parallel-mode --include=${LAMMPS_PYTHON_DIR}/lammps/*.py --omit=${LAMMPS_PYTHON_DIR}/install.py) else() set(PYTHON_TEST_RUNNER ${Python_EXECUTABLE} -u) endif()