├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── _templates │ └── autosummary │ │ └── class.rst ├── analyzer.rst ├── chem.rst ├── conf.py ├── environment.yml ├── examples.rst ├── examples │ ├── alkane.smt │ ├── alkane.zff │ ├── export.py │ └── type.py ├── forcefield.rst ├── index.rst ├── installation.rst ├── make.bat ├── ommhelper.rst ├── scheduler.rst ├── simsys.rst ├── topology.rst ├── trajectory.rst └── wrapper.rst ├── meta.yaml ├── mstk ├── __init__.py ├── analyzer │ ├── __init__.py │ ├── canny.py │ ├── energy_kernels.py │ ├── ewald.py │ ├── fitting.py │ ├── neighborlist.py │ ├── series.py │ ├── structure.py │ └── vle.py ├── chem │ ├── __init__.py │ ├── constant.py │ ├── element.py │ ├── formula.py │ └── rdkit.py ├── cli │ ├── __init__.py │ ├── analyze_rdf.py │ ├── build.py │ ├── export.py │ ├── ffconv.py │ ├── logplot.py │ ├── topconv.py │ ├── trjconv.py │ └── wham_pp.py ├── data │ └── forcefield │ │ ├── SPICA_v1.0.zff │ │ ├── gaff.smt │ │ ├── gaff.zff │ │ ├── primitive.smt │ │ └── primitive.zff ├── errors.py ├── forcefield │ ├── __init__.py │ ├── errors.py │ ├── ffterm.py │ ├── forcefield.py │ ├── io │ │ ├── __init__.py │ │ ├── padua.py │ │ ├── ppf.py │ │ ├── zff.py │ │ └── zfp.py │ └── typer │ │ ├── __init__.py │ │ ├── gaff_typer.py │ │ ├── smarts_typer.py │ │ └── typer.py ├── misc │ ├── __init__.py │ ├── docmeta.py │ └── singleton.py ├── ommhelper │ ├── __init__.py │ ├── force.py │ ├── grofile.py │ ├── reporter │ │ ├── __init__.py │ │ ├── checkpointreporter.py │ │ ├── drudetemperaturereporter.py │ │ ├── groreporter.py │ │ ├── maxforcereporter.py │ │ ├── statedatareporter.py │ │ └── viscosityreporter.py │ ├── unit.py │ └── utils.py ├── scheduler │ ├── __init__.py │ ├── pbsjob.py │ ├── remote_slurm.py │ ├── scheduler.py │ └── slurm.py ├── simsys │ ├── __init__.py │ ├── gmxexporter.py │ ├── lmpexporter.py │ ├── namdexporter.py │ ├── ommexporter.py │ └── system.py ├── topology │ ├── __init__.py │ ├── atom.py │ ├── connectivity.py │ ├── geometry.py │ ├── io │ │ ├── __init__.py │ │ ├── gro.py │ │ ├── lammps.py │ │ ├── mae.py │ │ ├── pdb.py │ │ ├── psf.py │ │ ├── sdf.py │ │ ├── smi.py │ │ ├── xyz.py │ │ └── zmat.py │ ├── molecule.py │ ├── residue.py │ ├── topology.py │ ├── unitcell.py │ └── virtualsite.py ├── trajectory │ ├── __init__.py │ ├── frame.py │ ├── handler.py │ ├── io │ │ ├── __init__.py │ │ ├── combined_trj.py │ │ ├── dcd.py │ │ ├── gro.py │ │ ├── lammps.py │ │ ├── xtc.py │ │ └── xyz.py │ └── trajectory.py ├── utils │ └── __init__.py └── wrapper │ ├── __init__.py │ ├── gauss.py │ ├── gmx.py │ └── packmol.py ├── pyproject.toml └── tests ├── analyzer ├── __init__.py ├── test_energy_kernels.py └── test_ewald.py ├── chem ├── __init__.py └── test_formula.py ├── forcefield ├── __init__.py ├── convert-gaff.py ├── convert-spica.py ├── files │ ├── CLP-define.smt │ ├── CLP.ff │ ├── CLPol-alpha.ff │ ├── CLPol-ljscale.ff │ ├── SPCE.ppf │ ├── SWM4-NDP.zff │ ├── TEAM_IL.ppf │ ├── TEAM_IL.zff │ ├── TIP4P.zff │ └── baselines │ │ ├── out-CLP.ppf │ │ ├── out-CLPol.zff │ │ ├── out-SPCE.zff │ │ └── out-TEAM_IL.zff ├── test_gaff.py ├── test_padua.py ├── test_ppf.py ├── test_primitive_ff.py ├── test_typer.py └── test_zff.py ├── simsys ├── __init__.py ├── files │ ├── 10-SDS-20-W.lmp │ ├── 10-benzene.in │ ├── 10-benzene.lmp │ ├── 10-benzene.ppf │ ├── 100-TIP4P.pdb │ ├── 5-Im21-BF4-drude.lmp │ ├── baselines │ │ ├── conf-drude.gro │ │ ├── conf-vsite.gro │ │ ├── conf.gro │ │ ├── conf.pdb │ │ ├── ff.prm │ │ ├── grompp-drude.mdp │ │ ├── grompp-vsite.mdp │ │ ├── grompp.mdp │ │ ├── top.psf │ │ ├── topol-drude.top │ │ ├── topol-vsite.top │ │ └── topol.top │ ├── c_3ad.gro │ ├── c_3ad.ppf │ └── c_3ad.psf ├── test_export.py └── test_simsys.py ├── topology ├── __init__.py ├── files │ ├── 10-H2O-5-C3H6.lmp │ ├── 10-H2O-5-C3H6.psf │ ├── 100-SPCE.gro │ ├── 9-SWM4.psf │ ├── CH3NH2.pdb │ ├── CH3NH3+.sdf │ ├── Im11.zmat │ ├── MoS2-13x8-layer1.xyz │ ├── MoS2.ff │ ├── Structures.mae │ ├── Structures.sdf │ ├── TIP3P.zmat │ ├── baselines │ │ ├── _MO_0.xyz │ │ ├── _pack.inp │ │ ├── gro-out.gro │ │ ├── lmp-out.psf │ │ ├── swm4-out.psf │ │ ├── zmat-out.pdb │ │ ├── zmat-out.psf │ │ └── zmat-out.xyz │ ├── c_3oh.ppf │ ├── c_3oh.psf │ ├── test.pdb │ └── urea.xyz ├── test_bond.py ├── test_geometry.py ├── test_gro.py ├── test_lmp.py ├── test_mae.py ├── test_molecule.py ├── test_pdb.py ├── test_psf.py ├── test_residue.py ├── test_sdf.py ├── test_topology.py ├── test_unitcell.py ├── test_virtual_site.py ├── test_xyz.py └── test_zmat.py └── trajectory ├── __init__.py ├── files ├── 100-SPCE.dcd ├── 100-SPCE.gro ├── 100-SPCE.psf ├── 100-SPCE.xtc ├── 100-SPCE.xyz ├── 100HOH.lammpstrj └── baselines │ ├── gro-out.dcd │ ├── gro-out.xtc │ ├── xtc-out.gro │ └── xtc-out.xyz ├── test_combined.py ├── test_dcd.py ├── test_gro.py ├── test_lammpstrj.py ├── test_xtc.py └── test_xyz.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | id-token: write 17 | contents: read 18 | 19 | jobs: 20 | deploy: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up Python 27 | uses: actions/setup-python@v3 28 | with: 29 | python-version: '3.x' 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install build 34 | - name: Build package 35 | run: python -m build 36 | - name: Publish package 37 | uses: pypa/gh-action-pypi-publish@v1.8.11 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .idea 92 | .vscode 93 | 94 | docs/_generated -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF 17 | #formats: 18 | # - pdf 19 | 20 | # Optionally set the version of Python and requirements required to build your docs 21 | #python: 22 | # version: 3.7 23 | # install: 24 | # - requirements: docs/requirements.txt 25 | 26 | build: 27 | os: "ubuntu-20.04" 28 | tools: 29 | python: "mambaforge-4.10" 30 | 31 | conda: 32 | environment: docs/environment.yml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mstk 2 | 3 | _A toolkit to make molecular simulation less painful_ 4 | 5 | `mstk` is a Python toolkit designed to streamline the setup and management of molecular simulations, particularly geared 6 | towards non-biological applications in material science and chemical engineering. It simplifies atom typing, force field 7 | parameter assignment, and input file generation for major simulation engines. 8 | 9 | ## Features 10 | 11 | * Assign atom types and force field parameters based on local chemical environment 12 | * Generate input files for LAMMPS, GROMACS, NAMD and OpenMM 13 | * Support Drude polarizable model, coarse-grained model, virtual site, linear angle... 14 | * Read/write common topology (PSF, ZMAT, PDB, LAMMPS, ...) and trajectory (GRO, XTC, DCD, LAMMPS, ...) files 15 | * Access local and remote job schedulers like Slurm 16 | 17 | ## Installation 18 | 19 | ``` 20 | conda install -c conda-forge numpy pandas rdkit openmm chemfiles packmol 21 | pip install mstk 22 | ``` 23 | 24 | ## Quick Example 25 | 26 | Build a liquid mixture of 100 benzene and 1000 water molecules, ready for simulation. 27 | 28 | ```python 29 | from mstk.topology import Molecule, Topology 30 | from mstk.forcefield import ForceField, Typer 31 | from mstk.simsys import System 32 | from mstk.wrapper import Packmol 33 | 34 | # Create topology from SMILES 35 | benzene = Molecule.from_smiles('c1ccccc1') 36 | water = Molecule.from_smiles('O') 37 | top = Topology([benzene, water]) 38 | 39 | # Assign atom types as defined in `data/forcefield/primitive.smt` 40 | typer = Typer.open('primitive.smt') 41 | typer.type(top) 42 | 43 | # Build a bulk liquid simulation box with Packmol 44 | packmol = Packmol('packmol') 45 | top.cell.set_box([4.0, 4.0, 4.0]) 46 | top.scale_with_packmol([100, 1000], packmol=packmol) 47 | 48 | # Assign force field parameters as defined in `data/forcefield/primitive.zff` 49 | ff = ForceField.open('primitive.zff') 50 | ff.assign_charge(top) 51 | system = System(top, ff) 52 | 53 | # Generate input files for LAMMPS, GROMACS and NAMD 54 | system.export_lammps() 55 | system.export_gromacs() 56 | system.export_namd() 57 | 58 | # Generate OpenMM system and topology 59 | omm_sys = system.to_omm_system() 60 | omm_top = top.to_omm_topology() 61 | ``` 62 | 63 | __Important Note__: 64 | *The __primitive__ atom typing and force field used above are for demonstration purpose only and are __not__ well 65 | optimized for production use.* 66 | *For reliable simulation, please prepare you own `smt` and `zff` files or get them from a validated source.* 67 | 68 | ## Documentation 69 | 70 | https://mstk.readthedocs.io/en/latest/index.html 71 | 72 | ## TODO 73 | 74 | - [ ] Refactor algorithms across `topology` and `analyzer` modules 75 | - [ ] Separate `ommhelper` module into its own package 76 | - [ ] Remove `analyzer` module 77 | - [ ] Revise the implementation of Drude polarization and virtual sites 78 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | 7 | {% block methods %} 8 | {% if methods %} 9 | .. rubric:: Methods 10 | 11 | .. autosummary:: 12 | {% for item in methods %} 13 | ~{{ name }}.{{ item }} 14 | {%- endfor %} 15 | {% endif %} 16 | {% endblock %} 17 | 18 | {% block attributes %} 19 | {% if attributes %} 20 | .. rubric:: Attributes 21 | 22 | .. autosummary:: 23 | {% for item in attributes %} 24 | ~{{ name }}.{{ item }} 25 | {%- endfor %} 26 | {% endif %} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /docs/analyzer.rst: -------------------------------------------------------------------------------- 1 | Analyzer 2 | ======== 3 | 4 | Analyzer module provides a bunch of shortcuts for performing the most commonly used analysing method, 5 | like curve fitting, structure determination and time series analysis. 6 | 7 | .. note:: 8 | *Deprecated. This module is developed for personal projects. It's not a comprehensive analysis toolkit.* 9 | 10 | Structural analysis 11 | ------------------- 12 | 13 | .. currentmodule:: mstk.analyzer.structure 14 | 15 | .. autosummary:: 16 | :toctree: _generated/ 17 | 18 | calc_weighted_average 19 | calc_com 20 | calc_rg 21 | calc_hull_volume 22 | 23 | Structural analysis for vapor-liquid interface 24 | ---------------------------------------------- 25 | 26 | .. currentmodule:: mstk.analyzer.vle 27 | 28 | .. autosummary:: 29 | :toctree: _generated/ 30 | 31 | check_vle_density 32 | N_vaporize_condense 33 | 34 | Time series analysis 35 | -------------------- 36 | 37 | .. currentmodule:: mstk.analyzer.series 38 | 39 | .. autosummary:: 40 | :toctree: _generated/ 41 | 42 | block_average 43 | average_of_blocks 44 | is_converged 45 | efficiency_with_block_size 46 | 47 | Curve fitting 48 | ------------- 49 | 50 | .. currentmodule:: mstk.analyzer.fitting 51 | 52 | .. autosummary:: 53 | :toctree: _generated/ 54 | 55 | polyfit 56 | polyval 57 | polyval_derivative 58 | polyfit_2d 59 | polyval_derivative_2d 60 | curve_fit_rsq 61 | fit_vle_dminus 62 | fit_vle_dplus 63 | -------------------------------------------------------------------------------- /docs/chem.rst: -------------------------------------------------------------------------------- 1 | 2 | Chemistry 3 | ========= 4 | 5 | .. currentmodule:: mstk.chem 6 | 7 | .. autosummary:: 8 | :toctree: _generated/ 9 | 10 | ~element.Element 11 | ~formula.Formula 12 | ~rdkit 13 | ~constant 14 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'mstk' 21 | copyright = '2017-2023, Zheng Gong' 22 | author = 'Zheng Gong' 23 | 24 | # -- General configuration --------------------------------------------------- 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = ['sphinx.ext.autodoc', 30 | 'sphinx.ext.autosummary', 31 | 'sphinx.ext.napoleon' 32 | ] 33 | 34 | autodoc_default_options = { 35 | 'members': True, 36 | 'member-order': 'bysource', 37 | 'undoc-members': True, 38 | } 39 | autosummary_generate = True 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 48 | 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | 52 | # The theme to use for HTML and HTML Help pages. See the documentation for 53 | # a list of builtin themes. 54 | # 55 | html_theme = 'alabaster' 56 | 57 | # Add any paths that contain custom static files (such as style sheets) here, 58 | # relative to this directory. They are copied after the builtin static files, 59 | # so a file named "default.css" will overwrite the builtin "default.css". 60 | html_static_path = ['_static'] -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: mstk-docs 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.7 6 | - numpy 7 | - pandas 8 | - openmm=7.7.0 9 | - rdkit=2022.09.1 10 | - sphinx=5.3.0 # sphinx 6 drops support for python 3.7 11 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Assign atom types using customized typing rule 5 | ---------------------------------------------- 6 | 7 | Atom type is the most fundamental element in a force field. 8 | It determines which parameters should be used for describing the potential energy of each topological element (bond, angle, etc...) in a simulation system. 9 | The assignment of atom types is usually the most cumbersome task in a simulation workflow. 10 | For bio simulations, template matching is commonly used, because almost all bio-polymers are made of several kinds of repeating units. 11 | However, the template matching won't work well for other molecules because there is no common repeating units. 12 | 13 | `mstk` provide a typing engine :class:`~mstk.forcefield.typer.SmartsTyper` for assigning atom types. 14 | It is based on local chemical environment defined by SMARTS pattern and a hierarchical rule described in a type definition file. 15 | 16 | In order to write the type definition file correctly, knowledge about 17 | `SMARTS pattern `_ is required. 18 | The idea of the hierarchical rule is described in 19 | `this article `_. 20 | 21 | Herein, an example of using :class:`~mstk.forcefield.typer.SmartsTyper` to assign atom types is provided. 22 | A type definition file should be fed to this typing engine. 23 | The text below shows the type definition for linear alkane in `TEAM` force field. 24 | Let's copy its content and save it to file `alkane.smt`. 25 | 26 | .. literalinclude:: examples/alkane.smt 27 | 28 | The script below will assign atom types for butane using this type definition. 29 | 30 | .. literalinclude:: examples/type.py 31 | 32 | A predefined typer :class:`~mstk.forcefield.typer.typer_primitive` is shipped with `mstk` for demonstration purpose. 33 | 34 | Generate input files for simulation engines 35 | ------------------------------------------- 36 | 37 | Note: this example requires that Packmol is installed. 38 | 39 | In order to generate input files for simulation engines, a force filed file is required. 40 | Currently, four formats are supported for storing force field: 41 | 42 | 1. `.zff` - format used by `mstk`. Refer to :class:`~mstk.forcefield.Zff` for details. 43 | 2. `.zfp` - deprecated XML format used by `mstk`. Refer to :class:`~mstk.forcefield.Zfp` for details. 44 | 3. `.ppf` - format used by DFF program. Refer to :class:`~mstk.forcefield.Ppf` for details. 45 | 4. `.ff` - format used by Agilio Padua's `fftool`. Refer to :class:`~mstk.forcefield.Padua` for details. 46 | 47 | Herein, an example force field file for linear alkanes is given in `ZFF` format. 48 | The parameters are taken from `TEAM_MGI` force field. 49 | Let's copy its content and save it to file `alkane.zff`. 50 | 51 | .. literalinclude:: examples/alkane.zff 52 | 53 | The workflow is as follows: 54 | 55 | .. literalinclude:: examples/export.py 56 | 57 | -------------------------------------------------------------------------------- /docs/examples/alkane.smt: -------------------------------------------------------------------------------- 1 | TypeDefinition 2 | 3 | h_1 [H][CX4] 4 | c_4 [CX4] 5 | c_4h2 [CX4;H2] 6 | c_4h3 [CX4;H3] 7 | 8 | HierarchicalTree 9 | 10 | h_1 11 | c_4 12 | c_4h2 13 | c_4h3 14 | -------------------------------------------------------------------------------- /docs/examples/alkane.zff: -------------------------------------------------------------------------------- 1 | Setting vdw_cutoff 1.0 2 | Setting vdw_long_range correct 3 | Setting lj_mixing_rule lorentz-berthelot 4 | Setting scale_14_vdw 0.5 5 | Setting scale_14_coulomb 0.8333333333333334 6 | 7 | #AtomType name mass charge eqt_bci eqt_vdw eqt_bond eqt_ang_c eqt_ang_s eqt_dih_c eqt_dih_s eqt_imp_c eqt_imp_s eqt_polar 8 | AtomType c_4h2 12.0110 0.0000 c_4 c_4h2 c_4 c_4 c_4 c_4 c_4 c_4 c_4 c_4h2 9 | AtomType c_4h3 12.0110 0.0000 c_4 c_4h3 c_4 c_4 c_4 c_4 c_4 c_4 c_4 c_4h3 10 | AtomType h_1 1.0079 0.0000 h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1 11 | #ChargeIncrement type1 type2 value 12 | ChargeIncrement c_4 h_1 -0.0600 13 | #LJ126 type1 type2 epsilon sigma 14 | LJ126 c_4h2 c_4h2 0.3138 0.3492 15 | LJ126 c_4h3 c_4h3 0.3284 0.3645 16 | LJ126 h_1 h_1 0.0891 0.2434 17 | #HarmonicBond type1 type2 length k fixed 18 | HarmonicBond c_4 c_4 0.1533 89309.6 False 19 | HarmonicBond c_4 h_1 0.1097 150741.0 True 20 | #HarmonicAngle type1 type2 type3 theta k fixed 21 | HarmonicAngle c_4 c_4 c_4 114.1800 260.5306 False 22 | HarmonicAngle c_4 c_4 h_1 110.1300 190.5402 False 23 | HarmonicAngle h_1 c_4 h_1 106.6800 149.1466 False 24 | #OplsDihedral type1 type2 type3 type4 k1 k2 k3 k4 25 | OplsDihedral c_4 c_4 c_4 c_4 -1.6891 -0.0075 0.2657 0.0000 26 | OplsDihedral c_4 c_4 c_4 h_1 -1.0544 0.7448 0.7870 0.0000 27 | OplsDihedral h_1 c_4 c_4 h_1 0.6485 1.0678 0.6226 0.0000 28 | -------------------------------------------------------------------------------- /docs/examples/export.py: -------------------------------------------------------------------------------- 1 | from mstk.topology import Molecule, Topology 2 | from mstk.forcefield import ForceField, SmartsTyper 3 | from mstk.simsys import System 4 | from mstk.wrapper import Packmol 5 | 6 | typer = SmartsTyper('alkane.smt') 7 | 8 | butane = Molecule.from_smiles('CCCC butane') 9 | typer.type(butane) 10 | 11 | # Load force field parameters from ZFF file 12 | ff = ForceField.open('alkane.zff') 13 | 14 | # Initialize a topology with periodic boundary condition 15 | # For now, it contains only one butane molecule 16 | top = Topology([butane]) 17 | top.cell.set_box([3, 3, 3]) 18 | 19 | # Assign atomic charges based on the charge parameters in the force field 20 | ff.assign_charge(top) 21 | 22 | # Call Packmol to build a configuration containing 100 butane molecules 23 | packmol = Packmol(r'/path/of/packmol') 24 | top.scale_with_packmol([100], packmol=packmol) 25 | 26 | # Associate force field parameters to the topology 27 | # And then export input files for simulation engines 28 | system = System(top, ff) 29 | system.export_gromacs() 30 | system.export_lammps() 31 | system.export_namd() 32 | -------------------------------------------------------------------------------- /docs/examples/type.py: -------------------------------------------------------------------------------- 1 | from mstk.forcefield.typer import SmartsTyper 2 | from mstk.topology import Molecule 3 | 4 | # Construct a typing engine from type definition file 5 | typer = SmartsTyper('alkane.smt') 6 | 7 | # Create a molecule from SMILES string 8 | butane = Molecule.from_smiles('CCCC') 9 | 10 | # Assign atom types based on SMARTS pattern 11 | typer.type(butane) 12 | for atom in butane.atoms: 13 | print(atom.name, atom.type) 14 | -------------------------------------------------------------------------------- /docs/forcefield.rst: -------------------------------------------------------------------------------- 1 | 2 | Force field 3 | =========== 4 | 5 | Typing engine 6 | ------------- 7 | 8 | .. currentmodule:: mstk.forcefield.typer 9 | 10 | .. autosummary:: 11 | :toctree: _generated/ 12 | 13 | Typer 14 | SmartsTyper 15 | GaffTyper 16 | 17 | Force field definition 18 | ---------------------- 19 | 20 | .. currentmodule:: mstk.forcefield 21 | 22 | .. autosummary:: 23 | :toctree: _generated/ 24 | 25 | ForceField 26 | FFTermFactory 27 | FFTerm 28 | AtomType 29 | 30 | vdW terms 31 | ~~~~~~~~~ 32 | 33 | .. autosummary:: 34 | :toctree: _generated/ 35 | 36 | VdwTerm 37 | LJ126Term 38 | MieTerm 39 | 40 | Charge increment terms 41 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | 43 | .. autosummary:: 44 | :toctree: _generated/ 45 | 46 | ChargeIncrementTerm 47 | 48 | Polarization terms 49 | ~~~~~~~~~~~~~~~~~~ 50 | 51 | .. autosummary:: 52 | :toctree: _generated/ 53 | 54 | PolarTerm 55 | DrudePolarTerm 56 | 57 | Virtual site terms 58 | ~~~~~~~~~~~~~~~~~~ 59 | 60 | .. autosummary:: 61 | :toctree: _generated/ 62 | 63 | VirtualSiteTerm 64 | TIP4PSiteTerm 65 | 66 | Bond terms 67 | ~~~~~~~~~~ 68 | 69 | .. autosummary:: 70 | :toctree: _generated/ 71 | 72 | BondTerm 73 | HarmonicBondTerm 74 | MorseBondTerm 75 | 76 | Angle terms 77 | ~~~~~~~~~~~ 78 | 79 | .. autosummary:: 80 | :toctree: _generated/ 81 | 82 | AngleTerm 83 | HarmonicAngleTerm 84 | SDKAngleTerm 85 | LinearAngleTerm 86 | 87 | Dihedral terms 88 | ~~~~~~~~~~~~~~ 89 | 90 | .. autosummary:: 91 | :toctree: _generated/ 92 | 93 | DihedralTerm 94 | OplsDihedralTerm 95 | PeriodicDihedralTerm 96 | 97 | Improper terms 98 | ~~~~~~~~~~~~~~ 99 | 100 | .. autosummary:: 101 | :toctree: _generated/ 102 | 103 | ImproperTerm 104 | OplsImproperTerm 105 | HarmonicImproperTerm 106 | 107 | Force field parser 108 | ------------------ 109 | 110 | .. currentmodule:: mstk.forcefield 111 | 112 | .. autosummary:: 113 | :toctree: _generated/ 114 | 115 | Zff 116 | Ppf 117 | Padua 118 | PaduaLJScaler 119 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. mstk documentation master file, created by 2 | sphinx-quickstart on Mon Jun 29 12:36:37 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | mstk 7 | ======= 8 | 9 | `mstk` is a toolkit for preparing, running and analyzing molecular simulations. 10 | It simplifies the workflow of setting up a simulation by 11 | manageable atom type and force field assignment, topology manipulation, trajectory reading/writing, etc... 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | installation 17 | examples 18 | 19 | Modules 20 | ------- 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | topology 26 | forcefield 27 | simsys 28 | trajectory 29 | ommhelper 30 | chem 31 | scheduler 32 | analyzer 33 | wrapper 34 | 35 | License 36 | ------- 37 | 38 | mstk is licensed under LGPL v2.1 -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Install dependencies 5 | -------------------- 6 | 7 | `mstk` requires `numpy`, `pandas` and `rdkit` for most of its functionalities. 8 | `openmm` is required for energy calculation, minimization etc... 9 | `chemfiles` is required for parsing `XTC` and `DCD` trajectory formats. 10 | `packmol` is required for building simulation box. 11 | `scipy`, `scikit-learn`, `matplotlib-base` and `pymbar-core` are required by `analyzer` module. 12 | 13 | It is recommended to install these dependencies with `conda`. 14 | 15 | .. code-block:: bash 16 | 17 | conda install -c conda-forge numpy pandas rdkit openmm chemfiles packmol scipy scikit-learn matplotlib-base pymbar-core 18 | 19 | 20 | Install `mstk` 21 | -------------- 22 | 23 | `mstk` itself can be installed either with `pip` or `conda`. 24 | 25 | Install `mstk` with pip 26 | ~~~~~~~~~~~~~~~~~~~~~~~ 27 | 28 | .. code-block:: bash 29 | 30 | pip install mstk 31 | 32 | Install `mstk` with conda 33 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 34 | 35 | .. code-block:: bash 36 | 37 | conda install -c conda-forge -c z-gong mstk 38 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/ommhelper.rst: -------------------------------------------------------------------------------- 1 | OpenMM helper 2 | ============= 3 | 4 | This module provides various tools to simplify the usage or extend the functionality of OpenMM. 5 | Mainly, they can be categorized into file parsers, pre-constructed forces and reporters. 6 | 7 | Forces 8 | ------ 9 | 10 | .. currentmodule:: mstk.ommhelper.force 11 | 12 | .. autosummary:: 13 | :toctree: _generated 14 | 15 | slab_correction 16 | restrain_particle_number 17 | wall_lj126 18 | wall_power 19 | point_wall_power 20 | electric_field 21 | spring_self 22 | CLPolCoulTT 23 | 24 | Reporters 25 | --------- 26 | 27 | .. currentmodule:: mstk.ommhelper.reporter 28 | 29 | .. autosummary:: 30 | :toctree: _generated/ 31 | 32 | ViscosityReporter 33 | DrudeTemperatureReporter 34 | GroReporter 35 | CheckpointReporter 36 | StateDataReporter 37 | 38 | Parsers 39 | ------- 40 | 41 | .. currentmodule:: mstk.ommhelper 42 | 43 | .. autosummary:: 44 | :toctree: _generated/ 45 | 46 | GroFile 47 | -------------------------------------------------------------------------------- /docs/scheduler.rst: -------------------------------------------------------------------------------- 1 | 2 | Job scheduler 3 | ============= 4 | 5 | Molecular simulation is computationally intensive. 6 | Therefore, it is usually impractical to run simulations on local computer. 7 | Instead, simulation jobs are usually submitted to a job scheduler on a distributed computing infrastructure. 8 | Then the jobs will be spread and executed on the compute nodes. 9 | 10 | Wrappers for several job schedulers are provided in `mstk`. 11 | They are designed to simplify the most common operations of job schedulers, 12 | like querying, submitting and deleting jobs, generate job scripts, etc... 13 | It also provide a cache to the scheduler so that when high-throughput simulations are performed, 14 | the job scheduler will not experience much pressure. 15 | 16 | .. currentmodule:: mstk.scheduler 17 | 18 | .. autosummary:: 19 | :toctree: _generated/ 20 | 21 | PbsJob 22 | JobParameter 23 | Scheduler 24 | Slurm 25 | RemoteSlurm 26 | -------------------------------------------------------------------------------- /docs/simsys.rst: -------------------------------------------------------------------------------- 1 | 2 | Simulation system 3 | ================= 4 | 5 | A simulation system is a combination of topology and force field. 6 | It defines a system that is ready for simulation. 7 | 8 | After a system is constructed, it can be exported to different simulation engines for calculation. 9 | Current, LAMMPS, GROMACS, NAMD and OpenMM are supported. 10 | Depending on the functional form used in the force field, some simulation engines may not be supported for specific system. 11 | 12 | .. currentmodule:: mstk.simsys 13 | 14 | .. autosummary:: 15 | :toctree: _generated/ 16 | 17 | System 18 | 19 | Exporter 20 | -------- 21 | 22 | .. currentmodule:: mstk.simsys 23 | 24 | .. autosummary:: 25 | :toctree: _generated/ 26 | 27 | GromacsExporter 28 | LammpsExporter 29 | NamdExporter 30 | OpenMMExporter 31 | -------------------------------------------------------------------------------- /docs/topology.rst: -------------------------------------------------------------------------------- 1 | Topology 2 | ======== 3 | 4 | Topology definition 5 | ------------------- 6 | 7 | .. currentmodule:: mstk.topology 8 | 9 | .. autosummary:: 10 | :toctree: _generated/ 11 | 12 | Topology 13 | Atom 14 | Molecule 15 | Residue 16 | UnitCell 17 | 18 | Connectivity 19 | ~~~~~~~~~~~~ 20 | 21 | .. currentmodule:: mstk.topology 22 | 23 | .. autosummary:: 24 | :toctree: _generated/ 25 | 26 | Bond 27 | Angle 28 | Dihedral 29 | Improper 30 | 31 | Virtual site 32 | ------------ 33 | 34 | .. currentmodule:: mstk.topology 35 | 36 | .. autosummary:: 37 | :toctree: _generated/ 38 | 39 | VirtualSite 40 | TIP4PSite 41 | 42 | Topology parser 43 | --------------- 44 | 45 | .. currentmodule:: mstk.topology 46 | 47 | .. autosummary:: 48 | :toctree: _generated/ 49 | 50 | Psf 51 | LammpsData 52 | Pdb 53 | Zmat 54 | Smi 55 | GroTopology 56 | XyzTopology 57 | -------------------------------------------------------------------------------- /docs/trajectory.rst: -------------------------------------------------------------------------------- 1 | 2 | Trajectory 3 | ========== 4 | 5 | .. currentmodule:: mstk.trajectory 6 | 7 | .. autosummary:: 8 | :toctree: _generated/ 9 | 10 | Trajectory 11 | Frame 12 | 13 | Trajectory handler 14 | ------------------ 15 | 16 | Handlers for reading/writing different trajectory formats by :class:`~mstk.trajectory.Trajectory`. 17 | They are not meant to be called directly. 18 | However, you can pass a handler class when initializing a :class:`~mstk.trajectory.Trajectory` object. 19 | 20 | .. currentmodule:: mstk.trajectory 21 | 22 | .. autosummary:: 23 | :toctree: _generated/ 24 | 25 | TrjHandler 26 | Dcd 27 | Gro 28 | LammpsTrj 29 | Xtc 30 | Xyz 31 | CombinedTrj 32 | -------------------------------------------------------------------------------- /docs/wrapper.rst: -------------------------------------------------------------------------------- 1 | 2 | Wrappers 3 | ======== 4 | 5 | Wrappers for GROAMCS, Gaussian, and Packmol to simplify the usage of these packages. 6 | 7 | .. note:: 8 | *This module does not provide a comprehensive API for these packages.* 9 | 10 | .. currentmodule:: mstk.wrapper 11 | 12 | .. autosummary:: 13 | :toctree: _generated/ 14 | 15 | GMX 16 | Gauss 17 | Packmol 18 | -------------------------------------------------------------------------------- /meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "mstk" %} 2 | {% set version = "0.3.27" %} 3 | {% set build = 0 %} 4 | 5 | package: 6 | name: {{ name }} 7 | version: {{ version }} 8 | 9 | source: 10 | git_url: https://github.com/z-gong/{{ name }}.git 11 | git_rev: {{ version }} 12 | 13 | build: 14 | number: {{ build }} 15 | script: python -m pip install . --no-deps # This is recommended for pip-based builds 16 | noarch: python # If your package works for all architectures 17 | 18 | requirements: 19 | build: 20 | - python >=3.6 21 | - pip # Since you are using a `setup.py`, pip is needed to install 22 | - setuptools 23 | 24 | run: 25 | - python >=3.6 26 | - numpy 27 | - pandas 28 | - rdkit 29 | - openmm >=8.1.2 30 | - chemfiles >=0.10.4 31 | - packmol 32 | - scipy 33 | - scikit-learn 34 | - matplotlib-base 35 | - pymbar-core 36 | 37 | about: 38 | home: https://github.com/z-gong/mstk 39 | license: LGPL-2.0-or-later 40 | license_file: LICENSE 41 | summary: Molecular simulation toolkit 42 | description: | 43 | A Python-based molecular simulation toolkit designed to streamline the 44 | development and execution of molecular dynamics simulations. 45 | -------------------------------------------------------------------------------- /mstk/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from pathlib import Path 4 | 5 | DIR_MSTK = Path(__file__).parent.as_posix() 6 | 7 | MSTK_FORCEFIELD_PATH = [os.path.join(DIR_MSTK, 'data', 'forcefield')] 8 | _ff_path_str = os.getenv('MSTK_FORCEFIELD_PATH') 9 | if _ff_path_str: 10 | MSTK_FORCEFIELD_PATH = _ff_path_str.strip(':').split(':') + MSTK_FORCEFIELD_PATH 11 | 12 | 13 | class _CustomFormatter(logging.Formatter): 14 | grey = "\x1b[38;20m" 15 | yellow = "\x1b[33;20m" 16 | red = "\x1b[31;20m" 17 | bold_red = "\x1b[31;1m" 18 | reset = "\x1b[0m" 19 | format = "%(asctime)s %(levelname)s (%(filename)s:%(lineno)d) %(message)s" 20 | 21 | FORMATS = { 22 | logging.DEBUG : grey + format + reset, 23 | logging.INFO : grey + format + reset, 24 | logging.WARNING : yellow + format + reset, 25 | logging.ERROR : red + format + reset, 26 | logging.CRITICAL: bold_red + format + reset 27 | } 28 | 29 | def format(self, record): 30 | log_fmt = self.FORMATS.get(record.levelno) 31 | formatter = logging.Formatter(log_fmt, "%m-%d %H:%M:%S") 32 | return formatter.format(record) 33 | 34 | 35 | def _get_logger(): 36 | logger = logging.getLogger('mstk') 37 | logger.setLevel(logging.INFO) 38 | handler = logging.StreamHandler() 39 | handler.setFormatter(_CustomFormatter()) 40 | logger.addHandler(handler) 41 | return logger 42 | 43 | 44 | logger = _get_logger() 45 | -------------------------------------------------------------------------------- /mstk/analyzer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/mstk/analyzer/__init__.py -------------------------------------------------------------------------------- /mstk/analyzer/neighborlist.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import numpy as np 3 | 4 | 5 | class NeighborList: 6 | ''' 7 | Parameters 8 | ---------- 9 | box : np.ndarray of shape (3,) 10 | cutoff : float 11 | 12 | Attributes 13 | ---------- 14 | box : np.ndarray of shape (3,) 15 | n_cell : np.ndarray of shape (3,) 16 | cell_size : np.ndarray of shape (3,) 17 | cell_indexes : list of tuple of int 18 | ''' 19 | 20 | def __init__(self, box, cutoff): 21 | if any(box / 3 < cutoff): 22 | raise Exception('Cutoff larger than box/3') 23 | n = np.zeros(3, dtype=int) 24 | for i in range(3): 25 | while True: 26 | if box[i] / (n[i] + 1) < cutoff: 27 | break 28 | n[i] += 1 29 | 30 | self.box = box 31 | self.n_cell = n 32 | self.cell_indexes = list(itertools.product(range(n[0]), range(n[1]), range(n[2]))) 33 | self.cell_size = box / n 34 | self._cells = [[[[] for _ in range(n[2])] for _ in range(n[1])] for _ in range(n[0])] 35 | 36 | def build(self, positions): 37 | ''' 38 | Parameters 39 | ---------- 40 | positions : np.ndarray 41 | ''' 42 | positions -= np.floor(positions / self.box) * self.box 43 | locations = np.clip(np.floor(positions / self.cell_size).astype(int), 0, self.n_cell - 1) 44 | for i, (x, y, z) in enumerate(locations): 45 | self._cells[x][y][z].append(i) 46 | 47 | def get_cell(self, index): 48 | ''' 49 | Parameters 50 | ---------- 51 | index : tuple of int 52 | 53 | Returns 54 | ------- 55 | index_atoms : list of int 56 | ''' 57 | return self._cells[index[0]][index[1]][index[2]] 58 | 59 | def get_interacting_cell(self, index): 60 | ''' 61 | Return the 27 interacting cells as a single large cell 62 | 63 | Parameters 64 | ---------- 65 | index : tuple of int 66 | 67 | Returns 68 | ------- 69 | index_atoms : list of int 70 | ''' 71 | idx_surround = [[], [], []] 72 | for i in range(3): 73 | idx = index[i] 74 | idx_surround[i] = [idx - 1 if idx - 1 >= 0 else self.n_cell[i] - 1, 75 | idx, 76 | idx + 1 if idx + 1 <= self.n_cell[i] - 1 else 0 77 | ] 78 | cell = [] 79 | for index in itertools.product(*idx_surround): 80 | cell.extend(self.get_cell(index)) 81 | 82 | return cell 83 | -------------------------------------------------------------------------------- /mstk/analyzer/structure.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | from mstk.topology import Atom 4 | 5 | __all__ = [ 6 | 'calc_weighted_average', 7 | 'calc_com', 8 | 'calc_rg', 9 | 'calc_hull_volume', 10 | ] 11 | 12 | 13 | def calc_weighted_average(array, weight): 14 | ''' 15 | Calculate the weighted average of 2-dimensional data 16 | 17 | Parameters 18 | ---------- 19 | array : np.array 20 | weight : np.array 21 | If weight for all elements equal to zero. Will ignore weight 22 | 23 | Returns 24 | ------- 25 | average : np.array 26 | ''' 27 | if all(weight == 0): 28 | weight = np.ones(weight.shape) 29 | return np.sum(array * weight[:, np.newaxis], axis=0) / np.sum(weight) 30 | 31 | 32 | def calc_com(atoms): 33 | ''' 34 | Calculate the center of mass of a group of atoms 35 | 36 | Parameters 37 | ---------- 38 | atoms : list of Atom 39 | 40 | Returns 41 | ------- 42 | com : np.array 43 | ''' 44 | positions = np.array([atom.position for atom in atoms]) 45 | masses = np.array([atom.mass for atom in atoms]) 46 | 47 | return calc_weighted_average(positions, masses) 48 | 49 | 50 | def calc_rg(atoms): 51 | ''' 52 | Calculate the radius of gyration of a group of atoms 53 | 54 | Parameters 55 | ---------- 56 | atoms : list of Atom 57 | 58 | Returns 59 | ------- 60 | rg : float 61 | ''' 62 | positions = np.array([atom.position for atom in atoms]) 63 | masses = np.array([atom.mass for atom in atoms]) 64 | if all(masses == 0): 65 | masses.fill(1.0) 66 | 67 | com = calc_weighted_average(positions, masses) 68 | positions -= com 69 | rsq_ew = np.sum(positions ** 2, axis=1) 70 | 71 | return math.sqrt(sum(masses * rsq_ew) / sum(masses)) 72 | 73 | 74 | def calc_hull_volume(atoms): 75 | ''' 76 | Calculate the volume occupied by a groups of at least four atoms 77 | 78 | Parameters 79 | ---------- 80 | atoms : list of Atom 81 | 82 | Returns 83 | ------- 84 | volume : float 85 | ''' 86 | if len(atoms) < 4: 87 | raise Exception('At least 4 atoms required for 3D convex hull') 88 | 89 | from scipy.spatial import ConvexHull 90 | 91 | positions = np.array([atom.position for atom in atoms]) 92 | hull = ConvexHull(positions) 93 | 94 | return float(hull.volume) 95 | -------------------------------------------------------------------------------- /mstk/chem/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/mstk/chem/__init__.py -------------------------------------------------------------------------------- /mstk/chem/constant.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Pre-defined physical constants. 3 | ''' 4 | 5 | PI = 3.1415926535 6 | PI_SQRT = PI ** 0.5 7 | VACUUM_PERMITTIVITY = 8.854_187_812_8E-12 # farad/meter 8 | ELEMENTARY_CHARGE = 1.602_176_62E-19 # coulomb 9 | AVOGADRO = 6.022_140_76E23 10 | CENTI = 1E-2 11 | MILLI = 1E-3 12 | MICRO = 1E-6 13 | NANO = 1E-9 14 | PICO = 1E-12 15 | # q^2/nm -> 138.93545522028575 kJ/mol 16 | ONE_4PI_EPS0 = 1 / (4 * PI * VACUUM_PERMITTIVITY) \ 17 | * ELEMENTARY_CHARGE ** 2 / NANO / 1000 * AVOGADRO 18 | RAD2DEG = 180.0 / PI 19 | DEG2RAD = PI / 180.0 20 | -------------------------------------------------------------------------------- /mstk/chem/element.py: -------------------------------------------------------------------------------- 1 | _atomic_number = {'H': 1, 'He': 2, 'Li': 3, 'Be': 4, 'B': 5, 2 | 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'Ne': 10, 3 | 'Na': 11, 'Mg': 12, 'Al': 13, 'Si': 14, 'P': 15, 4 | 'S': 16, 'Cl': 17, 'Ar': 18, 'K': 19, 'Ca': 20, 5 | 'Ti': 22, 'Fe': 26, 'Zn': 30, 'Se': 34, 'Br': 35, 6 | 'Kr': 36, 'Mo': 42, 'Ru': 44, 'Sn': 50, 'Te': 52, 7 | 'I': 53, 'Xe': 54, 'UNK': -1, 'DP': -2, 'VS': -3} 8 | 9 | _atomic_mass = {'H': 1.008, 'He': 4.003, 'Li': 6.941, 'Be': 9.012, 'B': 10.811, 10 | 'C': 12.011, 'N': 14.006, 'O': 15.999, 'F': 18.998, 'Ne': 20.180, 11 | 'Na': 22.990, 'Mg': 24.305, 'Al': 26.982, 'Si': 28.086, 'P': 30.974, 12 | 'S': 32.065, 'Cl': 35.453, 'Ar': 39.948, 'K': 39.098, 'Ca': 40.078, 13 | 'Ti': 47.867, 'Fe': 55.845, 'Zn': 65.38, 'Se': 78.971, 'Br': 79.904, 14 | 'Kr': 83.798, 'Mo': 95.96, 'Ru': 101.07, 'Sn': 118.710, 'Te': 127.60, 15 | 'I': 126.904, 'Xe': 131.293, 'UNK': 0.0, 'DP': 0.0, 'VS': 0.0} 16 | 17 | _atomic_symbol = {v: k for k, v in _atomic_number.items()} 18 | 19 | 20 | class Element(): 21 | def __init__(self, arg): 22 | if isinstance(arg, int): 23 | self.number = arg 24 | self.symbol = _atomic_symbol[arg] 25 | elif isinstance(arg, str): 26 | self.symbol = arg 27 | self.number = _atomic_number[arg] 28 | else: 29 | raise Exception('Element should be initiated with atomic number or symbol') 30 | 31 | self.mass = _atomic_mass[self.symbol] 32 | 33 | def __repr__(self): 34 | return f'' 35 | 36 | @staticmethod 37 | def guess_from_atom_type(type): 38 | ''' 39 | Guess the element from the first two characters of atom type 40 | Will only guess TEAM and CL&P atom types 41 | TEAM atom types are lowercase 42 | CL&P atom types are title case 43 | ''' 44 | if type[0].islower(): 45 | type = str.upper(type[0]) + type[1:] 46 | if type[:2] in _atomic_number.keys(): 47 | symbol = type[:2] 48 | elif type[0] in _atomic_number.keys(): 49 | symbol = type[0] 50 | else: 51 | symbol = 'UNK' 52 | return Element(symbol) 53 | -------------------------------------------------------------------------------- /mstk/chem/rdkit.py: -------------------------------------------------------------------------------- 1 | from mstk.errors import RDKitError 2 | 3 | try: 4 | from rdkit.Chem import AllChem as Chem 5 | except ImportError: 6 | RDKIT_FOUND = False 7 | else: 8 | RDKIT_FOUND = True 9 | 10 | 11 | def create_mol_from_smiles(smiles): 12 | ''' 13 | Create a RDKit molecule object from SMILES string. 14 | 15 | Parameters 16 | ---------- 17 | smiles : str 18 | 19 | Returns 20 | ------- 21 | rdmol : rdkit.rdchem.Mol 22 | ''' 23 | if not RDKIT_FOUND: 24 | raise ImportError('RDKit is required for parsing SMILES') 25 | 26 | try: 27 | rdmol = Chem.MolFromSmiles(smiles) 28 | except: 29 | raise RDKitError('Invalid SMILES') 30 | 31 | rdmol = Chem.AddHs(rdmol) 32 | if Chem.EmbedMolecule(rdmol, useRandomCoords=True) == -1: 33 | if Chem.EmbedMolecule(rdmol) == -1: 34 | raise RDKitError('Failed generating 3D coordinates') 35 | 36 | return rdmol 37 | -------------------------------------------------------------------------------- /mstk/cli/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from . import analyze_rdf, build, export, ffconv, logplot, topconv, trjconv, wham_pp 3 | 4 | 5 | def main(): 6 | parser = argparse.ArgumentParser(prog="mstk", description="Molecular Simulation Toolkit", 7 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 8 | subparsers = parser.add_subparsers(dest="command", required=True) 9 | 10 | # Add subcommands 11 | build.add_subcommand(subparsers) 12 | export.add_subcommand(subparsers) 13 | 14 | ffconv.add_subcommand(subparsers) 15 | topconv.add_subcommand(subparsers) 16 | trjconv.add_subcommand(subparsers) 17 | 18 | logplot.add_subcommand(subparsers) 19 | analyze_rdf.add_subcommand(subparsers) 20 | wham_pp.add_subcommand(subparsers) 21 | 22 | args = parser.parse_args() 23 | args.func(args) # Call the function associated with the subcommand 24 | -------------------------------------------------------------------------------- /mstk/cli/ffconv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from mstk.forcefield import ForceField 5 | 6 | 7 | def add_subcommand(subparsers): 8 | parser = subparsers.add_parser('ffconv', help='Convert force field files', 9 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 10 | parser.add_argument('-f', '--ff', nargs='+', type=str, required=True, help='force field files') 11 | parser.add_argument('-o', '--output', required=True, type=str, help='output file') 12 | 13 | parser.set_defaults(func=main) 14 | 15 | 16 | def main(args): 17 | ff = ForceField.open(*args.ff) 18 | print('%6i atom types\n' 19 | '%6i virtual site terms\n' 20 | '%6i self vdW terms\n' 21 | '%6i pairwise vdW terms\n' 22 | '%6i charge increment terms\n' 23 | '%6i bond terms\n' 24 | '%6i angle terms\n' 25 | '%6i dihedral terms\n' 26 | '%6i improper terms\n' 27 | '%6i polar terms' % ( 28 | len(ff.atom_types), len(ff.virtual_site_terms), len(ff.vdw_terms), len(ff.pairwise_vdw_terms), 29 | len(ff.bci_terms), len(ff.bond_terms), len(ff.angle_terms), 30 | len(ff.dihedral_terms), len(ff.improper_terms), len(ff.polar_terms) 31 | )) 32 | 33 | ff.write(args.output) 34 | -------------------------------------------------------------------------------- /mstk/cli/topconv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import numpy as np 5 | from mstk.topology import Topology 6 | from mstk.trajectory import Trajectory 7 | from mstk.forcefield import ForceField 8 | from mstk import logger 9 | 10 | 11 | def add_subcommand(subparsers): 12 | parser = subparsers.add_parser('topconv', help='Convert topology files', 13 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 14 | parser.add_argument('-p', '--top', nargs='+', type=str, required=True, help='topology files') 15 | parser.add_argument('-n', '--number', nargs='+', type=int, help='number of molecules') 16 | parser.add_argument('-c', '--conf', type=str, 17 | help='configuration file with positions and box. The last frame will be used. ' 18 | 'This is for writing topology in formats supporting positions, e.g. PDB, XYZ') 19 | parser.add_argument('-o', '--output', required=True, type=str, help='output topology file') 20 | parser.add_argument('--ignore', nargs='+', default=[], type=str, help='ignore these molecule types') 21 | parser.add_argument('-f', '--ff', type=str, help='reassign charge from FF') 22 | parser.add_argument('--qscale', default=1, type=float, help='scale the charge of atoms') 23 | parser.add_argument('--qscaleignore', nargs='+', default=[], type=str, 24 | help='ignore these molecule names for charge scaling') 25 | parser.add_argument('--box', nargs='+', type=float, 26 | help='overwrite the box dimensions') 27 | parser.add_argument('--shift', nargs=3, default=[0, 0, 0], type=float, 28 | help='shift the positions of all atoms') 29 | parser.add_argument('--ua', action='store_true', 30 | help='remove hydrogen atoms bonded to C/Si. ' 31 | 'Also remove the relevant bonds/angles/dihedrals/impropers') 32 | 33 | parser.set_defaults(func=main) 34 | 35 | 36 | def main(args): 37 | top_list = [Topology.open(inp) for inp in args.top] 38 | if args.number is None: 39 | args.number = [1] * len(top_list) 40 | 41 | molecules = [] 42 | for top, n in zip(top_list, args.number): 43 | molecules.extend(top.molecules * n) 44 | if args.ignore: 45 | molecules = [mol for mol in molecules if mol.name not in args.ignore] 46 | 47 | top = top_list[0] 48 | top.update_molecules(molecules) 49 | logger.info(str(top)) 50 | 51 | if args.conf: 52 | frame = Trajectory.read_frame_from_file(args.conf, -1) 53 | top.cell.set_box(frame.cell.vectors) 54 | top.set_positions(frame.positions) 55 | 56 | if args.ua: 57 | for mol in top.molecules: 58 | mol.remove_non_polar_hydrogens(update_topology=False) 59 | top.update_molecules(top.molecules) 60 | logger.info(str(top)) 61 | 62 | if args.ff: 63 | ff = ForceField.open(args.ff) 64 | ff.assign_charge(top) 65 | logger.info(f'Net charge = {sum(a.charge for a in top.atoms)}') 66 | 67 | if args.qscale != 1: 68 | for mol in top.molecules: 69 | if mol.name in args.qscaleignore: 70 | continue 71 | for atom in mol.atoms: 72 | atom.charge *= args.qscale 73 | 74 | if args.box is not None: 75 | if len(args.box) == 3: 76 | box = args.box 77 | elif len(args.box) == 1: 78 | box = args.box * 3 79 | else: 80 | raise Exception('box should be float or list of three floats') 81 | top.cell.set_box(box) 82 | 83 | if top.has_position and set(args.shift) != {0}: 84 | for atom in top.atoms: 85 | atom.position += args.shift 86 | 87 | if not top.has_position: 88 | top.set_positions(np.zeros((top.n_atom, 3), dtype=float)) 89 | 90 | top.write(args.output) 91 | -------------------------------------------------------------------------------- /mstk/cli/trjconv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import argparse 5 | import numpy as np 6 | 7 | from mstk.topology import Topology 8 | from mstk.trajectory import Trajectory 9 | from mstk import logger 10 | 11 | 12 | def add_subcommand(subparsers): 13 | parser = subparsers.add_parser('trjconv', help='Convert trajectory files', 14 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 15 | parser.add_argument('-p', '--top', type=str, required=True, help='topology file') 16 | parser.add_argument('-c', '--conf', nargs='+', type=str, required=True, help='trajectory files') 17 | parser.add_argument('-o', '--output', required=True, type=str, help='output trajectory file') 18 | parser.add_argument('-b', '--begin', default=0, type=int, 19 | help='read from this frame (included). ' 20 | 'A negative value has the same meaning as in Python list') 21 | parser.add_argument('-e', '--end', type=int, 22 | help='read until this frame (not included). ' 23 | 'A negative value has the same meaning as in Python list. ' 24 | 'If not set, will read until the end of the trajectory') 25 | parser.add_argument('--skip', default=1, type=int, help='read every N frames') 26 | parser.add_argument('--ignore', nargs='+', default=[], type=str, help='ignore these molecule names') 27 | parser.add_argument('--ignoreatom', nargs='+', default=[], type=str, help='ignore these atom types') 28 | parser.add_argument('--wrapfirst', action='store_true', 29 | help='shift the positions of all atoms so that all residues in the first frame are in the main cell') 30 | parser.add_argument('--box', nargs=3, default=[-1, -1, -1], type=float, 31 | help='overwrite the box dimensions') 32 | parser.add_argument('--boxscale', nargs=3, default=[1, 1, 1], type=float, 33 | help='scale the the box dimensions') 34 | parser.add_argument('--shift', nargs=3, default=[0, 0, 0], type=float, 35 | help='shift the positions of all atoms') 36 | parser.add_argument('--center', action='store_true', 37 | help='translate the postitions of all atoms so that the center of the system is at the center of box') 38 | 39 | parser.set_defaults(func=main) 40 | 41 | 42 | def main(args): 43 | top = Topology.open(args.top) 44 | logger.info(top) 45 | 46 | trj = Trajectory.open(args.conf) 47 | logger.info(trj) 48 | 49 | if (top.n_atom != trj.n_atom): 50 | raise Exception('Number of atoms in topology and trajectory files do not match') 51 | 52 | trj_out = Trajectory.open(args.output, 'w') 53 | 54 | if args.ignore or args.ignoreatom: 55 | subset = [atom.id for atom in top.atoms 56 | if atom.type not in args.ignoreatom and atom.molecule.name not in args.ignore] 57 | else: 58 | subset = None 59 | 60 | if args.begin < 0: 61 | args.begin += trj.n_frame 62 | if args.end is None or args.end > trj.n_frame: 63 | args.end = trj.n_frame 64 | elif args.end < 0: 65 | args.end += trj.n_frame 66 | 67 | cell_offset = np.zeros((top.n_atom, 3), dtype=int) 68 | if args.wrapfirst: 69 | frame = trj.read_frame(args.begin) 70 | box = frame.cell.get_size() 71 | for res in top.residues: 72 | ids = [atom.id for atom in res.atoms] 73 | positions = frame.positions[ids] 74 | center = np.sum(positions, axis=0) / len(positions) 75 | cell_offset[ids] = np.floor(center / box).astype(int) 76 | 77 | if any(val != 0 for val in args.shift): 78 | pos_shift = np.array([args.shift] * top.n_atom) 79 | else: 80 | pos_shift = None 81 | 82 | for i in range(args.begin, args.end, args.skip): 83 | sys.stdout.write('\r %i' % i) 84 | frame = trj.read_frame(i) 85 | box = frame.cell.get_size() 86 | for k in range(3): 87 | if args.box[k] != -1: 88 | box[k] = args.box[k] 89 | box[k] *= args.boxscale[k] 90 | frame.cell.set_box(box) 91 | if args.wrapfirst: 92 | frame.positions -= cell_offset * box 93 | if pos_shift is not None: 94 | frame.positions += pos_shift 95 | if args.center: 96 | center = np.sum(frame.positions, axis=0) / top.n_atom 97 | frame.positions += box / 2 - center 98 | trj_out.write_frame(frame, top, subset=subset) 99 | trj_out.close() 100 | -------------------------------------------------------------------------------- /mstk/data/forcefield/primitive.smt: -------------------------------------------------------------------------------- 1 | ## This is an example for defining atom types 2 | ## It is not supposed to be used for production 3 | 4 | TypingEngine SmartsTyper 5 | 6 | TypeDefinition 7 | 8 | H [#1] 9 | H1 [#1;X1] 10 | H1p [#1;X1][#7,#8] 11 | C [#6] 12 | C4 [#6;X4] 13 | C3 [#6;X3] 14 | C3a [c;X3] 15 | C3=O [C;X3]=O 16 | C2 [#6;X2] 17 | N [#7] 18 | N3 [#7;X3] 19 | N3CO [N;X3]C=O 20 | O [#8] 21 | O2 [#8;X2] 22 | O2w [O;X2;H2] 23 | O1 [O;X1] 24 | 25 | 26 | HierarchicalTree 27 | 28 | H 29 | H1 30 | H1p 31 | C 32 | C4 33 | C3 34 | C3a 35 | C3=O 36 | C2 37 | N 38 | N3 39 | N3CO 40 | O 41 | O2 42 | O2w 43 | O1 44 | -------------------------------------------------------------------------------- /mstk/errors.py: -------------------------------------------------------------------------------- 1 | class PackmolError(Exception): 2 | pass 3 | 4 | 5 | class GmxError(Exception): 6 | pass 7 | 8 | 9 | class RDKitError(Exception): 10 | pass 11 | 12 | 13 | class SchedulerError(Exception): 14 | pass 15 | -------------------------------------------------------------------------------- /mstk/forcefield/__init__.py: -------------------------------------------------------------------------------- 1 | from .ffterm import * 2 | from .forcefield import ForceField 3 | from .io.zfp import Zfp 4 | from .io.zff import Zff 5 | from .io.ppf import Ppf 6 | from .io.padua import Padua, PaduaLJScaler 7 | from .typer import * 8 | -------------------------------------------------------------------------------- /mstk/forcefield/errors.py: -------------------------------------------------------------------------------- 1 | class FFTermNotFoundError(Exception): 2 | pass 3 | 4 | 5 | class TypingNotSupportedError(Exception): 6 | pass 7 | 8 | 9 | class TypingUndefinedError(Exception): 10 | pass 11 | 12 | 13 | class ChargeIncrementNonZeroError(Exception): 14 | pass 15 | -------------------------------------------------------------------------------- /mstk/forcefield/io/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/mstk/forcefield/io/__init__.py -------------------------------------------------------------------------------- /mstk/forcefield/io/zfp.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from mstk.forcefield.forcefield import ForceField 3 | from mstk.forcefield.ffterm import * 4 | from mstk import logger 5 | 6 | 7 | class Zfp: 8 | ''' 9 | Load ForceField from ZFP file. 10 | 11 | XML language is used by ZFP file to serialize the setting and all the FFTerms in a ForceField object. 12 | 13 | In ZFP file, there is no leading 1/2 for harmonic energy terms. 14 | The length is in unit of nm, and angle is in unit of degree. 15 | The energies for harmonic bond, harmonic angle and vdW are in unit of kJ/mol/nm^2, kJ/mol/rad^2 and kJ/mol. 16 | 17 | A force field term describing one topology element should only appear once. 18 | If there are duplicated terms, an Exception will be raised. 19 | e.g. HarmonicAngleTerm('c_4', 'c_4', 'h_1') and SDKAngleTerm('c_4', 'c_4', 'h_1') are considered as duplicated, 20 | because both of them are describing the angle type ('c_4', 'c_4', 'h_1'). 21 | LJ126Term('c_4', 'h_1') and MieTerm('c_4', 'h_1') are also duplicated, 22 | because both of them are describing the vdW interactions between atom types 'c_4' and 'h_1'. 23 | 24 | ZFP format is deprecated. New features are not supported by ZFP format 25 | 1. Comments for force field and ffterm 26 | 27 | Parameters 28 | ---------- 29 | file : str 30 | 31 | Attributes 32 | ---------- 33 | forcefield : ForceField 34 | ''' 35 | 36 | def __init__(self, file): 37 | logger.warning('ZFP format is deprecated. Consider converting it to a ZFF file with ffconv.py') 38 | self.forcefield = ForceField() 39 | self._parse(file) 40 | 41 | def _parse(self, file): 42 | try: 43 | tree = ET.ElementTree(file=file) 44 | except: 45 | raise Exception('Invalid ZFP file') 46 | 47 | root = tree.getroot() 48 | if root is None: 49 | raise Exception('Empty ZFP file') 50 | 51 | ff = self.forcefield 52 | 53 | node = root.find('Setting') 54 | ff.vdw_cutoff = float(node.attrib['vdw_cutoff']) 55 | ff.vdw_long_range = node.attrib['vdw_long_range'] 56 | ff.lj_mixing_rule = node.attrib['lj_mixing_rule'] 57 | ff.scale_14_vdw = float(node.attrib['scale_14_vdw']) 58 | ff.scale_14_coulomb = float(node.attrib['scale_14_coulomb']) 59 | 60 | tags = { 61 | 'AtomTypes' : ff.atom_types, 62 | 'VirtualSiteTerms' : ff.virtual_site_terms, 63 | 'ChargeIncrementTerms': ff.bci_terms, 64 | 'VdwTerms' : ff.vdw_terms, 65 | 'PairwiseVdwTerms' : ff.pairwise_vdw_terms, 66 | 'BondTerms' : ff.bond_terms, 67 | 'AngleTerms' : ff.angle_terms, 68 | 'DihedralTerms' : ff.dihedral_terms, 69 | 'ImproperTerms' : ff.improper_terms, 70 | 'PolarTerms' : ff.polar_terms, 71 | } 72 | _duplicated = [] 73 | for tag, d in tags.items(): 74 | node = root.find(tag) 75 | if node is None: 76 | continue 77 | for element in node: 78 | try: 79 | term = FFTermFactory.create_from_zfp(element.tag, element.attrib) 80 | except: 81 | raise Exception('Invalid tag or attributes: %s, %s' % 82 | (element.tag, str(element.attrib))) 83 | if term.name in d.keys(): 84 | _duplicated.append(term) 85 | else: 86 | d[term.name] = term 87 | 88 | if _duplicated: 89 | msg = f'{len(_duplicated)} duplicated terms from {file}' 90 | for term in _duplicated: 91 | msg += f'\n {term}' 92 | logger.error(msg) 93 | raise Exception('Duplicated terms in ZFP file') 94 | 95 | @staticmethod 96 | def save_to(ff, file): 97 | ''' 98 | Save ForceField to a ZFP file 99 | 100 | Parameters 101 | ---------- 102 | ff : ForceField 103 | file : str 104 | ''' 105 | raise NotImplementedError('Writing to ZFP file has been deprecated') 106 | 107 | 108 | ForceField.register_format('.zfp', Zfp) 109 | -------------------------------------------------------------------------------- /mstk/forcefield/typer/__init__.py: -------------------------------------------------------------------------------- 1 | from .typer import Typer 2 | from .smarts_typer import SmartsTyper 3 | from .gaff_typer import GaffTyper 4 | -------------------------------------------------------------------------------- /mstk/forcefield/typer/gaff_typer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from mstk import DIR_MSTK 3 | from mstk.topology import Bond 4 | from .smarts_typer import SmartsTyper 5 | 6 | 7 | class GaffTyper(SmartsTyper): 8 | ''' 9 | `GaffTyper` is specifically designed for GAFF atom type assignment. 10 | By default, it uses the type definition file `gaff.smt` 11 | 12 | Compared with `SmartsTyper`, this class will handle conjugated atom types according to the GAFF convention, 13 | e.g. cc/cd, ce/cf etc... 14 | 15 | The official tool for GAFF - antechamber has several issues: 16 | - Mysterious aromatic assignment 17 | - A lost of parameters missing. `parmchk2` gives unreasonable guess for torsion parameters, e.g. biphenyl 18 | 19 | Parameters 20 | ---------- 21 | file : str or file-like object, optional 22 | Type definition file. 23 | 24 | Notes 25 | ----- 26 | * SMARTS is parsed by using RDKit package. Make sure it is installed. 27 | * In type definition file, empty lines are ignored, and comments should start with ##. 28 | 29 | ''' 30 | 31 | def __init__(self, file=None): 32 | file = file or os.path.join(DIR_MSTK, 'data', 'forcefield', 'gaff.smt') 33 | super().__init__(file) 34 | 35 | def _type_molecule(self, molecule): 36 | ''' 37 | Assign atom types in the molecule with rules in type definition file. 38 | 39 | The :attr:`~mstk.topology.Atom.type` attribute of all atoms in the molecule will be updated. 40 | 41 | GaffTyper use RDKit to do SMARTS matching, therefore the orders must be set for all the bonds in the molecule. 42 | Usually it means the molecule be initialized from SMILES. 43 | with :func:`~mstk.topology.Molecule.from_smiles` or :func:`~mstk.topology.Molecule.from_rdmol` 44 | 45 | If an atom can not match any type by the predefined SMARTS patterns, an Exception will be raised. 46 | 47 | Parameters 48 | ---------- 49 | molecule : Molecule 50 | ''' 51 | super()._type_molecule(molecule) 52 | 53 | # find the chain of conjugated atom types, add chose one from the two types for each atom 54 | conj_atoms = [atom for atom in molecule.atoms if '|' in atom.type] 55 | if not conj_atoms: 56 | return 57 | 58 | conj1_partners = {atom: [] for atom in conj_atoms} 59 | conj2_partners = {atom: [] for atom in conj_atoms} 60 | for atom in conj_atoms: 61 | for bond in atom.bonds: 62 | partner = bond.atom2 if bond.atom1 is atom else bond.atom1 63 | if partner not in conj_atoms: 64 | continue 65 | if bond.order == Bond.Order.DOUBLE or bond.is_aromatic: 66 | conj2_partners[atom].append(partner) 67 | else: 68 | conj1_partners[atom].append(partner) 69 | 70 | flag = {atom: False for atom in conj_atoms} 71 | 72 | def set_conj_type_partners(atom): 73 | type_suffix = atom.type[-1] 74 | for partner in conj1_partners[atom]: 75 | if flag[partner]: 76 | continue 77 | candidates = partner.type.split('|') 78 | try: 79 | partner.type = next(typ for typ in candidates if typ.endswith(type_suffix)) 80 | except StopIteration: 81 | partner.type = candidates[0] 82 | flag[partner] = True 83 | set_conj_type_partners(partner) 84 | for partner in conj2_partners[atom]: 85 | if flag[partner]: 86 | continue 87 | candidates = partner.type.split('|') 88 | try: 89 | partner.type = next(typ for typ in candidates if not typ.endswith(type_suffix)) 90 | except StopIteration: 91 | partner.type = candidates[0] 92 | flag[partner] = True 93 | set_conj_type_partners(partner) 94 | 95 | for atom in conj_atoms: 96 | if flag[atom]: 97 | continue 98 | atom.type = atom.type.split('|')[0] 99 | flag[atom] = True 100 | set_conj_type_partners(atom) 101 | -------------------------------------------------------------------------------- /mstk/forcefield/typer/typer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from mstk import MSTK_FORCEFIELD_PATH 3 | 4 | 5 | class Typer: 6 | ''' 7 | Base class for typing engines. 8 | 9 | Typing engine assigns atom types for atoms in a molecule or topology by some predefined rules. 10 | It is the very first step for force field assignment. 11 | It is also the basis of force field development. 12 | A well defined typing rule will make the force field development much less painful. 13 | ''' 14 | 15 | def type(self, top_or_mol): 16 | ''' 17 | Assign types for all atoms in a topology or molecule. 18 | 19 | The :attr:`~mstk.topology.Atom.type` attribute of all atoms in the topology/molecule will be updated. 20 | 21 | Parameters 22 | ---------- 23 | top_or_mol: Topology, Molecule 24 | ''' 25 | from mstk.topology import Topology, Molecule 26 | 27 | if type(top_or_mol) is Topology: 28 | for mol in top_or_mol.molecules: 29 | self._type_molecule(mol) 30 | elif type(top_or_mol) is Molecule: 31 | self._type_molecule(top_or_mol) 32 | else: 33 | raise Exception('A topology or molecule is expected') 34 | 35 | def _type_molecule(self, molecule): 36 | ''' 37 | Assign types for all the atoms in the molecule. 38 | This method should be implemented by subclasses. 39 | 40 | Parameters 41 | ---------- 42 | molecule : Molecule 43 | ''' 44 | raise NotImplementedError('This method haven\'t been implemented') 45 | 46 | @staticmethod 47 | def open(filename): 48 | ''' 49 | Load a typer from a type definition file. 50 | 51 | The typing engine is determined by the TypingEngine line in the file. 52 | 53 | Parameters 54 | ---------- 55 | filename : str 56 | Type definition file. 57 | If the file does not exist, will search it under directories defined by MSTK_FORCEFIELD_PATH. 58 | 59 | Returns 60 | ------- 61 | typer : subclass of Typer 62 | ''' 63 | from .smarts_typer import SmartsTyper 64 | from .gaff_typer import GaffTyper 65 | 66 | for dir in ['.'] + MSTK_FORCEFIELD_PATH: 67 | p = os.path.join(dir, filename) 68 | if os.path.exists(p): 69 | filepath = p 70 | break 71 | else: 72 | raise Exception(f'Typing file not found: {filename}') 73 | 74 | engine = 'SmartsTyper' 75 | with open(p) as f: 76 | for line in f: 77 | if line.lower().startswith('typingengine'): 78 | engine = line.split()[1] 79 | break 80 | 81 | if engine.lower() == 'smartstyper': 82 | return SmartsTyper(filepath) 83 | elif engine.lower() == 'gafftyper': 84 | return GaffTyper(filepath) 85 | else: 86 | raise Exception(f'Unknown typing engine: {engine}') 87 | -------------------------------------------------------------------------------- /mstk/misc/__init__.py: -------------------------------------------------------------------------------- 1 | from .singleton import Singleton 2 | from .docmeta import DocstringMeta -------------------------------------------------------------------------------- /mstk/misc/docmeta.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class DocstringMeta(abc.ABCMeta): 5 | ''' Metaclass that allows docstring inheritance ''' 6 | 7 | def __new__(mcls, classname, bases, cls_dict): 8 | cls = abc.ABCMeta.__new__(mcls, classname, bases, cls_dict) 9 | mro = cls.__mro__[1:] 10 | for name, member in cls_dict.items(): 11 | if not getattr(member, '__doc__'): 12 | for base in mro: 13 | try: 14 | member.__doc__ = getattr(base, name).__doc__ 15 | break 16 | except AttributeError: 17 | pass 18 | return cls 19 | -------------------------------------------------------------------------------- /mstk/misc/singleton.py: -------------------------------------------------------------------------------- 1 | class Singleton(): 2 | _instance = None 3 | def __new__(cls, *args, **kwargs): 4 | if not isinstance(cls._instance, cls): 5 | cls._instance = object.__new__(cls, *args, **kwargs) 6 | return cls._instance -------------------------------------------------------------------------------- /mstk/ommhelper/__init__.py: -------------------------------------------------------------------------------- 1 | from .grofile import GroFile 2 | from .reporter import * 3 | from .utils import * 4 | from .force import * 5 | from .unit import * 6 | -------------------------------------------------------------------------------- /mstk/ommhelper/reporter/__init__.py: -------------------------------------------------------------------------------- 1 | from .checkpointreporter import CheckpointReporter 2 | from .drudetemperaturereporter import DrudeTemperatureReporter 3 | from .groreporter import GroReporter 4 | from .statedatareporter import StateDataReporter 5 | from .viscosityreporter import ViscosityReporter 6 | from .maxforcereporter import MaxForceReporter -------------------------------------------------------------------------------- /mstk/ommhelper/reporter/checkpointreporter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import openmm.openmm as mm 3 | 4 | 5 | class CheckpointReporter(): 6 | ''' 7 | CheckpointReporter saves periodic checkpoints of a simulation. 8 | The checkpoints will overwrite old files -- only the latest three will be kept. 9 | State XML files can be saved together, in case the checkpoint files are broken. 10 | 11 | Parameters 12 | ---------- 13 | file : string 14 | The file to write to. 15 | Any current contents will be overwritten. 16 | The latest three checkpoint will be kept with the step appended to the file name. 17 | reportInterval : int 18 | The interval (in time steps) at which to write checkpoints. 19 | xml : string, optional 20 | If provided, the state will be serialized into XML format and saved together with checkpoint. 21 | Any current contents will be overwritten. 22 | The latest three XML files will be kept with the step appended to the file name. 23 | ''' 24 | 25 | def __init__(self, file, reportInterval, xml=None): 26 | self._reportInterval = reportInterval 27 | self._file = file 28 | self._xml = xml 29 | 30 | if type(file) is not str: 31 | raise Exception('file should be str') 32 | 33 | def describeNextReport(self, simulation): 34 | """Get information about the next report this object will generate. 35 | 36 | Parameters 37 | ---------- 38 | simulation : Simulation 39 | The Simulation to generate a report for 40 | 41 | Returns 42 | ------- 43 | tuple 44 | A five element tuple. The first element is the number of steps 45 | until the next report. The remaining elements specify whether 46 | that report will require positions, velocities, forces, and 47 | energies respectively. 48 | """ 49 | steps = self._reportInterval - simulation.currentStep % self._reportInterval 50 | return (steps, True, True, False, False, False) 51 | 52 | def report(self, simulation, state): 53 | """Generate a report. 54 | 55 | Parameters 56 | ---------- 57 | simulation : Simulation 58 | The Simulation to generate a report for 59 | state : State 60 | The current state of the simulation 61 | """ 62 | 63 | filename = self._file + '_%i' % simulation.currentStep 64 | with open(filename, 'wb') as out: 65 | out.write(simulation.context.createCheckpoint()) 66 | 67 | file_prev3 = self._file + '_%i' % (simulation.currentStep - 3 * self._reportInterval) 68 | if os.path.exists(file_prev3): 69 | os.remove(file_prev3) 70 | 71 | if self._xml is not None: 72 | xml_name = self._xml + '_%i' % simulation.currentStep 73 | xml = mm.XmlSerializer.serialize(state) 74 | with open(xml_name, 'w') as f: 75 | f.write(xml) 76 | 77 | xml_prev3 = self._xml + '_%i' % (simulation.currentStep - 3 * self._reportInterval) 78 | if os.path.exists(xml_prev3): 79 | os.remove(xml_prev3) 80 | -------------------------------------------------------------------------------- /mstk/ommhelper/reporter/groreporter.py: -------------------------------------------------------------------------------- 1 | import math 2 | from .. import GroFile 3 | 4 | 5 | class GroReporter: 6 | ''' 7 | GroReporter outputs a series of frames from a Simulation to a GRO file. 8 | 9 | Parameters 10 | ---------- 11 | file : string 12 | The file to write to 13 | reportInterval : int 14 | The interval (in time steps) at which to write frames 15 | logarithm : bool 16 | If set to True, then write trajectory at logarithm interval. 17 | reportInterval will be the minimum step for reporting. 18 | e.g. when reportInterval set to 30, then report at [30, 40, 50, ..., 90, 100, 200, ..., 900, 1000, 2000, ...] steps. 19 | enforcePeriodicBox: bool, Optional 20 | Specifies whether particle positions should be translated 21 | so the center of every molecule lies in the same periodic box. 22 | If None (the default), it will automatically decide whether to translate molecules 23 | based on whether the system being simulated uses periodic boundary conditions. 24 | subset : list of int, Optional 25 | If not None, only the selected atoms will be written 26 | reportVelocity: bool 27 | If set to True, velocities will be reported 28 | append: bool 29 | If set to True, will append to file 30 | ''' 31 | 32 | def __init__(self, file, reportInterval, logarithm=False, enforcePeriodicBox=None, subset=None, 33 | reportVelocity=False, append=False): 34 | self._reportInterval = reportInterval 35 | self._logarithm = logarithm 36 | self._enforcePeriodicBox = enforcePeriodicBox 37 | if append: 38 | self._out = open(file, 'a') 39 | else: 40 | self._out = open(file, 'w') 41 | self._reportVelocity = reportVelocity 42 | 43 | if subset is None: 44 | self._subset = None 45 | else: 46 | self._subset = subset[:] 47 | 48 | def describeNextReport(self, simulation): 49 | """Get information about the next report this object will generate. 50 | 51 | Parameters 52 | ---------- 53 | simulation : Simulation 54 | The Simulation to generate a report for 55 | 56 | Returns 57 | ------- 58 | tuple 59 | A six element tuple. The first element is the number of steps 60 | until the next report. The next four elements specify whether 61 | that report will require positions, velocities, forces, and 62 | energies respectively. The final element specifies whether 63 | positions should be wrapped to lie in a single periodic box. 64 | """ 65 | if self._logarithm: 66 | if simulation.currentStep < self._reportInterval: 67 | _base = self._reportInterval 68 | else: 69 | _base = 10 ** math.floor(math.log10(simulation.currentStep)) 70 | steps = _base - simulation.currentStep % _base 71 | else: 72 | steps = self._reportInterval - simulation.currentStep % self._reportInterval 73 | 74 | return (steps, True, self._reportVelocity, False, False, self._enforcePeriodicBox) 75 | 76 | def report(self, simulation, state): 77 | """Generate a report. 78 | 79 | Parameters 80 | ---------- 81 | simulation : Simulation 82 | The Simulation to generate a report for 83 | state : State 84 | The current state of the simulation 85 | """ 86 | time = state.getTime() 87 | positions = state.getPositions(asNumpy=True) 88 | velocities = state.getVelocities(asNumpy=True) if self._reportVelocity else None 89 | vectors = state.getPeriodicBoxVectors() 90 | GroFile.writeFile(simulation.topology, positions, vectors, self._out, time, self._subset, velocities) 91 | 92 | if hasattr(self._out, 'flush') and callable(self._out.flush): 93 | self._out.flush() 94 | 95 | def __del__(self): 96 | self._out.close() 97 | -------------------------------------------------------------------------------- /mstk/ommhelper/reporter/maxforcereporter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class MaxForceReporter: 5 | ''' 6 | MaxForceReporter outputs the atom experiencing the largest force 7 | 8 | Parameters 9 | ---------- 10 | file : string or file 11 | The file to write to, specified as a file name or file object 12 | reportInterval : int 13 | The interval (in time steps) at which to write frames 14 | append : bool 15 | If set to True, will append to file 16 | ''' 17 | 18 | def __init__(self, file, reportInterval, append=False): 19 | self._reportInterval = reportInterval 20 | self._openedFile = isinstance(file, str) 21 | if self._openedFile: 22 | if append: 23 | self._out = open(file, 'a') 24 | else: 25 | self._out = open(file, 'w') 26 | else: 27 | self._out = file 28 | self._hasInitialized = False 29 | 30 | def describeNextReport(self, simulation): 31 | """Get information about the next report this object will generate. 32 | 33 | Parameters 34 | ---------- 35 | simulation : Simulation 36 | The Simulation to generate a report for 37 | 38 | Returns 39 | ------- 40 | tuple 41 | A six element tuple. The first element is the number of steps 42 | until the next report. The next four elements specify whether 43 | that report will require positions, velocities, forces, and 44 | energies respectively. The final element specifies whether 45 | positions should be wrapped to lie in a single periodic box. 46 | """ 47 | steps = self._reportInterval - simulation.currentStep % self._reportInterval 48 | 49 | return (steps, False, False, True, False, False) 50 | 51 | def report(self, simulation, state): 52 | """Generate a report. 53 | 54 | Parameters 55 | ---------- 56 | simulation : Simulation 57 | The Simulation to generate a report for 58 | state : State 59 | The current state of the simulation 60 | """ 61 | if not self._hasInitialized: 62 | self._hasInitialized = True 63 | print('#"Step"\t"Atom id"\t"Force (kJ/mol/nm)"', file=self._out) 64 | 65 | forces = state.getForces(asNumpy=True)._value 66 | fsq = np.sum(forces * forces, axis=1) 67 | imax = np.argmax(fsq) 68 | fmax = fsq[imax] ** 0.5 69 | self._out.write(f'{simulation.currentStep}\t{imax}\t{fmax:.3f}\n') 70 | 71 | if hasattr(self._out, 'flush') and callable(self._out.flush): 72 | self._out.flush() 73 | 74 | def __del__(self): 75 | if self._openedFile: 76 | self._out.close() 77 | -------------------------------------------------------------------------------- /mstk/ommhelper/reporter/viscosityreporter.py: -------------------------------------------------------------------------------- 1 | from openmm import app, unit 2 | from ..unit import * 3 | 4 | 5 | class ViscosityReporter: 6 | ''' 7 | ViscosityReporter report the velocity amplitude and reciprocal of viscosity using cosine periodic perturbation method. 8 | An integrator supporting this method is required. 9 | E.g. the VVIntegrator from https://github.com/z-gong/openmm-velocityVerlet. 10 | 11 | Parameters 12 | ---------- 13 | file : string or file 14 | The file to write to, specified as a file name or file object 15 | reportInterval : int 16 | The interval (in time steps) at which to write frames 17 | append : bool 18 | Whether or not append to the existing file. 19 | ''' 20 | 21 | def __init__(self, file, reportInterval, append=False): 22 | self._reportInterval = reportInterval 23 | self._openedFile = isinstance(file, str) 24 | if self._openedFile: 25 | if append: 26 | self._out = open(file, 'a') 27 | else: 28 | self._out = open(file, 'w') 29 | else: 30 | self._out = file 31 | self._hasInitialized = False 32 | 33 | def describeNextReport(self, simulation): 34 | """Get information about the next report this object will generate. 35 | 36 | Parameters 37 | ---------- 38 | simulation : Simulation 39 | The Simulation to generate a report for 40 | 41 | Returns 42 | ------- 43 | tuple 44 | A six element tuple. The first element is the number of steps 45 | until the next report. The next four elements specify whether 46 | that report will require positions, velocities, forces, and 47 | energies respectively. The final element specifies whether 48 | positions should be wrapped to lie in a single periodic box. 49 | """ 50 | try: 51 | simulation.integrator.getCosAcceleration() 52 | except AttributeError: 53 | raise Exception('This integrator does not calculate viscosity') 54 | 55 | steps = self._reportInterval - simulation.currentStep % self._reportInterval 56 | return (steps, False, False, False, False) 57 | 58 | def report(self, simulation: app.Simulation, state): 59 | """Generate a report. 60 | 61 | Parameters 62 | ---------- 63 | simulation : Simulation 64 | The Simulation to generate a report for 65 | state : State 66 | The current state of the simulation 67 | """ 68 | if not self._hasInitialized: 69 | self._hasInitialized = True 70 | print('#"Step"\t"Acceleration (nm/ps^2)"\t"VelocityAmplitude (nm/ps)"\t"1/Viscosity (1/Pa.s)"', 71 | file=self._out) 72 | 73 | acceleration = simulation.integrator.getCosAcceleration().value_in_unit(nm / ps ** 2) 74 | vMax, invVis = simulation.integrator.getViscosity() 75 | vMax = vMax.value_in_unit(nm / ps) 76 | invVis = invVis.value_in_unit((unit.pascal * unit.second) ** -1) 77 | print(simulation.currentStep, acceleration, vMax, invVis, sep='\t', file=self._out) 78 | 79 | if hasattr(self._out, 'flush') and callable(self._out.flush): 80 | self._out.flush() 81 | 82 | def __del__(self): 83 | if self._openedFile: 84 | self._out.close() 85 | -------------------------------------------------------------------------------- /mstk/ommhelper/unit.py: -------------------------------------------------------------------------------- 1 | from openmm import unit as _unit 2 | 3 | kelvin = _unit.kelvin 4 | bar = _unit.bar 5 | ps = _unit.picosecond 6 | nm = _unit.nanometer 7 | kJ_mol = _unit.kilojoule_per_mole 8 | kcal_mol = _unit.kilocalorie_per_mole 9 | qe = _unit.elementary_charge 10 | dalton = _unit.dalton 11 | item = _unit.item 12 | meter = _unit.meter 13 | angstrom = _unit.angstrom 14 | volt = _unit.volt 15 | farad = _unit.farad 16 | -------------------------------------------------------------------------------- /mstk/scheduler/__init__.py: -------------------------------------------------------------------------------- 1 | from .pbsjob import PbsJob 2 | from .scheduler import Scheduler, JobParameter 3 | from .slurm import Slurm 4 | from .remote_slurm import RemoteSlurm -------------------------------------------------------------------------------- /mstk/scheduler/pbsjob.py: -------------------------------------------------------------------------------- 1 | class PbsJob: 2 | ''' 3 | A PbsJob object is a job managed by scheduler. 4 | 5 | Parameters 6 | ---------- 7 | id : int 8 | Job ID assigned by job scheduler. 9 | name : str 10 | Name of this job in job scheduler. 11 | state: [0, 1, 9] 12 | Whether this job is pending (0), running (1) or done (9). 13 | Successfully finished, Killed and failed are all considered as done. 14 | workdir : str 15 | The working directory of this job. 16 | user : str 17 | Name of the user who submitted this job. 18 | queue : str 19 | The queue this job is submitted to. 20 | 21 | Attributes 22 | ---------- 23 | id : int 24 | Job ID assigned by job scheduler. 25 | name : str 26 | Name of this job in job scheduler. 27 | state: [0, 1, 9] 28 | Whether this job is pending (0), running (1) or done (9). 29 | Successfully finished, Killed and failed are all considered as done. 30 | workdir : str 31 | The working directory of this job. 32 | user : str 33 | Name of the user who submitted this job. 34 | queue : str 35 | The queue this job is submitted to. 36 | state_str : str 37 | The string shown by the job scheduler, which represents the state of the job. 38 | ''' 39 | 40 | def __init__(self, id, name, state, workdir=None, user=None, queue=None): 41 | self.id = id 42 | self.name = name 43 | self.state = state 44 | self.workdir = workdir 45 | self.user = user 46 | self.queue = queue 47 | self.state_str = None 48 | 49 | def __repr__(self): 50 | return '' % (self.id, self.name, self.state, self.user) 51 | 52 | def __eq__(self, other): 53 | return self.id == other.id 54 | 55 | class State: 56 | PENDING = 0 57 | RUNNING = 1 58 | DONE = 9 59 | -------------------------------------------------------------------------------- /mstk/simsys/__init__.py: -------------------------------------------------------------------------------- 1 | from .system import System 2 | from .lmpexporter import LammpsExporter 3 | from .gmxexporter import GromacsExporter 4 | from .namdexporter import NamdExporter 5 | from .ommexporter import OpenMMExporter -------------------------------------------------------------------------------- /mstk/topology/__init__.py: -------------------------------------------------------------------------------- 1 | from .atom import Atom 2 | from .virtualsite import * 3 | from .connectivity import * 4 | from .residue import Residue 5 | from .molecule import Molecule 6 | from .topology import Topology 7 | from .unitcell import UnitCell 8 | from .io.psf import Psf 9 | from .io.zmat import Zmat 10 | from .io.pdb import Pdb 11 | from .io.lammps import LammpsData 12 | from .io.xyz import XyzTopology 13 | from .io.smi import Smi 14 | from .io.gro import GroTopology 15 | from .io.mae import Mae 16 | from .io.sdf import Sdf 17 | -------------------------------------------------------------------------------- /mstk/topology/io/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/mstk/topology/io/__init__.py -------------------------------------------------------------------------------- /mstk/topology/io/gro.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mstk.chem.element import Element 3 | from mstk.topology.atom import Atom 4 | from mstk.topology.molecule import Molecule 5 | from mstk.topology.topology import Topology 6 | 7 | 8 | class GroTopology: 9 | ''' 10 | Generate Topology from GRO file. 11 | 12 | Atom name, residue information, unit cell and positions are parsed. 13 | The atom name colume will be used as both atom name and atom type. 14 | GRO file does not contain connectivity information. Each residue will be considered as a molecule. 15 | 16 | Only the first frame will be considered. 17 | 18 | Parameters 19 | ---------- 20 | file : str 21 | 22 | Attributes 23 | ---------- 24 | topology : Topology 25 | 26 | Examples 27 | -------- 28 | >>> gro = Gro('input.gro') 29 | >>> topology = gro.topology 30 | 31 | >>> Gro.save_to(topology, 'output.gro') 32 | ''' 33 | 34 | def __init__(self, file, **kwargs): 35 | self.topology = Topology() 36 | self._parse(file) 37 | 38 | def _parse(self, file): 39 | molecules = {} # {int: Molecule} 40 | n_atom = 0 41 | last_resid = None 42 | with open(file) as f: 43 | for i, line in enumerate(f): 44 | if i == 0: 45 | continue 46 | if i == 1: 47 | n_atom = int(line) 48 | continue 49 | if i == n_atom + 2: 50 | _box = tuple(map(float, line.split())) 51 | if len(_box) == 3: 52 | self.topology.cell.set_box(_box) 53 | elif len(_box) == 9: 54 | ax, by, cz, ay, az, bx, bz, cx, cy = _box 55 | self.topology.cell.set_box([[ax, ay, az], [bx, by, bz], [cx, cy, cz]]) 56 | else: 57 | raise ValueError('Invalid box') 58 | break 59 | 60 | res_id = int(line[:5]) 61 | res_name = line[5:10].strip() 62 | atom_name = line[10:15].strip() 63 | element = Element.guess_from_atom_type(atom_name) 64 | atom_id = int(line[15:20]) 65 | x = float(line[20:28]) 66 | y = float(line[28:36]) 67 | z = float(line[36:44]) 68 | 69 | atom = Atom(atom_name) 70 | atom.type = atom_name 71 | atom.symbol = element.symbol 72 | atom.mass = element.mass 73 | atom.position = (x, y, z,) 74 | 75 | if res_id != last_resid: 76 | mol = Molecule(res_name) 77 | molecules[res_id] = mol 78 | last_resid = res_id 79 | else: 80 | mol = molecules[res_id] 81 | mol.add_atom(atom) 82 | 83 | self.topology.update_molecules(list(molecules.values())) 84 | 85 | @staticmethod 86 | def save_to(top, file, **kwargs): 87 | ''' 88 | Save topology into a GRO file 89 | 90 | Parameters 91 | ---------- 92 | top : Topology 93 | file : str 94 | ''' 95 | if not top.has_position: 96 | raise Exception('Position is required for writing GRO file') 97 | 98 | string = 'Created by mstk\n' 99 | string += f'{top.n_atom}\n' 100 | 101 | if np.any(np.abs(top.positions) >= 1000): 102 | raise Exception('Positions are too large to be written in GRO format') 103 | 104 | for atom in top.atoms: 105 | pos = atom.position 106 | string += "%5i%-5s%5s%5i%8.3f%8.3f%8.3f\n" % ( 107 | (atom.residue.id + 1) % 100000, atom.residue.name[:5], atom.name[:5], (atom.id + 1) % 100000, 108 | pos[0], pos[1], pos[2] 109 | ) 110 | 111 | if top.cell.is_rectangular: 112 | string += ' %.4f %.4f %.4f\n' % tuple(top.cell.get_size()) 113 | else: 114 | a, b, c = top.cell.vectors 115 | string += ' %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f\n' % ( 116 | a[0], b[1], c[2], a[1], a[2], b[0], b[2], c[0], c[1]) 117 | 118 | with open(file, 'wb') as f: 119 | f.write(string.encode()) 120 | 121 | 122 | Topology.register_format('.gro', GroTopology) 123 | -------------------------------------------------------------------------------- /mstk/topology/io/sdf.py: -------------------------------------------------------------------------------- 1 | from rdkit import Chem 2 | from mstk.topology.atom import Atom 3 | from mstk.topology.molecule import Molecule 4 | from mstk.topology.topology import Topology 5 | from mstk.chem.element import Element 6 | 7 | 8 | class Sdf: 9 | ''' 10 | Generate Topology from SDF file. 11 | 12 | RDKit is used to parse SDF file. Atom symbol, positions, formal charges and bonds are parsed. 13 | Data fields are ignored. 14 | 15 | Parameters 16 | ---------- 17 | file : str 18 | kwargs : dict 19 | Ignored 20 | 21 | Attributes 22 | ---------- 23 | topology : Topology 24 | 25 | Examples 26 | -------- 27 | >>> sdf = Sdf('input.sdf') 28 | >>> topology = sdf.topology 29 | ''' 30 | 31 | def __init__(self, file, **kwargs): 32 | suppl = Chem.SDMolSupplier(file, removeHs=False) 33 | mols = [Molecule.from_rdmol(rdmol) for rdmol in suppl] 34 | 35 | self.topology = Topology(mols) 36 | 37 | 38 | Topology.register_format('.sdf', Sdf) 39 | -------------------------------------------------------------------------------- /mstk/topology/io/smi.py: -------------------------------------------------------------------------------- 1 | from mstk.topology.molecule import Molecule 2 | from mstk.topology.topology import Topology 3 | 4 | 5 | class Smi(): 6 | ''' 7 | Generate Topology from SMI file. 8 | 9 | Each line is a molecule represented by SMILES string. 10 | Molecule name can optionally be put after the SMILES string, separated by spaces. 11 | Therefore, the moelcule name itself cannot contain space. 12 | Lines started with # will be treated as comments. 13 | The positions will be generated using RDKit. 14 | 15 | Parameters 16 | ---------- 17 | file : str 18 | kwargs : dict 19 | Ignored 20 | 21 | Attributes 22 | ---------- 23 | topology : Topology 24 | 25 | Examples 26 | -------- 27 | >>> smi = Smi('input.zmat') 28 | >>> topology = smi.topology 29 | ''' 30 | 31 | def __init__(self, file, **kwargs): 32 | self.topology = Topology() 33 | self._parse(file) 34 | 35 | def _parse(self, file): 36 | molecules = [] 37 | with open(file) as f: 38 | for line in f: 39 | line = line.strip() 40 | if line == '' or line.startswith('#'): 41 | continue 42 | mol = Molecule.from_smiles(line) 43 | molecules.append(mol) 44 | 45 | self.topology.update_molecules(molecules) 46 | 47 | 48 | Topology.register_format('.smi', Smi) 49 | -------------------------------------------------------------------------------- /mstk/topology/io/xyz.py: -------------------------------------------------------------------------------- 1 | from mstk.topology.atom import Atom 2 | from mstk.topology.molecule import Molecule 3 | from mstk.topology.topology import Topology 4 | from mstk.chem.element import Element 5 | 6 | 7 | class XyzTopology(): 8 | ''' 9 | Generate Topology from XYZ file. 10 | 11 | XYZ format only records the type (or atomic symbol) and position of atoms. 12 | There is no real topology, so all atoms are assumed to be in the same molecule. 13 | The first column is treated as atom type instead of name or symbol. 14 | 15 | Parameters 16 | ---------- 17 | file : str 18 | kwargs : dict 19 | Ignored 20 | 21 | Attributes 22 | ---------- 23 | topology : Topology 24 | 25 | Examples 26 | -------- 27 | >>> xyz = XyzTopology('input.xyz') 28 | >>> topology = xyz.topology 29 | 30 | >>> XyzTopology.save_to(topology, 'output.xyz') 31 | ''' 32 | 33 | def __init__(self, file, **kwargs): 34 | self.topology = Topology() 35 | self._parse(file) 36 | 37 | def _parse(self, file): 38 | with open(file) as f: 39 | n_atom = int(f.readline().strip()) 40 | mol = Molecule() 41 | try: 42 | mol.name = f.readline().strip().split()[0] 43 | except: 44 | pass 45 | for i in range(n_atom): 46 | words = f.readline().split() 47 | atom = Atom() 48 | atom.type = words[0] 49 | element = Element.guess_from_atom_type(atom.type) 50 | atom.symbol = element.symbol 51 | atom.mass = element.mass 52 | atom.name = atom.symbol + str(mol.n_atom + 1) 53 | atom.position = tuple(map(lambda x: float(x) / 10, words[1:4])) 54 | mol.add_atom(atom) 55 | 56 | self.topology.update_molecules([mol]) 57 | 58 | @staticmethod 59 | def save_to(top, file, **kwargs): 60 | ''' 61 | Save topology into a XYZ file. 62 | 63 | The name of the first molecule in the topology will be written as the remark of the topology. 64 | Only atom type and positions are written. 65 | If atom type is empty, use atom symbol instead. 66 | 67 | Parameters 68 | ---------- 69 | top : Topology 70 | file : str 71 | ''' 72 | if not top.has_position: 73 | raise Exception('Position is required for writing XYZ file') 74 | 75 | string = '%i\n' % top.n_atom 76 | string += '%s\n' % top.molecules[0].name 77 | for atom in top.atoms: 78 | pos = atom.position * 10 79 | string += '%-8s %11.5f %11.5f %11.5f\n' % ( 80 | atom.type or atom.symbol, pos[0], pos[1], pos[2]) 81 | with open(file, 'wb')as f: 82 | f.write(string.encode()) 83 | 84 | 85 | Topology.register_format('.xyz', XyzTopology) 86 | -------------------------------------------------------------------------------- /mstk/topology/residue.py: -------------------------------------------------------------------------------- 1 | class Residue: 2 | ''' 3 | A residue is a group of consecutive atoms, e.g. the amino acid in protein, the repeating unit in polymer. 4 | 5 | It can be considered as a special connectivity without energy contribution. 6 | It is mainly for the convenience of visualization of polymers. 7 | Small molecule usually contains one single residue. 8 | ''' 9 | 10 | def __init__(self, name='UNK'): 11 | self.id = -1 12 | self.id_in_mol = -1 13 | self.name = name 14 | self._atoms = [] 15 | 16 | def __repr__(self): 17 | return f'' 18 | 19 | @property 20 | def n_atom(self): 21 | return len(self._atoms) 22 | 23 | @property 24 | def atoms(self): 25 | return self._atoms 26 | 27 | def _add_atom(self, atom): 28 | atom._residue = self 29 | self._atoms.append(atom) 30 | 31 | def _remove_atom(self, atom): 32 | atom._residue = None 33 | self._atoms.remove(atom) 34 | -------------------------------------------------------------------------------- /mstk/trajectory/__init__.py: -------------------------------------------------------------------------------- 1 | from .frame import Frame 2 | from .trajectory import Trajectory 3 | from .handler import TrjHandler 4 | from .io.gro import Gro 5 | from .io.dcd import Dcd 6 | from .io.lammps import LammpsTrj 7 | from .io.xtc import Xtc 8 | from .io.xyz import Xyz 9 | from .io.combined_trj import CombinedTrj 10 | -------------------------------------------------------------------------------- /mstk/trajectory/handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | from io import IOBase 3 | 4 | 5 | class TrjHandler(): 6 | ''' 7 | Base class of trajectory handlers for various trajectory formats. 8 | 9 | The methods :func:`get_info`, :func:`read_frame` and :func:`write_frame` should be implemented by subclasses. 10 | The method :func:`close` should also be overriden by subclasses if more works are required more than close the file. 11 | ''' 12 | 13 | _klass_map = {} 14 | 15 | def __init__(self): 16 | self._file = IOBase() 17 | self.n_atom = -1 18 | self.n_frame = -1 19 | 20 | @staticmethod 21 | def register_format(extension, Handler): 22 | ''' 23 | Register a handler class for a trajectory format based on the extension name 24 | 25 | Parameters 26 | ---------- 27 | extension : str 28 | The extension should start with '.'. 29 | The upper of lower cases are not distinguished. 30 | Handler : subclass of TrjHandler 31 | ''' 32 | TrjHandler._klass_map[extension.lower()] = Handler 33 | 34 | @staticmethod 35 | def get_handler_for_file(file): 36 | ''' 37 | Get the appropriate handler class for a trajectory file. 38 | 39 | If a list of string is provided, the CombinedTrj will be returned. 40 | Otherwise, the handler class will be determined from the extension name of the file. 41 | 42 | Parameters 43 | ---------- 44 | file : str or list of str 45 | ''' 46 | from . import CombinedTrj 47 | 48 | if isinstance(file, list): 49 | Handler = CombinedTrj 50 | elif isinstance(file, str): 51 | try: 52 | Handler = TrjHandler._klass_map[os.path.splitext(file)[-1].lower()] 53 | except: 54 | raise Exception('Unknown extension for trajectory file: %s' % file) 55 | else: 56 | raise Exception('Only string and list of string are accepted for trajectory file') 57 | 58 | return Handler 59 | 60 | def get_info(self): 61 | ''' 62 | Get the number of atoms and frames in the trajectory. 63 | 64 | Also record the offset of lines and frames, so that we can read arbitrary frame later. 65 | It assumes all frames have the same number of atoms. 66 | 67 | Returns 68 | ------- 69 | n_atom : int 70 | n_frame : int 71 | ''' 72 | raise NotImplementedError('Method not implemented') 73 | 74 | def read_frame(self, i_frame, frame): 75 | ''' 76 | Read a single frame. 77 | 78 | Parameters 79 | ---------- 80 | i_frame : int 81 | The index of the frame in the trajectory 82 | frame : Frame 83 | The information read from the trajectory will be written into this Frame 84 | ''' 85 | raise NotImplementedError('Method not implemented') 86 | 87 | def write_frame(self, frame, **kwargs): 88 | ''' 89 | Write a frame into the trajectory file opened by the handler. 90 | 91 | The handler should be initialized in mode 'w' or 'a'. 92 | 93 | Parameters 94 | ---------- 95 | frame : Frame 96 | kwargs : dict 97 | ''' 98 | raise NotImplementedError('Method not implemented') 99 | 100 | def close(self): 101 | ''' 102 | Close the handler. 103 | ''' 104 | self._file.close() 105 | -------------------------------------------------------------------------------- /mstk/trajectory/io/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/mstk/trajectory/io/__init__.py -------------------------------------------------------------------------------- /mstk/trajectory/io/combined_trj.py: -------------------------------------------------------------------------------- 1 | from mstk.trajectory.handler import TrjHandler 2 | 3 | 4 | class CombinedTrj(TrjHandler): 5 | ''' 6 | Read several trajectory files at the same time. 7 | 8 | It is useful for processing truncated trajectories after restarting simulation. 9 | All trajectory files should contain the same number of atoms. 10 | ''' 11 | 12 | def __init__(self, files, mode='r'): 13 | if mode != 'r': 14 | raise Exception('CombinedTrj is read-only') 15 | 16 | super().__init__() 17 | 18 | self._handlers = [] 19 | for file in files: 20 | Handler = TrjHandler.get_handler_for_file(file) 21 | self._handlers.append(Handler(file, mode='r')) 22 | 23 | def get_info(self): 24 | self._i_frame_handler = [] 25 | self._i_frame_offset = [] 26 | for handler in self._handlers: 27 | handler.get_info() 28 | self._i_frame_handler += [handler] * handler.n_frame 29 | self._i_frame_offset += list(range(handler.n_frame)) 30 | 31 | if set([handler.n_atom for handler in self._handlers]).__len__() != 1: 32 | raise Exception('All trajectories should have same number of atoms') 33 | 34 | self.n_frame = len(self._i_frame_offset) 35 | self.n_atom = self._handlers[0].n_atom 36 | return self.n_atom, self.n_frame 37 | 38 | def close(self): 39 | for handler in self._handlers: 40 | handler.close() 41 | 42 | def read_frame(self, i_frame, frame): 43 | handler = self._i_frame_handler[i_frame] 44 | i = self._i_frame_offset[i_frame] 45 | handler.read_frame(i, frame) 46 | -------------------------------------------------------------------------------- /mstk/trajectory/io/dcd.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mstk.chem.constant import * 3 | from mstk.trajectory import Frame 4 | from mstk.trajectory.handler import TrjHandler 5 | 6 | 7 | class Dcd(TrjHandler): 8 | ''' 9 | Read and write step, cell and positions from/to DCD file. 10 | 11 | Currently mstk use chemfiles to support DCD file. 12 | ''' 13 | 14 | def __init__(self, file, mode='r'): 15 | super().__init__() 16 | 17 | try: 18 | import chemfiles 19 | except: 20 | raise ImportError('Currently mstk use chemfiles to parse DCD format. Cannot import chemfiles') 21 | 22 | if mode not in ('r', 'w', 'a'): 23 | raise Exception('Invalid mode') 24 | 25 | self._dcd = chemfiles.Trajectory(file, mode, format='DCD') 26 | 27 | def close(self): 28 | try: 29 | self._dcd.close() 30 | except: 31 | pass 32 | 33 | def get_info(self): 34 | self.n_frame = self._dcd.nsteps 35 | if self.n_frame == 0: 36 | raise Exception('Empty DCD file') 37 | cf_frame = self._dcd.read() 38 | self.n_atom = len(cf_frame.atoms) 39 | 40 | return self.n_atom, self.n_frame 41 | 42 | def read_frame(self, i_frame, frame): 43 | cf_frame = self._dcd.read_step(i_frame) 44 | cf_cell = cf_frame.cell 45 | lengths = [i / 10 for i in cf_cell.lengths] 46 | angles = [i * DEG2RAD for i in cf_cell.angles] 47 | frame.positions = cf_frame.positions.astype(float) / 10 48 | frame.cell.set_box([lengths, angles]) 49 | frame.step = frame.step 50 | 51 | def write_frame(self, frame, subset=None, **kwargs): 52 | ''' 53 | Write a frame into the opened DCD file 54 | 55 | Parameters 56 | ---------- 57 | frame : Frame 58 | subset : list of int, optional 59 | kwargs : dict 60 | Ignored 61 | ''' 62 | import chemfiles 63 | 64 | if subset is None: 65 | positions = frame.positions 66 | else: 67 | positions = frame.positions[subset] 68 | 69 | cf_frame = chemfiles.Frame() 70 | cf_frame.resize(len(positions)) 71 | cf_frame.positions[:] = positions * 10 72 | cf_frame.cell = chemfiles.UnitCell(frame.cell.lengths * 10, frame.cell.angles * RAD2DEG) 73 | cf_frame.step = frame.step 74 | 75 | self._dcd.write(cf_frame) 76 | 77 | 78 | TrjHandler.register_format('.dcd', Dcd) 79 | -------------------------------------------------------------------------------- /mstk/trajectory/io/xtc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mstk.chem.constant import * 3 | from mstk.trajectory import Frame 4 | from mstk.trajectory.handler import TrjHandler 5 | 6 | 7 | class Xtc(TrjHandler): 8 | ''' 9 | Read and write step, cell and positions from XTC file. 10 | 11 | Simulation time is ignored. 12 | 13 | Currently mstk use chemfiles to support XTC format. 14 | ''' 15 | 16 | def __init__(self, file, mode='r'): 17 | super().__init__() 18 | 19 | try: 20 | import chemfiles 21 | except: 22 | raise ImportError('Currently mstk use chemfiles to parse XTC format. Cannot import chemfiles') 23 | 24 | if mode not in ('r', 'w', 'a'): 25 | raise Exception('Invalid mode') 26 | 27 | self._xtc = chemfiles.Trajectory(file, mode, format='XTC') 28 | 29 | def close(self): 30 | try: 31 | self._xtc.close() 32 | except: 33 | pass 34 | 35 | def get_info(self): 36 | self.n_frame = self._xtc.nsteps 37 | if self.n_frame == 0: 38 | raise Exception('Empty XTC file') 39 | cf_frame = self._xtc.read() 40 | self.n_atom = len(cf_frame.atoms) 41 | 42 | return self.n_atom, self.n_frame 43 | 44 | def read_frame(self, i_frame, frame): 45 | cf_frame = self._xtc.read_step(i_frame) 46 | cf_cell = cf_frame.cell 47 | lengths = [i / 10 for i in cf_cell.lengths] 48 | angles = [i * DEG2RAD for i in cf_cell.angles] 49 | frame.positions = cf_frame.positions.astype(float) / 10 50 | frame.cell.set_box([lengths, angles]) 51 | frame.step = frame.step 52 | 53 | def write_frame(self, frame, subset=None, **kwargs): 54 | ''' 55 | Write a frame into the opened XTC file 56 | 57 | Parameters 58 | ---------- 59 | frame : Frame 60 | subset : list of int, optional 61 | kwargs : dict 62 | Ignored 63 | ''' 64 | import chemfiles 65 | 66 | if subset is None: 67 | positions = frame.positions 68 | else: 69 | positions = frame.positions[subset] 70 | 71 | cf_frame = chemfiles.Frame() 72 | cf_frame.resize(len(positions)) 73 | cf_frame.positions[:] = positions * 10 74 | cf_frame.cell = chemfiles.UnitCell(frame.cell.lengths * 10, frame.cell.angles * RAD2DEG) 75 | cf_frame.step = frame.step 76 | 77 | self._xtc.write(cf_frame) 78 | 79 | 80 | TrjHandler.register_format('.xtc', Xtc) 81 | -------------------------------------------------------------------------------- /mstk/trajectory/io/xyz.py: -------------------------------------------------------------------------------- 1 | from mstk.topology import Topology 2 | from mstk.trajectory import Frame 3 | from mstk.trajectory.handler import TrjHandler 4 | 5 | 6 | class Xyz(TrjHandler): 7 | ''' 8 | Read and write positions from XYZ file. 9 | ''' 10 | 11 | def __init__(self, file, mode='r'): 12 | super().__init__() 13 | if mode not in ('r', 'w', 'a'): 14 | raise Exception('Invalid mode') 15 | 16 | if mode == 'r': 17 | # open it in binary mode so that we can correctly seek despite of line ending 18 | self._file = open(file, 'rb') 19 | elif mode == 'a': 20 | self._file = open(file, 'ab') 21 | elif mode == 'w': 22 | self._file = open(file, 'wb') 23 | 24 | def get_info(self): 25 | try: 26 | self.n_atom = int(self._file.readline()) 27 | except: 28 | raise Exception('Invalid XYZ file') 29 | self._file.seek(0) 30 | 31 | # read in the file once and build a list of line offsets 32 | # the last element is the length of whole file 33 | self._line_offset = [0] 34 | offset = 0 35 | for line in self._file: 36 | offset += len(line) 37 | self._line_offset.append(offset) 38 | self._file.seek(0) 39 | 40 | # build a list of frame offsets 41 | self.n_frame = len(self._line_offset) // (2 + self.n_atom) 42 | self._frame_offset = [] 43 | for i in range(self.n_frame + 1): 44 | line_start = (2 + self.n_atom) * i 45 | self._frame_offset.append(self._line_offset[line_start]) 46 | 47 | return self.n_atom, self.n_frame 48 | 49 | def read_frame(self, i_frame, frame): 50 | # skip to frame i and read only this frame 51 | self._file.seek(self._frame_offset[i_frame]) 52 | lines = self._file.read(self._frame_offset[i_frame + 1] - self._frame_offset[i_frame]) \ 53 | .decode().splitlines() 54 | for i in range(self.n_atom): 55 | words = lines[i + 2].split() 56 | x = float(words[1]) / 10 # convert A to nm 57 | y = float(words[2]) / 10 58 | z = float(words[3]) / 10 59 | frame.positions[i][:] = x, y, z 60 | 61 | def write_frame(self, frame, topology, subset=None, **kwargs): 62 | ''' 63 | Write a frame into the opened XYZ file 64 | 65 | Parameters 66 | ---------- 67 | frame : Frame 68 | topology : Topology 69 | subset : list of int 70 | kwargs : dict 71 | Ignored 72 | ''' 73 | if subset is None: 74 | subset = list(range(len(frame.positions))) 75 | 76 | string = '%i\n' % len(subset) 77 | string += 'Created by mstk: step= %i, t= %f ps\n' % (frame.step, frame.time) 78 | 79 | for ii, id in enumerate(subset): 80 | atom = topology.atoms[id] 81 | pos = frame.positions[id] * 10 # convert from nm to A 82 | # atom.symbol is more friendly than atom.type for visualizing trajectory 83 | string += '%-8s %10.5f %10.5f %10.5f\n' % (atom.symbol, pos[0], pos[1], pos[2]) 84 | 85 | self._file.write(string.encode()) 86 | self._file.flush() 87 | 88 | TrjHandler.register_format('.xyz', Xyz) 89 | -------------------------------------------------------------------------------- /mstk/wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | from .packmol import Packmol 2 | from .gmx import GMX 3 | from .gauss import Gauss -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=40.8.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "mstk" 7 | version = "0.3.27" 8 | authors = [{ name = "Zheng Gong", email = "z.gong@outlook.com" }] 9 | description = "Molecular simulation toolkit" 10 | readme = "README.md" 11 | license = { file = "LICENSE" } # ensure you have a LICENSE file 12 | classifiers = [ 13 | "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", 14 | "Operating System :: OS Independent" 15 | ] 16 | urls = { "Homepage" = "https://github.com/z-gong/mstk" } 17 | 18 | [project.scripts] 19 | mstk = "mstk.cli:main" 20 | 21 | [tool.setuptools.package-data] 22 | mstk = ["data/forcefield/*"] 23 | -------------------------------------------------------------------------------- /tests/analyzer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/analyzer/__init__.py -------------------------------------------------------------------------------- /tests/analyzer/test_ewald.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | from mstk.analyzer.ewald import EwaldSum 5 | 6 | import os 7 | 8 | cwd = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def test_energy(): 12 | ewald = EwaldSum.create_test(100, seed=0, cutoff=1.2) 13 | assert pytest.approx(ewald.alpha, abs=1E-4) == 2.1902 14 | assert ewald.kmax_x == 7 15 | assert ewald.kmax_y == 9 16 | assert ewald.kmax_z == 11 17 | 18 | energy, forces = ewald.calc_energy_forces(ewald=True) 19 | assert pytest.approx(energy, rel=1E-6) == -1089.0515 20 | assert pytest.approx(forces[-1], rel=1E-4) == [-1.6694e+02, 5.3097e+02, -1.4420e+02] 21 | 22 | energy, forces = ewald.calc_energy_forces(ewald=False) 23 | assert pytest.approx(energy, rel=1E-6) == -1011.3025 24 | assert pytest.approx(forces[-1], rel=1E-4) == [-1.5860e+02, 4.9182e+02, -7.5005e+01] 25 | 26 | ewald = EwaldSum.create_test(100, seed=0, cutoff=1.4) 27 | assert pytest.approx(ewald.alpha, abs=1E-4) == 1.8773 28 | assert ewald.kmax_x == 5 29 | assert ewald.kmax_y == 7 30 | assert ewald.kmax_z == 9 31 | 32 | energy, forces = ewald.calc_energy_forces(ewald=True) 33 | assert pytest.approx(energy, rel=1E-6) == -1089.1090 34 | assert pytest.approx(forces[-1], rel=1E-4) == [-1.6675e+02, 5.3089e+02, -1.4421e+02] 35 | 36 | energy, forces = ewald.calc_energy_forces(ewald=False) 37 | assert pytest.approx(energy, rel=1E-6) == -1011.3025 38 | assert pytest.approx(forces[-1], rel=1E-4) == [-1.5860e+02, 4.9182e+02, -7.5005e+01] 39 | -------------------------------------------------------------------------------- /tests/chem/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/chem/__init__.py -------------------------------------------------------------------------------- /tests/chem/test_formula.py: -------------------------------------------------------------------------------- 1 | from mstk.chem.formula import Formula 2 | 3 | 4 | def test_formula(): 5 | formula = Formula('C10(H2C1Cl3)2') 6 | assert formula.atoms == {'C' : 12, 7 | 'H' : 4, 8 | 'Cl': 6 9 | } 10 | assert formula.to_str() == 'C12H4Cl6' 11 | 12 | formula = Formula('UukA2(C2(H1Cl3H1)2)1O') 13 | assert formula.atoms == {'Uuk': 1, 14 | 'A' : 2, 15 | 'C' : 2, 16 | 'H' : 4, 17 | 'Cl' : 6, 18 | 'O' : 1 19 | } 20 | assert formula.to_str() == 'C2H4A2Cl6OUuk' 21 | 22 | 23 | def test_formula_charge(): 24 | formula = Formula('C10(O+2)2') 25 | assert formula.atoms == {'C': 10, 26 | 'O': 2, 27 | } 28 | assert formula.to_str() == 'C10O2+4' 29 | 30 | formula = Formula('C10N-O2') 31 | assert formula.atoms == {'C': 10, 32 | 'O': 2, 33 | 'N': 1 34 | } 35 | assert formula.to_str() == 'C10NO2-' 36 | -------------------------------------------------------------------------------- /tests/forcefield/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/forcefield/__init__.py -------------------------------------------------------------------------------- /tests/forcefield/convert-gaff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import math 4 | from mstk.forcefield import ForceField 5 | from mstk.forcefield.ffterm import * 6 | 7 | 8 | def main(): 9 | with open('gaff.dat') as f: 10 | lines = f.read().splitlines() 11 | lines_atom = lines[1:84] 12 | lines_bond = lines[86:1014] 13 | lines_angle = lines[1015:6330] 14 | lines_dihedral = lines[6331:7075] 15 | lines_improper = lines[7076:7114] 16 | lines_vdw = lines[7119:7202] 17 | 18 | ff = ForceField() 19 | ff.vdw_cutoff = 1.0 20 | ff.vdw_long_range = ff.VDW_LONGRANGE_CORRECT 21 | ff.lj_mixing_rule = ff.LJ_MIXING_LB 22 | ff.scale_14_vdw = 0.5 23 | ff.scale_14_coulomb = 0.8333 24 | 25 | for line in lines_atom: 26 | atype, str_mass = line.split()[:2] 27 | aterm = AtomType(atype, mass=float(str_mass)) 28 | ff.add_term(aterm) 29 | for line in lines_bond: 30 | atypes = line[:2].strip(), line[3:5].strip() 31 | str_k, str_length = line[5:].split()[:2] 32 | k = float(str_k) * 100 * 4.184 33 | length = float(str_length) / 10 34 | fixed = any(typ.startswith('h') for typ in atypes) 35 | bterm = HarmonicBondTerm(*atypes, length, k, fixed=fixed) 36 | ff.add_term(bterm) 37 | for line in lines_angle: 38 | atypes = line[:2].strip(), line[3:5].strip(), line[6:8].strip() 39 | str_k, str_theta = line[8:].split()[:2] 40 | k = float(str_k) * 4.184 41 | theta = float(str_theta) * DEG2RAD 42 | aterm = HarmonicAngleTerm(*atypes, theta, k) 43 | ff.add_term(aterm) 44 | for line in lines_dihedral: 45 | type1, type2, type3, type4 = line[:2].strip(), line[3:5].strip(), line[6:8].strip(), line[9:11].strip() 46 | str_divider, str_k, str_phi, str_n = line[11:].split()[:4] 47 | k = float(str_k) / int(str_divider) * 4.184 48 | phi = float(str_phi) * DEG2RAD 49 | n = abs(int(float(str_n))) 50 | dterm = PeriodicDihedralTerm(type1.replace('X', '*'), type2, type3, type4.replace('X', '*')) 51 | if dterm.name in ff.dihedral_terms and k == 0: 52 | continue 53 | dterm = ff.dihedral_terms.get(dterm.name, dterm) 54 | dterm.add_phase(phi, k, n) 55 | ff.add_term(dterm, replace=True) 56 | for line in lines_improper: 57 | type1, type2, type3, type4 = line[:2].strip(), line[3:5].strip(), line[6:8].strip(), line[9:11].strip() 58 | str_k = line[11:].split()[0] 59 | k = float(str_k) * 4.184 60 | iterm = OplsImproperTerm(type3, type1.replace('X', '*'), type2.replace('X', '*'), type4.replace('X', '*'), k) 61 | ff.add_term(iterm, replace=True) 62 | for line in lines_vdw: 63 | atype, str_r0_2, str_epsilon = line.split()[:3] 64 | sigma = float(str_r0_2) * 2 / 2 ** (1 / 6) / 10 65 | epsilon = float(str_epsilon) * 4.184 66 | ljterm = LJ126Term(atype, atype, epsilon, sigma) 67 | ff.add_term(ljterm) 68 | 69 | for k, dterm in ff.dihedral_terms.items(): 70 | try: 71 | ff.dihedral_terms[k] = dterm.to_opls_term() 72 | except: 73 | pass 74 | 75 | ff.write('gaff.zff') 76 | 77 | 78 | if __name__ == '__main__': 79 | main() 80 | -------------------------------------------------------------------------------- /tests/forcefield/convert-spica.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | from mstk.forcefield import * 5 | 6 | ff = ForceField() 7 | ff.vdw_cutoff = 1.5 8 | ff.vdw_long_range = ForceField.VDW_LONGRANGE_SHIFT 9 | ff.lj_mixing_rule = ForceField.LJ_MIXING_NONE 10 | ff.scale_14_vdw = 1.0 11 | ff.scale_14_coulomb = 1.0 12 | 13 | with open('spica_top.json') as f: 14 | j = json.load(f) 15 | for k, v in j['topo'].items(): 16 | for i, typ in enumerate(v['type']): 17 | if typ in ff.atom_types: 18 | continue 19 | atype = AtomType(typ) 20 | atype.charge = round(v['charge'][i] * math.sqrt(80), 4) 21 | atype.mass = round(v['mass'][i], 4) 22 | ff.add_term(atype) 23 | ### fix charge of COO bead 24 | if typ == 'COO': 25 | atype.charge = -1.0 26 | ########################## 27 | 28 | #### add SO4 bead 29 | aterm = AtomType('SO4') 30 | aterm.charge = -1.0 31 | aterm.mass = 96.062 32 | ff.add_term(aterm) 33 | ################# 34 | 35 | with open('spica_par.json') as f: 36 | j = json.load(f) 37 | for par in j['params']: 38 | if par['param'] == 'bond': 39 | if par['potential'] != 'harmonic': 40 | raise Exception('Unknown potential form: %s' % par['potential']) 41 | types = par['types'] 42 | k = par['k'] * 4.184 * 100 43 | length = par['r0'] / 10 44 | bterm = HarmonicBondTerm(*types, length, k) 45 | ff.add_term(bterm) 46 | elif par['param'] == 'angle': 47 | types = par['types'] 48 | k = par['k'] * 4.184 49 | theta = par['theta0'] * DEG2RAD 50 | if par['potential'] == 'harmonic': 51 | aterm = HarmonicAngleTerm(*types, theta, k) 52 | elif par['potential'] == 'sdk': 53 | aterm = SDKAngleTerm(*types, theta, k) 54 | else: 55 | raise Exception('Unknown potential form: %s' % par['potential']) 56 | ff.add_term(aterm) 57 | elif par['param'] == 'dihedral': 58 | if par['potential'] != 'charmm': 59 | raise Exception('Unknown potential form: %s' % par['potential']) 60 | types = par['types'] 61 | k = par['k'] * 4.184 62 | n = par['n'] 63 | phi = par['d'] * DEG2RAD 64 | dterm = PeriodicDihedralTerm(*types) 65 | dterm.add_phase(phi, k, n) 66 | ff.add_term(dterm) 67 | elif par['param'] == 'pair': 68 | types = par['types'] 69 | epsilon = par['epsilon'] * 4.184 70 | sigma = par['sigma'] / 10 71 | if par['potential'] == 'lj9_6': 72 | vdw = MieTerm(*types, epsilon, sigma, 9, 6) 73 | elif par['potential'] == 'lj12_4': 74 | vdw = MieTerm(*types, epsilon, sigma, 12, 4) 75 | else: 76 | raise Exception('Unknown potential form: %s' % par['potential']) 77 | ff.add_term(vdw) 78 | 79 | ff.comments.append('Converted from spica_top.json and spica_prm.json in spica_v1.0.tar.gz') 80 | ff.comments.append('Downloaded from https://www.spica-ff.org/download.html') 81 | ff.write('SPICA_v1.0.zff') 82 | -------------------------------------------------------------------------------- /tests/forcefield/files/CLP-define.smt: -------------------------------------------------------------------------------- 1 | TypeDefinition 2 | ## basic 3 | H_1 [H] 4 | C_2 [#6X2] 5 | C_3 [#6X3] 6 | C_4 [#6X4] 7 | N_1 [#7X1] 8 | N_2 [#7X2] 9 | N_3 [#7X3] 10 | O_1 [#8X1] 11 | O_2 [#8X2] 12 | F_1 [#9X1] 13 | S_4 [#16X4] 14 | 15 | ## alkane 16 | HC [H][CX4] 17 | CT [CX4;H3] 18 | CS [CX4;H2] 19 | 20 | ## imidazolium 21 | NA n1cncc1 1 22 | CR c1nccn1 1 23 | CW c1cncn1 1 24 | HCR [H]c1nccn1 1 25 | HCW [H]c1cncn1 1 26 | C1 [CX4;H2,H3]n1cncc1 1 27 | H1 [H][CX4]n1cncc1 1 28 | C2 [CX4;H2][CX4]n1cncc1 1 29 | CE [CX4;H3][CX4]n1cncc1 1 30 | 31 | ## benzene 32 | CA c1ccccc1 33 | HA [H]c1ccccc1 34 | 35 | ## ethylbenzene-imidazolium 36 | CAT c1(C)ccccc1 37 | CAO [cH]1c(C)cccc1 38 | CAM [cH]1cc(C)ccc1 39 | CAP [cH]1ccc(C)cc1 40 | HAT [H][$(c1c(C)cccc1),$(c1cc(C)ccc1),$(c1ccc(C)cc1)] 41 | C_4H2_CA Cc1ccccc1 42 | HT [H][CX4]c1ccccc1 43 | C_4H2_NA [CH2](n1cncc1)Cc1ccccc1 1 44 | 45 | ## dicyanoimide 46 | N3A [N-]C#N -1 47 | CZA C([N-])#N -1 48 | NZA N#C[N-] -1 49 | 50 | ## FSI TFSI 51 | NBT [N-](S(=O)(=O))S(=O)(=O) -1 52 | SBT S(=O)(=O)[N-] -1 53 | OBT O=S(=O)[N-] -1 54 | FSI FS(=O)(=O)[N-] -1 55 | CBT C(F)(F)(F)S(=O)(=O)[N-] -1 56 | F1 F[CX4] 57 | 58 | HierarchicalTree 59 | H_1 60 | HCR 61 | HCW 62 | HC 63 | H1 64 | HT 65 | HA 66 | HAT 67 | C_2 68 | CZA 69 | C_3 70 | CR 71 | CW 72 | CA 73 | CAT 74 | CAO 75 | CAM 76 | CAP 77 | C_4 78 | C1 79 | C_4H2_NA 80 | CT 81 | CE 82 | CS 83 | C_4H2_CA 84 | C2 85 | CBT 86 | N_1 87 | NZA 88 | N_2 89 | N3A 90 | NBT 91 | N_3 92 | NA 93 | O_1 94 | OBT 95 | O_2 96 | F_1 97 | FSI 98 | F1 99 | S_4 100 | SBT 101 | -------------------------------------------------------------------------------- /tests/forcefield/files/CLPol-alpha.ff: -------------------------------------------------------------------------------- 1 | # ildrude_heid2018.ff 2 | # units: kJ/mol, A, deg 3 | # kforce is in the form k/2 r_D^2 4 | 5 | POLAR 6 | # type m_D/u q_D/e k_D alpha/A3 a_thole n_H 7 | # The line for H* means all hydrogen atoms share the same polarizability, 8 | # and it will be merged into connected heavy atoms. 9 | # Other lines stars with H (like HC, H1 or whatever) should not be defined. 10 | H* 0.0 0.0 0.0 0.323 0.0 11 | # imidazolium PCCP 20(2018)10992 12 | CR 0.4 -1.0 4184.0 1.122 2.6 13 | CW 0.4 -1.0 4184.0 1.122 2.6 14 | NA 0.4 -1.0 4184.0 1.208 2.6 15 | C1 0.4 -1.0 4184.0 1.016 2.6 16 | C1A 0.4 -1.0 4184.0 1.016 2.6 17 | C2 0.4 -1.0 4184.0 1.016 2.6 18 | CS 0.4 -1.0 4184.0 1.016 2.6 19 | CT 0.4 -1.0 4184.0 1.016 2.6 20 | CE 0.4 -1.0 4184.0 1.016 2.6 21 | # pyrrolidinium PCCP 20(2018)10992 22 | N4 0.4 -1.0 4184.0 1.208 2.6 23 | # ntf2, fsi PCCP PCCP 20(2018)10992 24 | CBT 0.4 -1.0 4184.0 1.016 2.6 25 | NBT 0.4 -1.0 4184.0 1.698 2.6 26 | OBT 0.4 -1.0 4184.0 1.144 2.6 27 | SBT 0.4 -1.0 4184.0 1.553 2.6 28 | F1 0.4 -1.0 4184.0 0.625 2.6 29 | FSI 0.4 -1.0 4184.0 0.625 2.6 30 | # dca PCCP PCCP 20(2018)10992 31 | N3A 0.4 -1.0 4184.0 1.698 2.6 32 | CZA 0.4 -1.0 4184.0 1.587 2.6 33 | NZA 0.4 -1.0 4184.0 1.698 2.6 34 | # BF4 PCCP PCCP 20(2018)10992 35 | B 0.4 -1.0 4184.0 0.578 2.6 36 | FB 0.4 -1.0 4184.0 0.625 2.6 37 | # triflate PCCP PCCP 20(2018)10992 38 | OTF 0.4 -1.0 4184.0 1.144 2.6 39 | # OAc PCCP 20(2018)10992 40 | O2 0.4 -1.0 4184.0 1.144 2.6 41 | CO2 0.4 -1.0 4184.0 1.432 2.6 42 | CTA 0.4 -1.0 4184.0 1.016 2.6 43 | # cholinium PCCP 20(2018)10992 44 | COL 0.4 -1.0 4184.0 1.016 2.6 45 | OH 0.4 -1.0 4184.0 1.144 2.6 46 | # chloride Chem. Rev. 119(2019) 7940 47 | Cl 0.4 -1.0 4184.0 3.4 2.6 48 | # sulfonate PCCP PCCP 20(2018)10992 49 | CS4 0.4 -1.0 4184.0 1.016 2.6 50 | OC4 0.4 -1.0 4184.0 1.144 2.6 51 | SO 0.4 -1.0 4184.0 1.553 2.6 52 | OS4 0.4 -1.0 4184.0 1.144 2.6 53 | # MoS2 54 | MoS 0.8 -1.0 4184.0 6.5 2.6 55 | SMo 0.8 -1.0 4184.0 3.39 2.6 56 | -------------------------------------------------------------------------------- /tests/forcefield/files/SPCE.ppf: -------------------------------------------------------------------------------- 1 | #DFF:EQT 2 | #AAT : NONB ATC BINC BOND A/C A/S T/C T/S O/C O/S 3 | h_1o : h_1o h_1o h_1o h_1 h_1 h_1 h_1 h_1 h_1 h_1 4 | o_2w : o_2w o_2w o_2w o_2w o_2w o_2w o_2w o_2 o_2w o_2 5 | #DFF:PPF 6 | #PROTOCOL = AMBER 7 | #TYPINGRULE = 8 | ATYPE: h_1o : 1.00000*, 1.00790* 9 | ATYPE: o_2w : 8.00000*, 15.99940* 10 | ATC: h_1o : 0.00000* 11 | ATC: o_2w : 0.00000* 12 | BINC: h_1o , o_2w : 0.42380* 13 | N12_6: h_1o : 0.00010*, 0.00010* 14 | N12_6: o_2w : 3.55320*, 0.15530* 15 | N12_6: s_2 : 3.78000*, 0.44800* 16 | N12_6: s_2h : 3.97500*, 0.43000* 17 | BHARM: h_1 , o_2w : 1.00000*, 450.00000* 18 | AHARM: h_1 , o_2w , h_1 : 109.47000*, 55.00000* 19 | P12_6: c_4h3 , o_2w : 3.70576*, 0.11537* 20 | P12_6: o_2 , o_2w : 3.44671*, 0.22936* 21 | -------------------------------------------------------------------------------- /tests/forcefield/files/SWM4-NDP.zff: -------------------------------------------------------------------------------- 1 | Setting vdw_cutoff 1.2 2 | Setting vdw_long_range correct 3 | Setting lj_mixing_rule lorentz-berthelot 4 | Setting scale_14_vdw 0.5 5 | Setting scale_14_coulomb 0.8333333333333334 6 | 7 | #AtomType name mass charge eqt_bci eqt_vdw eqt_bond eqt_ang_c eqt_ang_s eqt_dih_c eqt_dih_s eqt_imp_c eqt_imp_s eqt_polar 8 | AtomType OW 15.9994 0.0000 OW OW OW OW OW OW OW OW OW OW 9 | AtomType HW 1.0079 0.55733 HW HW HW HW HW HW HW HW HW HW 10 | AtomType VS_OW 15.9994 -1.11466 VS_OW VS_OW VS_OW VS_OW VS_OW VS_OW VS_OW VS_OW VS_OW VS_OW 11 | #TIP4PSite type type_O type_H d 12 | TIP4PSite VS_OW OW HW 0.024034 13 | #LJ126 type1 type2 epsilon sigma 14 | LJ126 OW OW 0.882573 0.318395 15 | LJ126 HW HW 0.0000 0.0000 16 | LJ126 VS_OW VS_OW 0.0000 0.0000 17 | #HarmonicBond type1 type2 length k fixed 18 | HarmonicBond HW OW 0.09572 251208.0 True 19 | #HarmonicAngle type1 type2 type3 theta k fixed 20 | HarmonicAngle HW OW HW 104.5200 314.0100 True 21 | #DrudePolar type alpha thole k mass merge_alpha_H 22 | DrudePolar OW 9.782e-04 2.6000 209200.0 0.4000 0.0000 23 | -------------------------------------------------------------------------------- /tests/forcefield/files/TIP4P.zff: -------------------------------------------------------------------------------- 1 | Setting vdw_cutoff 1.2 2 | Setting vdw_long_range correct 3 | Setting lj_mixing_rule lorentz-berthelot 4 | Setting scale_14_vdw 0.5 5 | Setting scale_14_coulomb 0.8333333333333334 6 | 7 | #AtomType name mass charge eqt_bci eqt_vdw eqt_bond eqt_ang_c eqt_ang_s eqt_dih_c eqt_dih_s eqt_imp_c eqt_imp_s eqt_polar 8 | AtomType OW 15.9994 0.0000 OW OW OW OW OW OW OW OW OW OW 9 | AtomType HW 1.0079 0.5200 HW HW HW HW HW HW HW HW HW HW 10 | AtomType MW 15.9994 -1.0400 MW MW MW MW MW MW MW MW MW MW 11 | #TIP4PSite type type_O type_H d 12 | TIP4PSite MW OW HW 0.0150 13 | #LJ126 type1 type2 epsilon sigma 14 | LJ126 OW OW 0.648520 0.315365 15 | LJ126 HW HW 0.0000 0.0000 16 | LJ126 MW MW 0.0000 0.0000 17 | #HarmonicBond type1 type2 length k fixed 18 | HarmonicBond HW OW 0.09572 251208.0 True 19 | #HarmonicAngle type1 type2 type3 theta k fixed 20 | HarmonicAngle HW OW HW 104.5200 314.0100 True 21 | -------------------------------------------------------------------------------- /tests/forcefield/files/baselines/out-SPCE.zff: -------------------------------------------------------------------------------- 1 | Setting vdw_cutoff 1.2 2 | Setting vdw_long_range correct 3 | Setting lj_mixing_rule lorentz-berthelot 4 | Setting scale_14_vdw 0.5 5 | Setting scale_14_coulomb 0.8333333333333334 6 | 7 | #AtomType name mass charge eqt_bci eqt_vdw eqt_bond eqt_ang_c eqt_ang_s eqt_dih_c eqt_dih_s eqt_imp_c eqt_imp_s eqt_polar 8 | AtomType h_1o 1.0079 0.0000 h_1o h_1o h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1o 9 | AtomType o_2w 15.9994 0.0000 o_2w o_2w o_2w o_2w o_2w o_2w o_2 o_2w o_2 o_2w 10 | #ChargeIncrement type1 type2 value 11 | ChargeIncrement h_1o o_2w 0.4238 12 | #LJ126 type1 type2 epsilon sigma 13 | LJ126 h_1o h_1o 4.2e-04 8.9e-06 14 | LJ126 o_2w o_2w 0.6498 0.3166 15 | LJ126 s_2 s_2 1.8744 0.3368 16 | LJ126 s_2h s_2h 1.7991 0.3541 17 | LJ126 c_4h3 o_2w 0.4827 0.3301 18 | LJ126 o_2 o_2w 0.9596 0.3071 19 | #HarmonicBond type1 type2 length k fixed 20 | HarmonicBond h_1 o_2w 0.1000 188280.0 True 21 | #HarmonicAngle type1 type2 type3 theta k fixed 22 | HarmonicAngle h_1 o_2w h_1 109.4700 230.1200 True 23 | -------------------------------------------------------------------------------- /tests/forcefield/test_padua.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | from mstk.chem.constant import * 5 | from mstk.forcefield import ForceField, Padua, PaduaLJScaler 6 | 7 | import os 8 | 9 | cwd = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | def test_read(): 13 | ff = ForceField.open(cwd + '/files/CLP.ff', cwd + '/files/CLPol-alpha.ff') 14 | br = ff.atom_types['Br'] 15 | assert br.mass == 79.904 16 | assert br.charge == -1.0 17 | 18 | cap = ff.atom_types['CAP'] 19 | ca = ff.atom_types['CA'] 20 | assert cap.eqt_vdw == 'CAP' 21 | assert cap.eqt_bond == 'CA' 22 | assert cap.eqt_imp_s == ca.name 23 | assert ca.eqt_vdw == ca.name 24 | assert ca.eqt_ang_c == ca.name 25 | assert ca.eqt_dih_c == 'CA' 26 | 27 | vdw = ff.vdw_terms['Br,Br'] 28 | assert vdw.epsilon == 0.86 29 | assert vdw.sigma == 3.97 / 10 30 | 31 | bond = ff.bond_terms['CT,CT'] 32 | assert bond.length == 1.529 / 10 33 | assert pytest.approx(bond.k, abs=1E-6) == 2242.0 / 2 * 100 34 | 35 | angle = ff.angle_terms['CT,CT,HC'] 36 | assert pytest.approx(angle.theta, abs=1e-6) == 110.7 * DEG2RAD 37 | assert angle.k == 313.8 / 2 38 | 39 | term = ff.dihedral_terms['CT,CT,CT,HC'] 40 | assert term.k1 == 0 41 | assert term.k2 == 0 42 | assert term.k3 == 1.2552 / 2 43 | assert term.k4 == 0 44 | 45 | term = ff.dihedral_terms['CT,CT,CT,CT'] 46 | assert term.k1 == 5.4392 / 2 47 | assert term.k2 == -0.2092 / 2 48 | assert term.k3 == 0.8368 / 2 49 | assert term.k4 == 0 50 | 51 | improper = ff.improper_terms['NA,CR,CT,CW'] 52 | assert improper.k == 8.368 / 2 53 | 54 | drude = ff.polar_terms['CT'] 55 | assert drude.mass == 0.4 56 | assert pytest.approx(drude.k / 100, abs=1E-6) == 4184 / 2 57 | assert pytest.approx(drude.alpha * 1000, abs=1E-6) == 1.016 58 | assert drude.thole == 2.6 59 | 60 | 61 | def test_ljscaler(): 62 | ff = ForceField.open(cwd + '/files/CLP.ff', cwd + '/files/CLPol-alpha.ff') 63 | scaler = PaduaLJScaler(cwd + '/files/CLPol-ljscale.ff') 64 | scaler.scale(ff) 65 | 66 | vdw = ff.get_vdw_term('C1', 'C1') 67 | assert pytest.approx(vdw.epsilon, abs=1E-5) == 0.047123 * 4.184 68 | assert pytest.approx(vdw.sigma, abs=1E-5) == 3.869688 / 10 / 2 ** (1 / 6) 69 | assert vdw.comments == ['eps*0.714', 'sig*0.985'] 70 | 71 | vdw = ff.get_vdw_term('CR', 'CZA') 72 | assert pytest.approx(vdw.epsilon, abs=1E-5) == 0.046627 * 4.184 73 | assert pytest.approx(vdw.sigma, abs=1E-5) == 3.784243 / 10 / 2 ** (1 / 6) 74 | assert vdw.comments == ['eps*0.686', 'sig*0.985'] 75 | 76 | -------------------------------------------------------------------------------- /tests/forcefield/test_ppf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import tempfile 4 | import filecmp 5 | import pytest 6 | from mstk.chem.constant import * 7 | from mstk.forcefield import ForceField, Ppf 8 | 9 | import os 10 | import shutil 11 | 12 | cwd = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | 15 | def test_read(): 16 | ff = ForceField.open(cwd + '/files/TEAM_IL.ppf', cwd + '/files/SPCE.ppf') 17 | assert len(ff.atom_types) == 65 18 | 19 | c_4pp = ff.atom_types.get('c_4pp') 20 | assert c_4pp.charge == 0 21 | assert c_4pp.eqt_vdw == c_4pp.name 22 | assert c_4pp.eqt_bci == c_4pp.name 23 | assert c_4pp.eqt_bond == 'c_4' 24 | assert c_4pp.eqt_ang_c == 'c_4' 25 | assert c_4pp.eqt_ang_s == 'c_4' 26 | assert c_4pp.eqt_dih_c == 'c_4' 27 | assert c_4pp.eqt_dih_s == 'c_4' 28 | assert c_4pp.eqt_imp_c == 'c_4' 29 | assert c_4pp.eqt_imp_s == 'c_4' 30 | assert c_4pp.version == '0.25' 31 | 32 | c_3_ = ff.atom_types.get('c_3-') 33 | assert c_3_.charge == -1 34 | assert c_3_.version == '0.1' 35 | 36 | o_1_t = ff.atom_types.get('o_1-t') 37 | assert o_1_t.charge == -0.3333 38 | 39 | binc = ff.bci_terms.get('c_35an2,h_1') 40 | assert binc.type1 == 'c_35an2' 41 | assert binc.type2 == 'h_1' 42 | assert binc.version == '0.16' 43 | assert binc.value == -0.22 44 | 45 | vdw = ff.vdw_terms.get('b_4-,b_4-') 46 | assert vdw.type1 == 'b_4-' 47 | assert vdw.type2 == 'b_4-' 48 | assert pytest.approx(vdw.epsilon / 4.184, abs=1E-6) == 0.05 49 | assert pytest.approx(vdw.sigma * 2 ** (1 / 6) * 10, abs=1E-6) == 3.8 50 | assert vdw.version == '0.36' 51 | 52 | vdw = ff.pairwise_vdw_terms.get('o_2,o_2w') 53 | assert vdw.type1 == 'o_2' 54 | assert vdw.type2 == 'o_2w' 55 | assert pytest.approx(vdw.epsilon / 4.184, abs=1E-6) == 0.22936 56 | assert pytest.approx(vdw.sigma * 2 ** (1 / 6) * 10, abs=1E-6) == 3.44671 57 | assert vdw.version == None 58 | 59 | bond = ff.bond_terms.get('c_2n,n_2-') 60 | assert bond.type1 == 'c_2n' 61 | assert bond.type2 == 'n_2-' 62 | assert pytest.approx(bond.length * 10, abs=1E-6) == 1.276 63 | assert pytest.approx(bond.k / 4.184 / 100, abs=1E-6) == 487.36 64 | assert bond.version == '0.3' 65 | 66 | angle = ff.angle_terms.get('c_3a,c_3a,h_1') 67 | assert angle.type1 == 'c_3a' 68 | assert angle.type2 == 'c_3a' 69 | assert angle.type3 == 'h_1' 70 | assert pytest.approx(angle.theta, abs=1E-6) == 118.916 * DEG2RAD 71 | assert pytest.approx(angle.k / 4.184, abs=1E-6) == 43.8769 72 | assert angle.version == '0.1' 73 | 74 | dihedral = ff.dihedral_terms.get('*,c_4,c_4,*') 75 | assert dihedral.type1 == '*' 76 | assert dihedral.type2 == 'c_4' 77 | assert dihedral.type3 == 'c_4' 78 | assert dihedral.type4 == '*' 79 | phase1 = dihedral.phases[0] 80 | assert pytest.approx(phase1.k / 4.184, abs=1E-6) == 0.6214 81 | assert phase1.n == 1 82 | assert phase1.phi == 0.0 83 | phase2 = dihedral.phases[1] 84 | assert pytest.approx(phase2.k / 4.184, abs=1E-6) == 0.0507 85 | assert phase2.n == 2 86 | assert pytest.approx(phase2.phi, abs=1E-6) == PI 87 | phase3 = dihedral.phases[2] 88 | assert pytest.approx(phase3.k / 4.184, abs=1E-6) == 0.0688 89 | assert phase3.n == 3 90 | assert phase3.phi == 0.0 91 | assert dihedral.version == '0.12' 92 | 93 | improper = ff.improper_terms.get('c_3o,o_1,*,*') 94 | assert pytest.approx(improper.k / 4.184, abs=1E-6) == 18 95 | assert improper.version == '0.21' 96 | 97 | assert len(ff.polar_terms) == 0 98 | -------------------------------------------------------------------------------- /tests/forcefield/test_primitive_ff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from mstk.topology import Topology, Molecule, Atom, Bond 5 | from mstk.forcefield import ForceField 6 | from mstk.forcefield.typer import Typer 7 | from mstk.simsys import System 8 | 9 | cwd = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | def test_primitive_ff(): 13 | mol = Molecule.from_smiles('C=CCC#CCOC(=O)NC(O)C(=O)OC') 14 | typer = Typer.open('primitive.smt') 15 | typer.type(mol) 16 | assert [atom.type for atom in mol.atoms] == [ 17 | 'C3', 'C3', 'C4', 'C2', 'C2', 'C4', 'O2', 'C3=O', 'O1', 'N3CO', 'C4', 'O2', 'C3=O', 'O1', 'O2', 'C4', 18 | 'H1', 'H1', 'H1', 'H1', 'H1', 'H1', 'H1', 'H1p', 'H1', 'H1p', 'H1', 'H1', 'H1' 19 | ] 20 | top = Topology([mol]) 21 | ff = ForceField.open('primitive.zff') 22 | ff.assign_charge(top) 23 | system = System(top, ff) 24 | -------------------------------------------------------------------------------- /tests/forcefield/test_typer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from mstk.forcefield.typer import Typer, SmartsTyper 4 | from mstk.topology import Topology, Molecule, Atom, Bond 5 | import os 6 | 7 | cwd = os.path.dirname(os.path.abspath(__file__)) 8 | 9 | typer_primitive = Typer.open('primitive.smt') 10 | 11 | def test_smarts_typer(): 12 | im61 = Molecule.from_smiles('C[n+]1cn(cc1)CCCCCC') 13 | im2eben = Molecule.from_smiles('C[n+]1cn(cc1)CCc1ccccc1') 14 | im2bben = Molecule.from_smiles('C[n+]1cn(cc1)CCCc1ccccc1') 15 | dca = Molecule.from_smiles('N#C[N-]C#N') 16 | fsi = Molecule.from_smiles('FS(=O)(=O)[N-]S(=O)(=O)F') 17 | tfsi = Molecule.from_smiles('FC(F)(F)S(=O)(=O)[N-]S(=O)(=O)C(F)(F)(F)') 18 | top = Topology([im61, im2eben, im2bben, dca, fsi, tfsi]) 19 | 20 | typer = SmartsTyper(cwd + '/files/CLP-define.smt') 21 | typer.type(top) 22 | 23 | assert [atom.type for atom in im61.atoms] == ( 24 | ['C1', 'NA', 'CR', 'NA', 'CW', 'CW', 'C1', 'C2', 'CS', 'CS', 'CS', 'CT'] 25 | + ['H1', 'H1', 'H1', 'HCR', 'HCW', 'HCW'] + ['H1', 'H1'] + ['HC'] * 11) 26 | assert [atom.type for atom in im2eben.atoms] == ( 27 | ['C1', 'NA', 'CR', 'NA', 'CW', 'CW', 'C_4H2_NA', 28 | 'C_4H2_CA', 'CAT', 'CAO', 'CAM', 'CAP', 'CAM', 'CAO'] 29 | + ['H1', 'H1', 'H1', 'HCR', 'HCW', 'HCW'] 30 | + ['H1', 'H1', 'HT', 'HT'] + ['HAT'] * 5) 31 | assert [atom.type for atom in dca.atoms] == ['NZA', 'CZA', 'N3A', 'CZA', 'NZA'] 32 | assert [atom.type for atom in fsi.atoms] == ( 33 | ['FSI', 'SBT', 'OBT', 'OBT', 'NBT', 'SBT', 'OBT', 'OBT', 'FSI']) 34 | assert [atom.type for atom in tfsi.atoms] == ( 35 | ['F1', 'CBT', 'F1', 'F1', 'SBT', 'OBT', 'OBT', 'NBT', 'SBT', 'OBT', 'OBT', 36 | 'CBT', 'F1', 'F1', 'F1']) 37 | 38 | 39 | def test_typer_primitive(): 40 | mol = Molecule() 41 | for i in range(6): 42 | atom = Atom() 43 | atom.symbol = 'C' 44 | mol.add_atom(atom) 45 | for i in range(6): 46 | C = mol.atoms[i] 47 | C2 = mol.atoms[i + 1] if i < 5 else mol.atoms[0] 48 | mol.add_bond(C, C2, order=Bond.Order.DOUBLE) 49 | 50 | typer_primitive.type(mol) 51 | assert [atom.type for atom in mol.atoms] == ['C2'] * 6 52 | 53 | 54 | def test_typer_primitive_kekulize(): 55 | mol = Molecule() 56 | for i in range(6): 57 | atom = Atom() 58 | atom.symbol = 'C' 59 | mol.add_atom(atom) 60 | for i in range(6): 61 | atom = Atom() 62 | atom.symbol = 'H' 63 | mol.add_atom(atom) 64 | for i in range(6): 65 | C, H = mol.atoms[i], mol.atoms[i + 6] 66 | mol.add_bond(C, H, order=Bond.Order.SINGLE) 67 | C2 = mol.atoms[i + 1] if i < 5 else mol.atoms[0] 68 | order = Bond.Order.SINGLE if i % 2 else Bond.Order.DOUBLE 69 | mol.add_bond(C, C2, order=order) 70 | 71 | typer_primitive.type(mol) 72 | assert [atom.type for atom in mol.atoms] == ['C3a'] * 6 + ['H1'] * 6 73 | 74 | 75 | -------------------------------------------------------------------------------- /tests/forcefield/test_zff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import tempfile 5 | import filecmp 6 | import pytest 7 | import shutil 8 | 9 | from mstk.forcefield import ForceField, Zff 10 | from mstk.forcefield.ffterm import * 11 | 12 | cwd = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | 15 | def test_read(): 16 | ff = ForceField.open(cwd + '/files/TEAM_IL.zff') 17 | assert ff.vdw_cutoff == 1.2 18 | assert ff.vdw_long_range == ForceField.VDW_LONGRANGE_CORRECT 19 | assert ff.scale_14_vdw == 0.5 20 | assert ff.lj_mixing_rule == ForceField.LJ_MIXING_LB 21 | assert pytest.approx(ff.scale_14_coulomb, abs=1E-4) == 0.8333 22 | 23 | atype = ff.atom_types['s_1-'] 24 | assert atype.name == 's_1-' 25 | assert atype.charge == -1 26 | assert atype.eqt_imp_s == 's_1' 27 | assert atype.eqt_imp_c == 's_1-' 28 | 29 | term = ff.bci_terms['b_4-,c_2nb'] 30 | assert term.type1 == 'b_4-' 31 | assert term.type2 == 'c_2nb' 32 | assert term.value == 0.262 33 | 34 | term = ff.bci_terms['b_4-,f_1'] 35 | assert term.type1 == 'b_4-' 36 | assert term.type2 == 'f_1' 37 | assert term.value == 0.4215 38 | 39 | term = ff.dihedral_terms['o_1,s_4o,n_2-,s_4'] 40 | assert type(term) == PeriodicDihedralTerm 41 | assert term.type1 == 'o_1' 42 | assert term.type2 == 's_4o' 43 | assert term.type3 == 'n_2-' 44 | assert term.type4 == 's_4' 45 | term = term.to_opls_term() 46 | assert pytest.approx([term.k1, term.k2, term.k3, term.k4], abs=1E-4) == [12.9809, 7.9709, 1.6389, 0] 47 | 48 | term = ff.improper_terms['c_3a,c_3a,c_3a,h_1'] 49 | assert type(term) == OplsImproperTerm 50 | assert term.type1 == 'c_3a' 51 | assert term.type2 == 'c_3a' 52 | assert term.type3 == 'c_3a' 53 | assert term.type4 == 'h_1' 54 | assert pytest.approx(term.k, abs=1E-4) == 7.1270 55 | 56 | 57 | def test_write(): 58 | tmpdir = tempfile.mkdtemp() 59 | tmp = os.path.join(tmpdir, 'out-CLPol.zff') 60 | clp = ForceField.open(cwd + '/files/CLP.ff', cwd + '/files/CLPol-alpha.ff') 61 | Zff.save_to(clp, tmp) 62 | assert filecmp.cmp(tmp, cwd + '/files/baselines/out-CLPol.zff') 63 | shutil.rmtree(tmpdir) 64 | 65 | 66 | def test_write2(): 67 | tmpdir = tempfile.mkdtemp() 68 | tmp = os.path.join(tmpdir, 'out-TEAM_IL.zff') 69 | il = ForceField.open(cwd + '/files/TEAM_IL.ppf') 70 | Zff.save_to(il, tmp) 71 | assert filecmp.cmp(tmp, cwd + '/files/baselines/out-TEAM_IL.zff') 72 | shutil.rmtree(tmpdir) 73 | 74 | 75 | def test_write3(): 76 | tmpdir = tempfile.mkdtemp() 77 | tmp = os.path.join(tmpdir, 'out-SPCE.zff') 78 | spce = ForceField.open(cwd + '/files/SPCE.ppf') 79 | spce.write(tmp) 80 | assert filecmp.cmp(tmp, cwd + '/files/baselines/out-SPCE.zff') 81 | shutil.rmtree(tmpdir) 82 | -------------------------------------------------------------------------------- /tests/simsys/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/simsys/__init__.py -------------------------------------------------------------------------------- /tests/simsys/files/10-benzene.in: -------------------------------------------------------------------------------- 1 | #Lammps 2009 input file generate by DFF 2 | 3 | units real 4 | atom_style full 5 | boundary p p p 6 | #boundary s s s 7 | 8 | pair_style lj/cut/coul/long 12.0 9 | #pair_style lj/cut/coul/cut 100 10 | pair_modify mix arithmetic 11 | pair_modify tail yes 12 | #pair_modify shift yes 13 | 14 | kspace_style pppm 1.0e-4 15 | dielectric 1.0 16 | special_bonds amber 17 | bond_style harmonic 18 | angle_style harmonic 19 | dihedral_style opls 20 | improper_style cvff 21 | 22 | read_data 10-benzene.lmp 23 | 24 | pair_coeff 1 2 1.079 3.5023 25 | 26 | 27 | timestep 1.0 28 | fix 1 all nvt temp 298.15 298.15 100.0 29 | 30 | variable elec equal ecoul+elong 31 | 32 | thermo_style custom pe ebond eangle edihed eimp evdwl v_elec 33 | thermo 10000 34 | 35 | run 100000 36 | 37 | -------------------------------------------------------------------------------- /tests/simsys/files/10-benzene.ppf: -------------------------------------------------------------------------------- 1 | #DFF:EQT 2 | #AAT : NB ATC BINC Bond A/C A/S T/C T/S O/C O/S 3 | c_3a : c_3a c_3a c_3a c_3a c_3a c_3a c_3a c_3a c_3a c_3a: V 1.0, S MGI, C D, R R1 4 | h_1 : h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1: V 1.0, S MGI, C D, R R1 5 | #DFF:PPF 6 | #PROTOCOL = AMBER 7 | #TYPINGRULE = 8 | ATYPE: c_3a: 6.00000*, 12.01100*: V 1.0, S MGI, C D, R R1 9 | ATYPE: h_1: 1.00000*, 1.00790*: V 1.0, S MGI, C D, R R1 10 | ATC: c_3a: 0.00000*: V 1.0, S MGI, C D, R R1 11 | ATC: h_1: 0.00000*: V 1.0, S MGI, C D, R R1 12 | BINC: c_3a, c_3a: 0.00000*: V 1.0, S MGI, C D, R R1 13 | BINC: c_3a, h_1: -0.12000*: V 1.0, S MGI, C D, R R1 14 | N12_6: c_3a: 3.93123*, 0.07871*: V 1.80, S MGI, C D, R R9 15 | N12_6: h_1: 2.73172*, 0.02125*: V 1.78, S MGI, C D, R R9 16 | BHARM: c_3a, c_3a: 1.38700*, 389.05130*: V 1.0, S MGI, C D, R R1 17 | BHARM: c_3a, h_1: 1.08860*, 400.70700*: V 1.0, S MGI, C D, R R1 18 | AHARM: c_3a, c_3a, c_3a: 120.00040*, 46.35840*: V 1.0, S MGI, C D, R R1 19 | AHARM: c_3a, c_3a, h_1: 118.91600*, 43.87690*: V 1.0, S MGI, C D, R R1 20 | TCOSP: c_3a, c_3a, c_3a, c_3a: 180.00000*, 3.49930*, 2.00000*: V 1.0, S MGI, C D, R R1 21 | TCOSP: c_3a, c_3a, c_3a, h_1: 180.00000*, 3.99950*, 2.00000*: V 1.0, S MGI, C D, R R1 22 | TCOSP: h_1, c_3a, c_3a, h_1: 180.00000*, 1.97650*, 2.00000*: V 1.0, S MGI, C D, R R1 23 | IBCOS: c_3a, c_3a, c_3a, h_1: 180.00000*, 1.70340*, 2.00000*: V 1.0, S MGI, C D, R R1 24 | P12_6: c_3a, h_1: 3.93123*, 1.07871*: Fake parameters to test pairwise vdW interactions 25 | -------------------------------------------------------------------------------- /tests/simsys/files/baselines/ff.prm: -------------------------------------------------------------------------------- 1 | * Created by mstk 2 | * 3 | 4 | ATOMS 5 | MASS 1 c_3a 12.0110 6 | MASS 2 h_1 1.0079 7 | 8 | BONDS 9 | !V(bond) = Kb(b - b0)**2 10 | !Kb: kcal/mole/A**2 11 | !b0: A 12 | ! 13 | ! atom type Kb b0 14 | c_3a c_3a 389.051300 1.3870 15 | c_3a h_1 400.707000 1.0886 16 | 17 | ANGLES 18 | !V(angle) = Ktheta(Theta - Theta0)**2 19 | !V(Urey-Bradley) = Kub(S - S0)**2 20 | !Ktheta: kcal/mole/rad**2 21 | !Theta0: degrees 22 | !Kub: kcal/mole/A**2 (Urey-Bradley) 23 | !S0: A 24 | ! 25 | ! atom types Ktheta Theta0 Kub S0 26 | c_3a c_3a c_3a 46.358400 120.0004 27 | c_3a c_3a h_1 43.876900 118.9160 28 | 29 | DIHEDRALS 30 | !V(dihedral) = Kchi(1 + cos(n(chi) - delta)) 31 | !Kchi: kcal/mole 32 | !n: multiplicity 33 | !delta: degrees 34 | ! 35 | !atom types Kchi n delta 36 | c_3a c_3a c_3a c_3a 3.499300 2 180.00 37 | c_3a c_3a c_3a h_1 3.999500 2 180.00 38 | h_1 c_3a c_3a h_1 1.976500 2 180.00 39 | 40 | IMPROPERS 41 | !V(improper) = Kpsi(psi - psi0)**2 42 | !Kpsi: kcal/mole/rad**2 43 | !psi0: degrees 44 | ! 45 | ! atom types Kpsi ignored psi0 46 | c_3a c_3a c_3a h_1 1.703400 0 0.00 47 | 48 | NONBONDED 49 | !V(Lennard-Jones) = Eps,i,j[(Rmin,i,j/ri,j)**12 - 2(Rmin,i,j/ri,j)**6] 50 | !epsilon: kcal/mole, Eps,i,j = sqrt(eps,i * eps,j) 51 | !Rmin/2: A, Rmin,i,j = Rmin/2,i + Rmin/2,j 52 | ! 53 | ! atom ignored epsilon Rmin/2 ignored eps,1-4 Rmin/2,1-4 54 | c_3a 0.0000 -0.078710 1.965615 0.0000 -0.039355 1.965615 ! 55 | h_1 0.0000 -0.021250 1.365860 0.0000 -0.010625 1.365860 ! 56 | 57 | NBFIX 58 | ! atom atom epsilon Rmin eps,1-4 Rmin,1-4 59 | c_3a h_1 -1.078710 3.931230 -0.539355 3.931230 ! 60 | 61 | END 62 | -------------------------------------------------------------------------------- /tests/simsys/files/baselines/grompp-drude.mdp: -------------------------------------------------------------------------------- 1 | ; Created by mstk 2 | integrator = sd 3 | dt = 0.002 ; ps 4 | nsteps = 1000000 5 | 6 | nstxout = 0 7 | nstvout = 0 8 | nstfout = 0 9 | nstxout-compressed = 1000 10 | compressed-x-grps = System 11 | 12 | cutoff-scheme = Verlet 13 | pbc = xyz 14 | ; rlist = 1.2 15 | coulombtype = PME 16 | rcoulomb = 1.2 17 | vdwtype = Cut-off 18 | rvdw = 1.2 19 | DispCorr = EnerPres 20 | 21 | tcoupl = no; v-rescale 22 | tc_grps = System 23 | tau_t = 0.2 24 | ref_t = 300 25 | 26 | pcoupl = berendsen ; parrinello-rahman 27 | pcoupltype = isotropic 28 | tau_p = 0.5; 5 29 | compressibility = 4.5e-5 30 | ref_p = 1.0 31 | 32 | gen_vel = yes 33 | gen_temp = 300 34 | 35 | constraints = h-bonds 36 | constraint-algorithm = LINCS 37 | -------------------------------------------------------------------------------- /tests/simsys/files/baselines/grompp-vsite.mdp: -------------------------------------------------------------------------------- 1 | ; Created by mstk 2 | integrator = sd 3 | dt = 0.002 ; ps 4 | nsteps = 1000000 5 | 6 | nstxout = 0 7 | nstvout = 0 8 | nstfout = 0 9 | nstxout-compressed = 1000 10 | compressed-x-grps = System 11 | 12 | cutoff-scheme = Verlet 13 | pbc = xyz 14 | ; rlist = 1.2 15 | coulombtype = PME 16 | rcoulomb = 1.2 17 | vdwtype = Cut-off 18 | rvdw = 1.2 19 | DispCorr = EnerPres 20 | 21 | tcoupl = no; v-rescale 22 | tc_grps = System 23 | tau_t = 1.0 24 | ref_t = 300 25 | 26 | pcoupl = berendsen ; parrinello-rahman 27 | pcoupltype = isotropic 28 | tau_p = 0.5; 5 29 | compressibility = 4.5e-5 30 | ref_p = 1.0 31 | 32 | gen_vel = yes 33 | gen_temp = 300 34 | 35 | constraints = h-bonds 36 | constraint-algorithm = LINCS 37 | -------------------------------------------------------------------------------- /tests/simsys/files/baselines/grompp.mdp: -------------------------------------------------------------------------------- 1 | ; Created by mstk 2 | integrator = sd 3 | dt = 0.002 ; ps 4 | nsteps = 1000000 5 | 6 | nstxout = 0 7 | nstvout = 0 8 | nstfout = 0 9 | nstxout-compressed = 1000 10 | compressed-x-grps = System 11 | 12 | cutoff-scheme = Verlet 13 | pbc = xyz 14 | ; rlist = 1.2 15 | coulombtype = PME 16 | rcoulomb = 1.2 17 | vdwtype = Cut-off 18 | rvdw = 1.2 19 | DispCorr = EnerPres 20 | 21 | tcoupl = no; v-rescale 22 | tc_grps = System 23 | tau_t = 1.0 24 | ref_t = 300 25 | 26 | pcoupl = berendsen ; parrinello-rahman 27 | pcoupltype = isotropic 28 | tau_p = 0.5; 5 29 | compressibility = 4.5e-5 30 | ref_p = 1.0 31 | 32 | gen_vel = yes 33 | gen_temp = 300 34 | 35 | constraints = h-bonds 36 | constraint-algorithm = LINCS 37 | -------------------------------------------------------------------------------- /tests/simsys/files/baselines/topol-vsite.top: -------------------------------------------------------------------------------- 1 | ; GROMACS topol file created by mstk 2 | 3 | [ defaults ] 4 | ; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ 5 | 1 2 yes 0.5000 0.8333 6 | 7 | [ atomtypes ] 8 | ; name mass charge ptype sigma epsilon 9 | OW 0.0000 0.000000 A 0.315365 0.648520 ; 10 | HW 0.0000 0.000000 A 0.000000 0.000000 ; 11 | MW 0.0000 0.000000 A 0.000000 0.000000 ; 12 | 13 | [ nonbond_params ] 14 | ; i j func sigma epsilon 15 | 16 | [ moleculetype ] 17 | ; Name nrexcl 18 | SOL_1 3 19 | 20 | [ atoms ] 21 | ; nr type resnr residue atom cgnr charge mass 22 | 1 OW 1 SOL O 1 0.000000 15.9990 23 | 2 HW 1 SOL H 2 0.520000 1.0080 24 | 3 HW 1 SOL H 3 0.520000 1.0080 25 | 4 MW 1 SOL VS 4 -1.040000 0.0000 26 | 27 | [ virtual_sites3 ] 28 | ;Vsite from1 from2 from3 funct a b 29 | 4 1 2 3 1 0.128012 0.128012 30 | 31 | [ pairs ] 32 | 33 | [ constraints ] 34 | 1 2 1 0.095720 35 | 1 3 1 0.095720 36 | 2 3 2 0.151390 37 | 38 | [ bonds ] 39 | 40 | [ exclusions ] 41 | ; ai aj ... 42 | 4 1 2 3 43 | 44 | [ angles ] 45 | 46 | [ dihedrals ] 47 | 48 | [ dihedrals ] 49 | 50 | [ system ] 51 | 52 | [ molecules ] 53 | SOL_1 100 54 | -------------------------------------------------------------------------------- /tests/simsys/files/c_3ad.gro: -------------------------------------------------------------------------------- 1 | Created by mstk 2 | 25 3 | 1Methy C1 1 -0.601 -0.350 0.000 4 | 1Methy C2 2 -0.684 -0.229 0.000 5 | 1Methy C3 3 -0.620 -0.096 -0.000 6 | 1Methy C4 4 -0.473 -0.085 -0.000 7 | 1Methy C5 5 -0.383 -0.206 -0.000 8 | 1Methy C6 6 -0.453 -0.339 0.000 9 | 1Methy C7 7 -0.232 -0.194 0.000 10 | 1Methy C8 8 -0.143 -0.315 -0.000 11 | 1Methy C9 9 0.006 -0.304 -0.000 12 | 1Methy C10 10 0.068 -0.170 0.000 13 | 1Methy C11 11 -0.016 -0.049 0.000 14 | 1Methy C12 12 -0.163 -0.061 0.000 15 | 1Methy C13 13 0.090 -0.428 -0.000 16 | 1Methy H14 14 -0.648 -0.448 0.000 17 | 1Methy H15 15 -0.792 -0.237 -0.000 18 | 1Methy H16 16 -0.681 -0.007 0.000 19 | 1Methy H17 17 -0.434 0.016 -0.000 20 | 1Methy H18 18 -0.400 -0.433 0.000 21 | 1Methy H19 19 -0.182 -0.415 -0.000 22 | 1Methy H20 20 0.176 -0.160 -0.000 23 | 1Methy H21 21 0.031 0.048 -0.000 24 | 1Methy H22 22 -0.216 0.033 0.000 25 | 1Methy H23 23 0.199 -0.405 -0.000 26 | 1Methy H24 24 0.067 -0.489 0.091 27 | 1Methy H25 25 0.067 -0.489 -0.091 28 | 0.0000 0.0000 0.0000 29 | -------------------------------------------------------------------------------- /tests/simsys/files/c_3ad.ppf: -------------------------------------------------------------------------------- 1 | #DFF:EQT 2 | #AAT : NB ATC BINC Bond A/C A/S T/C T/S O/C O/S 3 | c_3a : c_3a c_3a c_3a c_3a c_3a c_3a c_3a c_3a c_3a c_3a: V 1.0, S MGI, C D, R R1 4 | c_3ac : c_3ac c_3ac c_3a c_3a c_3a c_3a c_3a c_3a c_3a c_3a: V 1.15, S MGI, C V, R R1 5 | c_3ad : c_3ac c_3a c_3a c_3a c_3a c_3a c_3ad c_3a c_3a c_3a: V 1.80, S MGI, C D, R R9 6 | c_4h3 : c_4h3 c_4h3 c_4 c_4 c_4 c_4 c_4 c_4 c_4 c_4: V 1.0, S MGI, C D, R R1 7 | h_1 : h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1: V 1.0, S MGI, C D, R R1 8 | #DFF:PPF 9 | #PROTOCOL = AMBER 10 | #TYPINGRULE = D:\Projects\DFF\Developing\database\TEAMFF.ref\MGI\MGI.ext 11 | ATYPE: c_3a: 6.00000*, 12.01100*: V 1.0, S MGI, C D, R R1 12 | ATYPE: c_3ac: 6.00000*, 12.01100*: V 1.35, S MGI, C D, R R1 13 | ATYPE: c_4h3: 6.00000*, 12.01100*: V 1.0, S MGI, C D, R R1 14 | ATYPE: h_1: 1.00000*, 1.00790*: V 1.0, S MGI, C D, R R1 15 | ATC: c_3a: 0.00000*: V 1.0, S MGI, C D, R R1 16 | ATC: c_3ac: 0.00000*: V 1.91, S MGI, C D, R R9 17 | ATC: c_4h3: 0.00000*: V 1.0, S MGI, C D, R R1 18 | ATC: h_1: 0.00000*: V 1.0, S MGI, C D, R R1 19 | BINC: c_3a, c_3a: 0.00000*: V 1.0, S MGI, C D, R R1 20 | BINC: c_3a, c_4: 0.08820*: V 1.0, S MGI, C D, R R1 21 | BINC: c_3a, h_1: -0.12000*: V 1.0, S MGI, C D, R R1 22 | BINC: c_4, h_1: -0.06000*: V 1.0, S MGI, C D, R R1 23 | N12_6: c_3a: 3.93123*, 0.07871*: V 1.80, S MGI, C D, R R9 24 | N12_6: c_3ac: 3.87585*, 0.07797*: V 1.80, S MGI, C D, R R9 25 | N12_6: c_3ad: 3.81943*, 0.07719*: V 1.75, S MGI, C D, R R9 26 | N12_6: c_4h3: 4.09133*, 0.07855*: V 1.78, S MGI, C D, R R9 27 | N12_6: h_1: 2.73172*, 0.02125*: V 1.78, S MGI, C D, R R9 28 | BHARM: c_3a, c_3a: 1.38700*, 389.05130*: V 1.0, S MGI, C D, R R1 29 | BHARM: c_3a, c_4: 1.50840*, 254.18840*: V 1.0, S MGI, C D, R R1 30 | BHARM: c_3a, h_1: 1.08860*, 400.70700*: V 1.0, S MGI, C D, R R1 31 | BHARM: c_4, h_1: 1.09670*, 360.27960*: V 1.0, S MGI, C D, R R1 32 | AHARM: c_3a, c_3a, c_3a: 120.00040*, 46.35840*: V 1.0, S MGI, C D, R R1 33 | AHARM: c_3a, c_3a, c_4: 122.15610*, 71.46610*: V 1.0, S MGI, C D, R R1 34 | AHARM: c_3a, c_3a, h_1: 118.91600*, 43.87690*: V 1.0, S MGI, C D, R R1 35 | AHARM: c_3a, c_4, h_1: 110.60100*, 35.90670*: V 1.0, S MGI, C D, R R1 36 | AHARM: h_1, c_4, h_1: 106.67940*, 35.64690*: V 1.0, S MGI, C D, R R1 37 | TCOSP: X, c_3a, c_3ad, X: 180.00000*, 4.03420*, 2.00000*: V 1.53, S MGI, C D, R R9 38 | TCOSP: c_3a, c_3a, c_3a, c_3a: 180.00000*, 3.49930*, 2.00000*: V 1.0, S MGI, C D, R R1 39 | TCOSP: c_3a, c_3a, c_3a, c_4: 180.00000*, 4.59820*, 2.00000*: V 1.0, S MGI, C D, R R1 40 | TCOSP: c_3a, c_3a, c_3a, h_1: 180.00000*, 3.99950*, 2.00000*: V 1.0, S MGI, C D, R R1 41 | TCOSP: c_3a, c_3a, c_4, h_1: 0.00000*, 0.07830*, 3.00000*, 0.00000*, -1.80450*, 1.00000*, 180.00000*, 0.28990*, 2.00000*: V 1.0, S MGI, C D, R R1 42 | TCOSP: c_3a, c_3ad, c_3ad, c_3a: 180.00000*, 0.99030*, 2.00000*: V 1.80, S MGI, C D, R R9 43 | TCOSP: c_4, c_3a, c_3a, h_1: 180.00000*, 1.39840*, 2.00000*: V 1.0, S MGI, C D, R R1 44 | TCOSP: h_1, c_3a, c_3a, h_1: 180.00000*, 1.97650*, 2.00000*: V 1.0, S MGI, C D, R R1 45 | IBCOS: c_3a, c_3a, c_3a, c_3a: 180.00000*, 4.52940*, 2.00000*: V 1.90, S MGI, C D, R R9 46 | IBCOS: c_3a, c_3a, c_3a, c_4: 180.00000*, 4.68180*, 2.00000*: V 1.0, S MGI, C D, R R1 47 | IBCOS: c_3a, c_3a, c_3a, h_1: 180.00000*, 1.70340*, 2.00000*: V 1.0, S MGI, C D, R R1 48 | -------------------------------------------------------------------------------- /tests/simsys/test_export.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import tempfile 5 | import filecmp 6 | import pytest 7 | from mstk.topology import Topology, UnitCell 8 | from mstk.forcefield import ForceField 9 | from mstk.simsys import System 10 | import shutil 11 | 12 | cwd = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | 15 | def test_gmx(): 16 | tmpdir = tempfile.mkdtemp() 17 | ff = ForceField.open(cwd + '/files/10-benzene.ppf') 18 | top = Topology.open(cwd + '/files/10-benzene.lmp', improper_center=3) 19 | ff.assign_charge(top) 20 | system = System(top, ff) 21 | tmpgro = os.path.join(tmpdir, 'conf.gro') 22 | tmptop = os.path.join(tmpdir, 'topol.top') 23 | tmpmdp = os.path.join(tmpdir, 'grompp.mdp') 24 | system.export_gromacs(gro_out=tmpgro, top_out=tmptop, mdp_out=tmpmdp) 25 | assert filecmp.cmp(tmpgro, cwd + '/files/baselines/conf.gro') 26 | assert filecmp.cmp(tmptop, cwd + '/files/baselines/topol.top') 27 | assert filecmp.cmp(tmpmdp, cwd + '/files/baselines/grompp.mdp') 28 | shutil.rmtree(tmpdir) 29 | 30 | 31 | def test_gmx_drude(): 32 | tmpdir = tempfile.mkdtemp() 33 | ff = ForceField.open(cwd + '/../forcefield/files/CLP.ff', cwd + '/../forcefield/files/CLPol-alpha.ff') 34 | top = Topology.open(cwd + '/files/5-Im21-BF4-drude.lmp') 35 | top.generate_angle_dihedral_improper() 36 | top.generate_drude_particles(ff) 37 | ff.assign_charge(top) 38 | system = System(top, ff) 39 | tmpgro = os.path.join(tmpdir, 'conf-drude.gro') 40 | tmptop = os.path.join(tmpdir, 'topol-drude.top') 41 | tmpmdp = os.path.join(tmpdir, 'grompp-drude.mdp') 42 | system.export_gromacs(gro_out=tmpgro, top_out=tmptop, mdp_out=tmpmdp) 43 | assert filecmp.cmp(tmpgro, cwd + '/files/baselines/conf-drude.gro') 44 | assert filecmp.cmp(tmptop, cwd + '/files/baselines/topol-drude.top') 45 | assert filecmp.cmp(tmpmdp, cwd + '/files/baselines/grompp-drude.mdp') 46 | shutil.rmtree(tmpdir) 47 | 48 | 49 | def test_gmx_tip4p(): 50 | tmpdir = tempfile.mkdtemp() 51 | mol = Topology.open(cwd + '/../topology/files/TIP3P.zmat').molecules[0] 52 | ff = ForceField.open(cwd + '/../forcefield/files/TIP4P.zff') 53 | mol.generate_virtual_sites(ff) 54 | ff.assign_charge(mol) 55 | 56 | top = Topology([mol], numbers=[100], cell=UnitCell([3, 3, 3])) 57 | top.set_positions(Topology.open(cwd + '/files/100-TIP4P.pdb').positions) 58 | 59 | system = System(top, ff) 60 | tmpgro = os.path.join(tmpdir, 'conf-vsite.gro') 61 | tmptop = os.path.join(tmpdir, 'topol-vsite.top') 62 | tmpmdp = os.path.join(tmpdir, 'grompp-vsite.mdp') 63 | system.export_gromacs(gro_out=tmpgro, top_out=tmptop, mdp_out=tmpmdp) 64 | assert filecmp.cmp(tmpgro, cwd + '/files/baselines/conf-vsite.gro') 65 | assert filecmp.cmp(tmptop, cwd + '/files/baselines/topol-vsite.top') 66 | assert filecmp.cmp(tmpmdp, cwd + '/files/baselines/grompp-vsite.mdp') 67 | shutil.rmtree(tmpdir) 68 | 69 | 70 | def test_namd(): 71 | tmpdir = tempfile.mkdtemp() 72 | ff = ForceField.open(cwd + '/files/10-benzene.ppf') 73 | top = Topology.open(cwd + '/files/10-benzene.lmp', improper_center=3) 74 | ff.assign_charge(top) 75 | system = System(top, ff) 76 | tmppdb = os.path.join(tmpdir, 'conf.pdb') 77 | tmppsf = os.path.join(tmpdir, 'top.psf') 78 | tmpprm = os.path.join(tmpdir, 'ff.prm') 79 | system.export_namd(pdb_out=tmppdb, psf_out=tmppsf, prm_out=tmpprm) 80 | assert filecmp.cmp(tmppdb, cwd + '/files/baselines/conf.pdb') 81 | assert filecmp.cmp(tmppsf, cwd + '/files/baselines/top.psf') 82 | assert filecmp.cmp(tmpprm, cwd + '/files/baselines/ff.prm') 83 | shutil.rmtree(tmpdir) 84 | -------------------------------------------------------------------------------- /tests/topology/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/topology/__init__.py -------------------------------------------------------------------------------- /tests/topology/files/CH3NH2.pdb: -------------------------------------------------------------------------------- 1 | REMARK 1 File created by GaussView 6.0.16 2 | HETATM 1 C 0 -1.255 -3.525 0.000 C 3 | HETATM 2 H 0 -0.899 -4.534 0.000 H 4 | HETATM 3 H 0 -0.899 -3.020 -0.874 H 5 | HETATM 4 H 0 -2.325 -3.525 0.000 H 6 | HETATM 5 N 0 -0.765 -2.832 1.200 N 7 | HETATM 6 H 0 0.235 -2.832 1.200 H 8 | HETATM 7 H 0 -1.098 -1.889 1.200 H 9 | END 10 | CONECT 1 2 3 4 5 11 | CONECT 2 1 12 | CONECT 3 1 13 | CONECT 4 1 14 | CONECT 5 6 7 1 15 | CONECT 6 5 16 | CONECT 7 5 17 | -------------------------------------------------------------------------------- /tests/topology/files/CH3NH3+.sdf: -------------------------------------------------------------------------------- 1 | CH3NH3+ 2 | RDKit 3D 3 | 4 | 8 7 0 0 0 0 0 0 0 0999 V2000 5 | -0.7133 -0.0736 0.0052 C 0 0 0 0 0 0 0 0 0 0 0 0 6 | 0.7355 0.0131 -0.0126 N 0 0 0 0 0 4 0 0 0 0 0 0 7 | -1.0894 -1.1007 -0.0670 H 0 0 0 0 0 0 0 0 0 0 0 0 8 | -1.1076 0.5194 -0.8394 H 0 0 0 0 0 0 0 0 0 0 0 0 9 | -1.0598 0.4156 0.9309 H 0 0 0 0 0 0 0 0 0 0 0 0 10 | 1.1512 -0.4030 -0.8602 H 0 0 0 0 0 0 0 0 0 0 0 0 11 | 1.1461 -0.4190 0.8334 H 0 0 0 0 0 0 0 0 0 0 0 0 12 | 0.9372 1.0482 0.0097 H 0 0 0 0 0 0 0 0 0 0 0 0 13 | 1 2 1 0 14 | 1 3 1 0 15 | 1 4 1 0 16 | 1 5 1 0 17 | 2 6 1 0 18 | 2 7 1 0 19 | 2 8 1 0 20 | M CHG 1 2 1 21 | M END 22 | $$$$ 23 | -------------------------------------------------------------------------------- /tests/topology/files/Im11.zmat: -------------------------------------------------------------------------------- 1 | c1c1im+ 2 | 3 | 1 NA 4 | 2 CR 1 1.315 5 | 3 NA 2 1.315 1 109.8 6 | 4 CW 3 1.378 2 108.0 1 0.0 7 | 5 CW 4 1.341 3 107.1 2 0.0 8 | 6 C1 1 1.466 5 125.6 4 180.0 9 | 7 HCR 2 1.080 1 125.1 3 180.0 10 | 8 C1 3 1.466 2 126.3 1 180.0 11 | 9 HCW 4 1.080 3 126.45 2 180.0 12 | 10 HCW 5 1.080 4 126.45 3 180.0 13 | 11 H1 6 1.090 1 109.5 5 60.0 14 | 12 H1 6 1.090 1 109.5 5 180.0 15 | 13 H1 6 1.090 1 109.5 5 300.0 16 | 14 H1 8 1.090 3 109.5 4 60.0 17 | 15 H1 8 1.090 3 109.5 4 180.0 18 | 16 H1 8 1.090 3 109.5 4 300.0 19 | 20 | connect 5 1 21 | 22 | improper 5 2 1 6 23 | improper 1 3 2 7 24 | improper 2 4 3 8 25 | improper 3 5 4 9 26 | improper 4 1 5 10 27 | 28 | il.ff -------------------------------------------------------------------------------- /tests/topology/files/MoS2.ff: -------------------------------------------------------------------------------- 1 | # oplsaa.ff, version 2017/06/19 2 | # units: kJ/mol, A, deg 3 | # bond and angle force constants are in the form k/2 (x - x0)^2 4 | # use cons for constrained bonds and angles 5 | # improper dihedrals are relative to the third atom in the list 6 | 7 | ATOMS 8 | # type m/u q/e pot pars 9 | # MoS2 10 | MoS MoS 95.937 +0.5 lj 4.43 0.485 11 | SMo SMo 32.064 -0.25 lj 3.34 2.085 12 | 13 | BONDS 14 | # i j pot re/A kr/kJmol-1 15 | # MoS2 16 | MoS SMo harm 2.41 430.3 17 | 18 | ANGLES 19 | # i j k pot th/deg ka/kjmol-1 20 | # MoS2 21 | SMo MoS SMo harm 83.8 1187.7 22 | MoS SMo MoS harm 83.8 2050.0 23 | 24 | DIHEDRALS 25 | # i j k l pot v1 v2 v3 v4 26 | # alkanes JACS 118(1996)11225, JPC 100(1996)18010 27 | #HC CT CT HC opls 0.0000 0.0000 1.2552 0.0000 28 | 29 | IMPROPER 30 | # improper C aromatics AMBER JACS 117(1995)5179 31 | #CA CA CA HA opls 0.0000 9.2048 0.0000 0.0000 32 | -------------------------------------------------------------------------------- /tests/topology/files/TIP3P.zmat: -------------------------------------------------------------------------------- 1 | SOL 2 | 3 | OW 4 | HW 1 0.9572 5 | HW 1 0.9572 2 104.52 -------------------------------------------------------------------------------- /tests/topology/files/baselines/_MO_0.xyz: -------------------------------------------------------------------------------- 1 | 16 2 | c1c1im+ 3 | NA 0.00000 0.00000 0.00000 4 | CR 1.31500 0.00000 0.00000 5 | NA 1.76044 1.23726 0.00000 6 | CW 0.67161 2.08184 0.00000 7 | CW -0.42553 1.31076 0.00000 8 | C1 -0.87025 -1.17976 0.00000 9 | HCR 1.93601 -0.88360 0.00000 10 | C1 3.16607 1.65362 0.00000 11 | HCW 0.69705 3.16154 0.00000 12 | HCW -1.45002 1.65256 0.00000 13 | H1 -1.49966 -1.16760 -0.88982 14 | H1 -0.25937 -2.08250 -0.00000 15 | H1 -1.49966 -1.16760 0.88982 16 | H1 3.36903 2.24955 0.88982 17 | H1 3.80676 0.77180 0.00000 18 | H1 3.36903 2.24955 -0.88982 19 | -------------------------------------------------------------------------------- /tests/topology/files/baselines/_pack.inp: -------------------------------------------------------------------------------- 1 | filetype xyz 2 | tolerance 2.0 3 | output /tmp/_mstk_test_packmol/_out.xyz 4 | seed 1 5 | structure /tmp/_mstk_test_packmol/_MO_0.xyz 6 | number 10 7 | inside box 0 0 0 28.000000 28.000000 28.000000 8 | end structure 9 | -------------------------------------------------------------------------------- /tests/topology/files/baselines/zmat-out.pdb: -------------------------------------------------------------------------------- 1 | REMARK Created by mstk 2 | CRYST1 0.000 0.000 0.000 90.00 90.00 90.00 P 1 1 3 | HETATM 1 N1 c1c1 1 0.000 0.000 0.000 N 4 | HETATM 2 C2 c1c1 1 1.315 0.000 0.000 C 5 | HETATM 3 N3 c1c1 1 1.760 1.237 0.000 N 6 | HETATM 4 C4 c1c1 1 0.672 2.082 0.000 C 7 | HETATM 5 C5 c1c1 1 -0.426 1.311 0.000 C 8 | HETATM 6 C6 c1c1 1 -0.870 -1.180 0.000 C 9 | HETATM 7 H7 c1c1 1 1.936 -0.884 0.000 H 10 | HETATM 8 C8 c1c1 1 3.166 1.654 0.000 C 11 | HETATM 9 H9 c1c1 1 0.697 3.162 0.000 H 12 | HETATM 10 H10 c1c1 1 -1.450 1.653 0.000 H 13 | HETATM 11 H11 c1c1 1 -1.500 -1.168 -0.890 H 14 | HETATM 12 H12 c1c1 1 -0.259 -2.082 -0.000 H 15 | HETATM 13 H13 c1c1 1 -1.500 -1.168 0.890 H 16 | HETATM 14 H14 c1c1 1 3.369 2.250 0.890 H 17 | HETATM 15 H15 c1c1 1 3.807 0.772 0.000 H 18 | HETATM 16 H16 c1c1 1 3.369 2.250 -0.890 H 19 | CONECT 1 2 6 5 20 | CONECT 2 3 7 21 | CONECT 3 4 8 22 | CONECT 4 5 9 23 | CONECT 5 10 24 | CONECT 6 11 12 13 25 | CONECT 8 14 15 16 26 | -------------------------------------------------------------------------------- /tests/topology/files/baselines/zmat-out.psf: -------------------------------------------------------------------------------- 1 | PSF NAMD 2 | 3 | 1 !NTITLE 4 | REMARKS Created by mstk 5 | 6 | 16 !NATOM 7 | 1 S 1 c1c1im+ N1 NA 0.000000 14.0060 0 -0.0000 0.0000 8 | 2 S 1 c1c1im+ C2 CR 0.000000 12.0110 0 -0.0000 0.0000 9 | 3 S 1 c1c1im+ N3 NA 0.000000 14.0060 0 -0.0000 0.0000 10 | 4 S 1 c1c1im+ C4 CW 0.000000 12.0110 0 -0.0000 0.0000 11 | 5 S 1 c1c1im+ C5 CW 0.000000 12.0110 0 -0.0000 0.0000 12 | 6 S 1 c1c1im+ C6 C1 0.000000 12.0110 0 -0.0000 0.0000 13 | 7 S 1 c1c1im+ H7 HCR 0.000000 1.0080 0 -0.0000 0.0000 14 | 8 S 1 c1c1im+ C8 C1 0.000000 12.0110 0 -0.0000 0.0000 15 | 9 S 1 c1c1im+ H9 HCW 0.000000 1.0080 0 -0.0000 0.0000 16 | 10 S 1 c1c1im+ H10 HCW 0.000000 1.0080 0 -0.0000 0.0000 17 | 11 S 1 c1c1im+ H11 H1 0.000000 1.0080 0 -0.0000 0.0000 18 | 12 S 1 c1c1im+ H12 H1 0.000000 1.0080 0 -0.0000 0.0000 19 | 13 S 1 c1c1im+ H13 H1 0.000000 1.0080 0 -0.0000 0.0000 20 | 14 S 1 c1c1im+ H14 H1 0.000000 1.0080 0 -0.0000 0.0000 21 | 15 S 1 c1c1im+ H15 H1 0.000000 1.0080 0 -0.0000 0.0000 22 | 16 S 1 c1c1im+ H16 H1 0.000000 1.0080 0 -0.0000 0.0000 23 | 24 | 16 !NBOND: bonds 25 | 1 2 2 3 3 4 4 5 26 | 1 6 2 7 3 8 4 9 27 | 5 10 6 11 6 12 6 13 28 | 8 14 8 15 8 16 5 1 29 | 30 | 27 !NTHETA: angles 31 | 2 1 6 2 1 5 6 1 5 32 | 1 2 3 1 2 7 3 2 7 33 | 2 3 4 2 3 8 4 3 8 34 | 3 4 5 3 4 9 5 4 9 35 | 4 5 10 4 5 1 10 5 1 36 | 1 6 11 1 6 12 1 6 13 37 | 11 6 12 11 6 13 12 6 13 38 | 3 8 14 3 8 15 3 8 16 39 | 14 8 15 14 8 16 15 8 16 40 | 41 | 32 !NPHI: dihedrals 42 | 6 1 2 3 6 1 2 7 43 | 5 1 2 3 5 1 2 7 44 | 1 2 3 4 1 2 3 8 45 | 7 2 3 4 7 2 3 8 46 | 2 3 4 5 2 3 4 9 47 | 8 3 4 5 8 3 4 9 48 | 3 4 5 10 3 4 5 1 49 | 9 4 5 10 9 4 5 1 50 | 2 1 6 11 2 1 6 12 51 | 2 1 6 13 5 1 6 11 52 | 5 1 6 12 5 1 6 13 53 | 2 3 8 14 2 3 8 15 54 | 2 3 8 16 4 3 8 14 55 | 4 3 8 15 4 3 8 16 56 | 4 5 1 2 4 5 1 6 57 | 10 5 1 2 10 5 1 6 58 | 59 | 5 !NIMPHI: impropers 60 | 1 2 6 5 2 1 3 7 61 | 3 2 4 8 4 3 5 9 62 | 5 4 10 1 63 | 64 | 0 !NDON: donors 65 | 66 | 0 !NACC: acceptors 67 | 68 | 0 !NNB 69 | 70 | 71 | 0 0 !NUMLP NUMLPH 72 | 73 | 0 !NUMANISO 74 | 75 | -------------------------------------------------------------------------------- /tests/topology/files/baselines/zmat-out.xyz: -------------------------------------------------------------------------------- 1 | 16 2 | c1c1im+ 3 | NA 0.00000 0.00000 0.00000 4 | CR 1.31500 0.00000 0.00000 5 | NA 1.76044 1.23726 0.00000 6 | CW 0.67161 2.08184 0.00000 7 | CW -0.42553 1.31076 0.00000 8 | C1 -0.87025 -1.17976 0.00000 9 | HCR 1.93601 -0.88360 0.00000 10 | C1 3.16607 1.65362 0.00000 11 | HCW 0.69705 3.16154 0.00000 12 | HCW -1.45002 1.65256 0.00000 13 | H1 -1.49966 -1.16760 -0.88982 14 | H1 -0.25937 -2.08250 -0.00000 15 | H1 -1.49966 -1.16760 0.88982 16 | H1 3.36903 2.24955 0.88982 17 | H1 3.80676 0.77180 0.00000 18 | H1 3.36903 2.24955 -0.88982 19 | -------------------------------------------------------------------------------- /tests/topology/files/c_3oh.ppf: -------------------------------------------------------------------------------- 1 | #DFF:EQT 2 | #AAT : NB ATC BINC Bond A/C A/S T/C T/S O/C O/S 3 | c_3h : c_3h c_3h c_3 c_3 c_3 c_3 c_3 c_3 c_3 c_3: V 1.0, S MGI, C D, R R1 4 | c_3h2 : c_3h2 c_3h2 c_3 c_3 c_3 c_3 c_3 c_3 c_3 c_3: V 1.0, S MGI, C D, R R1 5 | c_3oh : c_3oh c_3oh c_3oh c_3o c_3o c_3o c_3o c_3 c_3o c_3: V 1.0, S MGI, C D, R R1 6 | h_1 : h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1 h_1: V 1.0, S MGI, C D, R R1 7 | o_1 : o_1 o_1 o_1 o_1 o_1 o_1 o_1 o_1 o_1 o_1: V 1.0, S MGI, C D, R R1 8 | #DFF:PPF 9 | #PROTOCOL = AMBER 10 | #TYPINGRULE = 11 | ATYPE: c_3h: 6.00000*, 12.01100*: V 1.0, S MGI, C D, R R1 12 | ATYPE: c_3h2: 6.00000*, 12.01100*: V 1.0, S MGI, C D, R R1 13 | ATYPE: c_3oh: 6.00000*, 12.01100*: V 1.0, S MGI, C D, R R1 14 | ATYPE: h_1: 1.00000*, 1.00790*: V 1.0, S MGI, C D, R R1 15 | ATYPE: o_1: 8.00000*, 15.99940*: V 1.0, S MGI, C D, R R1 16 | ATC: c_3h: 0.00000*: V 1.0, S MGI, C D, R R1 17 | ATC: c_3h2: 0.00000*: V 1.0, S MGI, C D, R R1 18 | ATC: c_3oh: 0.00000*: V 1.0, S MGI, C D, R R1 19 | ATC: h_1: 0.00000*: V 1.0, S MGI, C D, R R1 20 | ATC: o_1: 0.00000*: V 1.0, S MGI, C D, R R1 21 | BINC: c_3, c_3: 0.00000*: V 1.0, S MGI, C D, R R1 22 | BINC: c_3, c_3ohxx: 1.0*: !Faked parameter for testing transfer_bci_terms() 23 | BINC: c_3, c_3o: -0.03540*: 24 | BINC: c_3, h_1: -0.16000*: V 1.0, S MGI, C D, R R1 25 | BINC: c_3oh, h_1: 0.02910*: V 1.0, S MGI, C D, R R1 26 | BINC: c_3oh, o_1: 0.45000*: V 1.0, S MGI, C D, R R1 27 | N12_6: c_3h: 3.99394*, 0.08093*: V 1.83, S MGI, C D, R R9 28 | N12_6: c_3h2: 4.12434*, 0.08752*: V 1.83, S MGI, C D, R R9 29 | N12_6: c_3oh: 3.99210*, 0.10480*: V 1.81, S MGI, C D, R R9 30 | N12_6: h_1: 2.73172*, 0.02125*: V 1.78, S MGI, C D, R R9 31 | N12_6: o_1: 3.18061*, 0.15903*: V 1.81, S MGI, C D, R R9 32 | BHARM: c_3, c_3: 1.33280*, 631.84030*: V 1.0, S MGI, C D, R R1 33 | BHARM: c_3, c_3o: 1.48150*, 255.20730*: V 1.0, S MGI, C D, R R1 34 | BHARM: c_3, h_1: 1.08930*, 388.47430*: V 1.0, S MGI, C D, R R1 35 | BHARM: c_3o, h_1: 1.11910*, 318.88820*: V 1.0, S MGI, C D, R R1 36 | BHARM: c_3o, o_1: 1.21410*, 884.15980*: V 1.0, S MGI, C D, R R1 37 | AHARM: c_3, c_3, c_3o: 118.81770*, 89.58780*: V 1.0, S MGI, C D, R R1 38 | AHARM: c_3, c_3oxx, h_1: 119.10270*, 35.54310*: !Faked parameter for testing transfer_bonded_terms() 39 | AHARM: c_3, c_3, h_1: 119.10270*, 35.54310*: V 1.0, S MGI, C D, R R1 40 | AHARM: c_3, c_3o, o_1: 125.89090*, 51.50440*: V 1.0, S MGI, C D, R R1 41 | AHARM: c_3o, c_3, h_1: 116.64320*, 40.50880*: V 1.0, S MGI, C D, R R1 42 | AHARM: h_1, c_3, h_1: 115.65010*, 32.11610*: V 1.0, S MGI, C D, R R1 43 | AHARM: h_1, c_3o, o_1: 120.23800*, 58.02860*: V 1.0, S MGI, C D, R R1 44 | TCOSP: X, c_3, c_3, X: 180.00000*, 7.79420*, 2.00000*: V 1.52, S MGI, C D, R R9 45 | TCOSP: X, c_3, c_3o, X: 180.00000*, 3.31080*, 2.00000*: V 1.0, S MGI, C D, R R1 46 | TCOSP: c_3, c_3, c_3o, o_1: 180.00000*, 2.13000*, 2.00000*, 0.00000*, -2.55870*, 1.00000*, 0.00000*, 1.13570*, 3.00000*: V 1.0, S MGI, C D, R R1 47 | TCOSP: h_1, c_3, c_3o, o_1: 180.00000*, 0.12860*, 2.00000*, 0.00000*, -0.44580*, 1.00000*, 0.00000*, 0.55990*, 3.00000*: V 1.0, S MGI, C D, R R1 48 | IBCOS: X, X, c_3, X: 180.00000*, 4.00000*, 2.00000*: V 1.98, S MGI, C D, R R7 49 | IBCOS: X, X, c_3o, X: 180.00000*, 18.00000*, 2.00000*: V 1.106, S MGI, C D, R R7 50 | -------------------------------------------------------------------------------- /tests/topology/files/c_3oh.psf: -------------------------------------------------------------------------------- 1 | PSF NAMD 2 | 3 | 1 !NTITLE 4 | REMARKS Created by mstk 5 | 6 | 8 !NATOM 7 | 1 S 1 UNK C1 c_3h2 0.000000 12.0110 0 -0.0000 0.0000 8 | 2 S 1 UNK C2 c_3h 0.000000 12.0110 0 -0.0000 0.0000 9 | 3 S 1 UNK C3 c_3oh 0.000000 12.0110 0 -0.0000 0.0000 10 | 4 S 1 UNK O4 o_1 0.000000 15.9990 0 -0.0000 0.0000 11 | 5 S 1 UNK H5 h_1 0.000000 1.0080 0 -0.0000 0.0000 12 | 6 S 1 UNK H6 h_1 0.000000 1.0080 0 -0.0000 0.0000 13 | 7 S 1 UNK H7 h_1 0.000000 1.0080 0 -0.0000 0.0000 14 | 8 S 1 UNK H8 h_1 0.000000 1.0080 0 -0.0000 0.0000 15 | 16 | 7 !NBOND: bonds 17 | 1 2 2 3 3 4 1 5 18 | 1 6 2 7 3 8 19 | 20 | 9 !NTHETA: angles 21 | 2 1 5 2 1 6 5 1 6 22 | 1 2 3 1 2 7 3 2 7 23 | 2 3 4 2 3 8 4 3 8 24 | 25 | 8 !NPHI: dihedrals 26 | 5 1 2 3 5 1 2 7 27 | 6 1 2 3 6 1 2 7 28 | 1 2 3 4 1 2 3 8 29 | 7 2 3 4 7 2 3 8 30 | 31 | 3 !NIMPHI: impropers 32 | 1 2 5 6 2 1 3 7 33 | 3 2 4 8 34 | 35 | 0 !NDON: donors 36 | 37 | 0 !NACC: acceptors 38 | 39 | 0 !NNB 40 | 41 | 42 | 0 0 !NUMLP NUMLPH 43 | 44 | 0 !NUMANISO 45 | 46 | -------------------------------------------------------------------------------- /tests/topology/files/test.pdb: -------------------------------------------------------------------------------- 1 | REMARK Created by mstk 2 | CRYST1 3.000 4.000 5.000 90.00 90.00 90.00 P 1 1 3 | HETATM 1 N1 c1c1 1 0.000 0.000 0.000 N 4 | HETATM 2 C2 c1c1 1 1.315 0.000 0.000 C 5 | HETATM 3 N3 c1c1 1 1.761 1.237 0.000 N 6 | HETATM 4 C4 c1c1 1 0.672 2.082 0.000 C 7 | HETATM 5 C5 c1c1 1 -0.425 1.311 0.000 C 8 | HETATM 6 C6 c1c1 1 -0.870 -1.180 0.000 C 9 | HETATM 7 H7 c1c1 1 1.936 -0.884 0.000 H 10 | HETATM 8 C8 c1c1 1 3.166 1.653 0.000 C 11 | HETATM 9 H9 c1c1 1 0.697 3.162 0.000 H 12 | HETATM 10 H10 c1c1 1 -1.450 1.653 0.000 H 13 | HETATM 11 H11 c1c1 1 -1.500 -1.167 -0.890 H 14 | HETATM 12 H12 c1c1 1 -0.260 -2.082 -0.000 H 15 | HETATM 13 H13 c1c1 1 -1.500 -1.167 0.890 H 16 | HETATM 14 H14 c1c1 1 3.369 2.249 0.890 H 17 | HETATM 15 H15 c1c1 1 3.807 0.771 0.000 H 18 | HETATM 16 H16 c1c1 1 3.369 2.249 -0.890 H 19 | CONECT 1 2 6 5 20 | CONECT 2 3 7 21 | CONECT 3 4 8 22 | CONECT 4 5 9 23 | CONECT 5 10 24 | CONECT 6 11 12 13 25 | CONECT 8 14 15 16 26 | -------------------------------------------------------------------------------- /tests/topology/files/urea.xyz: -------------------------------------------------------------------------------- 1 | 8 2 | urea 3 | OU -0.69265 1.40835 -0.22910 4 | CU -1.29431 0.34754 -0.30568 5 | NU -1.44367 -0.25612 -1.50814 6 | NU -1.80763 -0.21721 0.81244 7 | HU -1.69355 0.24297 1.72783 8 | HU -2.31745 -1.11158 0.76328 9 | HU -1.05255 0.17444 -2.35923 10 | HU -1.94866 -1.15100 -1.58814 11 | -------------------------------------------------------------------------------- /tests/topology/test_bond.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import math 5 | import pytest 6 | from mstk.topology import Topology, Molecule 7 | 8 | cwd = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def test_cccc(): 12 | cccc = Molecule.from_smiles('C=CC=C') 13 | assert [b.is_aromatic for b in cccc.bonds] == [False] * 9 14 | assert [b.is_conjugated for b in cccc.bonds] == [True] * 3 + [False] * 6 15 | 16 | 17 | def test_cccco(): 18 | cccc = Molecule.from_smiles('C=CC=CO') 19 | assert [b.is_aromatic for b in cccc.bonds] == [False] * 10 20 | assert [b.is_conjugated for b in cccc.bonds] == [True] * 4 + [False] * 6 21 | 22 | 23 | def test_im1(): 24 | im1 = Molecule.from_smiles('C[n+]1c[nH](cc1)') 25 | assert [b.is_aromatic for b in im1.bonds] == [False] + [True] * 5 + [False] * 7 26 | assert [b.is_conjugated for b in im1.bonds] == [False] + [True] * 5 + [False] * 7 27 | 28 | 29 | def test_bf4(): 30 | bf4 = Molecule.from_smiles('[B-](F)(F)(F)F') 31 | assert [b.is_aromatic for b in bf4.bonds] == [False] * 4 32 | assert [b.is_conjugated for b in bf4.bonds] == [False] * 4 33 | -------------------------------------------------------------------------------- /tests/topology/test_geometry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from mstk.topology.geometry import * 4 | 5 | 6 | def test_grow_particle(): 7 | xyz1 = np.array([0, 0, 0]) 8 | xyz2 = np.array([0, 1, 0]) 9 | print(grow_particle(xyz1, xyz2, 2, np.pi * 0.1)) 10 | 11 | 12 | def test_cluster(): 13 | elements = list(range(10)) 14 | bonds = [(7, 1), (1, 0), (3, 4), (5, 6), (4, 7)] 15 | matrix = np.zeros((10, 10)) 16 | for i, j in bonds: 17 | matrix[i][j] = 1 18 | matrix[j][i] = 1 19 | 20 | assert find_clusters(elements, lambda x, y: matrix[x][y]) == [[0, 1, 3, 4, 7], [2], [5, 6], [8], [9]] 21 | assert find_clusters_consecutive(elements, lambda x, y: matrix[x][y]) == [[0, 1, 2, 3, 4, 5, 6, 7], [8], [9]] 22 | -------------------------------------------------------------------------------- /tests/topology/test_gro.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pytest 5 | import tempfile 6 | import filecmp 7 | import shutil 8 | from mstk.topology import Topology, Molecule 9 | 10 | cwd = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | 13 | def test_read(): 14 | gro = Topology.open(cwd + '/files/100-SPCE.gro') 15 | assert gro.n_atom == 300 16 | assert pytest.approx(gro.cell.volume, abs=1E-6) == 27.0 17 | 18 | assert gro.n_molecule == 100 19 | mol = gro.molecules[0] 20 | assert mol.name == 'SPCE' 21 | assert mol.n_atom == 3 22 | 23 | assert gro.n_residue == 100 24 | res = gro.residues[-1] 25 | assert res.name == 'SPCE' 26 | assert res.n_atom == 3 27 | 28 | atom = gro.atoms[-1] 29 | assert atom.name == 'H' 30 | assert atom.type == 'H' 31 | assert atom.symbol == 'H' 32 | assert pytest.approx(atom.position, abs=1E-6) == [2.726, 2.750, 1.876] 33 | assert atom.has_position 34 | 35 | 36 | def test_write(): 37 | tmpdir = tempfile.mkdtemp() 38 | zmat = Topology.open(cwd + '/files/100-SPCE.gro') 39 | tmp = os.path.join(tmpdir, 'gro-out.gro') 40 | zmat.write(tmp) 41 | assert filecmp.cmp(tmp, cwd + '/files/baselines/gro-out.gro') 42 | shutil.rmtree(tmpdir) 43 | -------------------------------------------------------------------------------- /tests/topology/test_lmp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pytest 5 | from mstk.topology import Topology 6 | 7 | cwd = os.path.dirname(os.path.abspath(__file__)) 8 | 9 | 10 | def test_read(): 11 | lmp = Topology.open(cwd + '/files/10-H2O-5-C3H6.lmp') 12 | assert lmp.n_atom == 75 13 | assert lmp.n_molecule == 15 14 | assert lmp.is_drude == False 15 | assert lmp.cell.is_rectangular 16 | assert pytest.approx(lmp.cell.lengths, abs=1E-6) == [3.18888, 3.18888, 3.18888] 17 | 18 | atom = lmp.atoms[0] 19 | assert atom.id == 0 20 | assert atom.molecule.id == 0 21 | assert atom.molecule.name == 'WAT' 22 | assert atom.type == 'o_2w' 23 | assert atom.name == 'O1' 24 | assert atom.charge == -0.8476 25 | assert atom.mass == 15.9994 26 | 27 | atom = lmp.atoms[-1] 28 | assert atom.id == 74 29 | assert atom.molecule.id == 14 30 | assert atom.molecule.name == 'C3H6' 31 | assert atom.type == 'h_1' 32 | assert atom.name == 'H9' 33 | assert atom.charge == 0.06 34 | assert atom.mass == 1.0079 35 | 36 | mol = lmp.molecules[0] 37 | assert mol.id == 0 38 | assert mol.name == 'WAT' 39 | 40 | mol = lmp.molecules[-1] 41 | assert mol.id == 14 42 | assert mol.name == 'C3H6' 43 | 44 | assert lmp.has_position 45 | atom = lmp.atoms[0] 46 | assert pytest.approx(atom.position, abs=1E-6) == [2.4257, 0.3594, 0.3218] 47 | atom = lmp.atoms[-1] 48 | assert pytest.approx(atom.position, abs=1E-6) == [1.6725, 2.1756, 0.5918] 49 | 50 | 51 | def test_improper(): 52 | lmp = Topology.open(cwd + '/files/10-H2O-5-C3H6.lmp') 53 | assert lmp.n_improper == 10 54 | improper = lmp.impropers[0] 55 | assert improper.atom1.id + 1 == 32 56 | assert improper.atom2.id + 1 == 34 57 | assert improper.atom3.id + 1 == 31 58 | assert improper.atom4.id + 1 == 35 59 | 60 | lmp = Topology.open(cwd + '/files/10-H2O-5-C3H6.lmp', improper_center=3) 61 | improper = lmp.impropers[0] 62 | assert improper.atom1.id + 1 == 31 63 | assert improper.atom2.id + 1 == 32 64 | assert improper.atom3.id + 1 == 34 65 | assert improper.atom4.id + 1 == 35 66 | -------------------------------------------------------------------------------- /tests/topology/test_mae.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pytest 5 | import tempfile 6 | import filecmp 7 | from mstk.topology import Topology 8 | from mstk.chem.constant import RAD2DEG 9 | 10 | cwd = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | 13 | def test_read(): 14 | top = Topology.open(cwd + '/files/Structures.mae') 15 | assert top.n_atom == 473 16 | assert top.n_molecule == 7 17 | assert top.n_residue == 37 18 | assert top.n_bond == 476 19 | assert top.n_improper == 80 20 | assert pytest.approx(top.cell.lengths, abs=1E-6) == [3.709398, 3.8, 3.9] 21 | assert pytest.approx(top.cell.angles * RAD2DEG, abs=1E-6) == [90, 90, 90] 22 | 23 | mol = top.molecules[0] 24 | assert mol.name == 'C4OH' 25 | assert mol.n_atom == 15 26 | assert mol.n_residue == 1 27 | 28 | atom = mol.atoms[0] 29 | assert atom.name == 'O1' 30 | assert atom.type == 'UNK' 31 | assert atom.symbol == 'O' 32 | assert atom.charge == 0.0 33 | assert pytest.approx(atom.position, abs=1E-6) == [-0.0460000, 0.1301076, 0.0000000] 34 | assert atom.has_position 35 | assert atom.molecule.name == 'C4OH' 36 | assert atom.residue.name == 'C4OH' 37 | 38 | mol = top.molecules[1] 39 | atom = mol.atoms[-4] 40 | assert atom.name == 'O5' 41 | assert atom.type == 'UNK' 42 | assert atom.symbol == 'O' 43 | assert atom.formal_charge == -1.0 44 | assert atom.charge == 0.0 45 | assert atom.molecule.name == 'CSO3-' 46 | assert atom.residue.name == 'CSO3-' 47 | 48 | mol = top.molecules[2] 49 | assert mol.name == 'UNK' 50 | assert mol.n_atom == 90 51 | assert mol.n_bond == 91 52 | assert mol.n_residue == 7 53 | 54 | atom = mol.atoms[-1] 55 | assert atom.name == 'H1' 56 | assert atom.type == 'UNK' 57 | assert atom.symbol == 'H' 58 | assert atom.charge == 0.0 59 | assert pytest.approx(atom.position, abs=1E-6) == [0.6584924, 1.3906516, 1.9601327] 60 | assert atom.has_position 61 | assert atom.molecule.name == 'UNK' 62 | assert atom.residue.name == 'TRM' 63 | -------------------------------------------------------------------------------- /tests/topology/test_molecule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pytest 5 | from mstk.chem.constant import * 6 | from mstk.topology import Topology, Molecule, Bond 7 | 8 | cwd = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def test_smiles(): 12 | im41 = Molecule.from_smiles('C[n+]1cn(cc1)CCCC Im41') 13 | assert im41.name == 'Im41' 14 | assert im41.n_atom == 25 15 | assert im41.n_improper == 5 16 | 17 | bf4 = Molecule.from_smiles('[B-](F)(F)(F)F') 18 | assert bf4.n_atom == 5 19 | assert bf4.name == 'BF4-' 20 | assert bf4.n_improper == 0 21 | 22 | 23 | def test_connectivity(): 24 | ethane = Topology.open(cwd + '/files/CH3NH2.pdb').molecules[0] 25 | assert pytest.approx(ethane.bonds[0].evaluate(), abs=1E-4) == 0.1070 26 | assert pytest.approx(ethane.angles[0].evaluate(), abs=1E-4) == 109.5278 * DEG2RAD 27 | assert pytest.approx(ethane.dihedrals[0].evaluate(), abs=1E-4) == 60.0301 * DEG2RAD 28 | assert pytest.approx(ethane.dihedrals[-1].evaluate(), abs=1E-4) == -60.0159 * DEG2RAD 29 | assert pytest.approx(ethane.impropers[0].evaluate(), abs=1E-4) == -33.0905 * DEG2RAD 30 | 31 | 32 | def test_distance_matrix(): 33 | ethanol = Molecule.from_smiles('CO ethanol') 34 | assert ethanol.get_distance_matrix().tolist() == [[0, 1, 1, 1, 1, 2], 35 | [1, 0, 2, 2, 2, 1], 36 | [1, 2, 0, 2, 2, 3], 37 | [1, 2, 2, 0, 2, 3], 38 | [1, 2, 2, 2, 0, 3], 39 | [2, 1, 3, 3, 3, 0]] 40 | assert ethanol.get_distance_matrix(max_bond=2).tolist() == [[0, 1, 1, 1, 1, 2], 41 | [1, 0, 2, 2, 2, 1], 42 | [1, 2, 0, 2, 2, 0], 43 | [1, 2, 2, 0, 2, 0], 44 | [1, 2, 2, 2, 0, 0], 45 | [2, 1, 0, 0, 0, 0]] 46 | 47 | 48 | def test_merge_split(): 49 | def print_mol(mol): 50 | print(mol) 51 | for atom in mol.atoms: 52 | print(atom.residue, atom) 53 | for bond in mol.bonds: 54 | print(bond) 55 | 56 | ethanol = Molecule.from_smiles('CO ethanol') 57 | water = Molecule.from_smiles('O water') 58 | ethyne = Molecule.from_smiles('C#C ethyne') 59 | mol = Molecule.merge([ethanol, water, ethyne]) 60 | assert mol.n_atom == 13 61 | 62 | C1_ethyne = mol.atoms[-4] 63 | C2_ethyne = mol.atoms[-3] 64 | assert C2_ethyne.name == 'C2' 65 | bond = C2_ethyne.bonds[0] 66 | assert bond.equals(Bond(C1_ethyne, C2_ethyne)) 67 | mol.remove_connectivity(bond) 68 | mol.generate_angle_dihedral_improper() 69 | 70 | O_ethanol = mol.atoms[1] 71 | assert O_ethanol.name == 'O2' 72 | mol.add_bond(O_ethanol, C1_ethyne) 73 | 74 | pieces = mol.split() 75 | assert len(pieces) == 3 76 | piece1, piece2, piece3 = pieces 77 | assert piece1.n_atom == 8 78 | assert piece1.n_residue == 2 79 | assert piece1.n_bond == 7 80 | assert piece1.n_angle == 7 81 | print_mol(piece1) 82 | assert piece2.n_atom == 3 83 | assert piece2.n_residue == 1 84 | assert piece2.n_bond == 2 85 | assert piece2.n_angle == 1 86 | print_mol(piece2) 87 | assert piece3.n_atom == 2 88 | assert piece3.n_residue == 1 89 | assert piece3.n_bond == 1 90 | assert piece3.n_angle == 0 91 | print_mol(piece3) 92 | 93 | pieces = mol.split_residues() 94 | assert len(pieces) == 2 95 | piece1, piece2 = pieces 96 | assert piece1.n_atom == 10 97 | assert piece1.n_residue == 2 98 | assert piece1.n_bond == 8 99 | assert piece1.n_angle == 7 100 | print_mol(piece1) 101 | assert piece2.n_atom == 3 102 | assert piece2.n_residue == 1 103 | assert piece2.n_bond == 2 104 | assert piece2.n_angle == 1 105 | print_mol(piece2) -------------------------------------------------------------------------------- /tests/topology/test_pdb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pytest 5 | import tempfile 6 | import filecmp 7 | import shutil 8 | from mstk.topology import Topology, Molecule 9 | 10 | cwd = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | 13 | def test_read(): 14 | pdb = Topology.open(cwd + '/files/test.pdb') 15 | assert pdb.n_atom == 16 16 | assert pytest.approx(pdb.cell.volume, abs=1E-6) == 0.06 17 | 18 | mol = pdb.molecules[0] 19 | assert mol.name == 'c1c1' 20 | 21 | atom = pdb.atoms[-1] 22 | assert atom.name == 'H16' 23 | assert atom.type == 'H16' 24 | assert atom.symbol == 'H' 25 | assert pytest.approx(atom.position, abs=1E-6) == [0.3369, 0.2249, -0.0890] 26 | assert atom.has_position 27 | 28 | 29 | def test_write(): 30 | tmpdir = tempfile.mkdtemp() 31 | zmat = Topology.open(cwd + '/files/Im11.zmat') 32 | tmp = os.path.join(tmpdir, 'zmat-out.pdb') 33 | zmat.write(tmp) 34 | assert filecmp.cmp(tmp, cwd + '/files/baselines/zmat-out.pdb') 35 | shutil.rmtree(tmpdir) 36 | -------------------------------------------------------------------------------- /tests/topology/test_psf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import tempfile 5 | import filecmp 6 | import pytest 7 | import shutil 8 | from mstk.topology import Topology, TIP4PSite 9 | from mstk.forcefield import ForceField 10 | 11 | cwd = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | def test_read(): 15 | psf = Topology.open(cwd + '/files/10-H2O-5-C3H6.psf') 16 | assert psf.is_drude == False 17 | assert psf.n_atom == 75 18 | assert psf.n_molecule == 15 19 | assert psf.n_bond == 60 20 | assert psf.n_angle == 70 21 | assert psf.n_dihedral == 50 22 | assert psf.n_improper == 10 23 | assert psf.cell.volume == 0 24 | 25 | assert psf.bonds[-1].atom1.name == 'C69' 26 | assert psf.angles[-1].atom2.name == 'C69' 27 | assert psf.dihedrals[-1].atom4.name == 'H75' 28 | assert psf.impropers[-1].atom1.type == 'c_3h' 29 | 30 | atom = psf.atoms[0] 31 | assert atom.id == 0 32 | assert atom.molecule.id == 0 33 | assert atom.type == 'o_2w' 34 | assert atom.name == 'O1' 35 | assert atom.charge == -0.8476 36 | assert atom.mass == 15.9994 37 | 38 | atom = psf.atoms[-1] 39 | assert atom.id == 74 40 | assert atom.molecule.id == 14 41 | assert atom.type == 'h_1' 42 | assert atom.name == 'H75' 43 | assert atom.charge == 0.06 44 | assert atom.mass == 1.00794 45 | 46 | mol = psf.molecules[0] 47 | assert mol.id == 0 48 | assert mol.name == 'WAT' 49 | 50 | mol = psf.molecules[-1] 51 | assert mol.id == 14 52 | assert mol.name == 'C3H' 53 | 54 | 55 | def test_read_swm4(): 56 | psf = Topology.open(cwd + '/files/9-SWM4.psf') 57 | assert psf.n_molecule == 9 58 | 59 | mol = psf.molecules[-1] 60 | assert mol.n_atom == 5 61 | 62 | assert not mol.atoms[0].is_drude 63 | assert mol.atoms[1].is_drude 64 | assert not mol.atoms[2].is_drude 65 | assert not mol.atoms[3].is_drude 66 | assert not mol.atoms[4].is_drude 67 | 68 | atom = mol.atoms[0] 69 | assert atom.alpha == 0.0009782 70 | assert atom.thole == 2.6 71 | 72 | assert mol.atoms[0].virtual_site is None 73 | assert mol.atoms[1].virtual_site is None 74 | assert mol.atoms[2].virtual_site is None 75 | assert mol.atoms[3].virtual_site is None 76 | assert mol.atoms[4].virtual_site is not None 77 | 78 | vsite = mol.atoms[-1].virtual_site 79 | assert type(vsite) is TIP4PSite 80 | assert vsite.parents[0] is mol.atoms[0] 81 | assert vsite.parents[1] is mol.atoms[2] 82 | assert vsite.parents[2] is mol.atoms[3] 83 | assert vsite.parameters == [0.024034] 84 | 85 | 86 | def test_write(): 87 | tmpdir = tempfile.mkdtemp() 88 | lmp = Topology.open(cwd + '/files/10-H2O-5-C3H6.lmp', improper_center=3) 89 | tmp = os.path.join(tmpdir, 'lmp-out.psf') 90 | lmp.write(tmp) 91 | assert filecmp.cmp(tmp, cwd + '/files/baselines/lmp-out.psf') 92 | 93 | zmat = Topology.open(cwd + '/files/Im11.zmat') 94 | tmp = os.path.join(tmpdir, 'zmat-out.psf') 95 | zmat.write(tmp) 96 | assert filecmp.cmp(tmp, cwd + '/files/baselines/zmat-out.psf') 97 | shutil.rmtree(tmpdir) 98 | 99 | 100 | def test_write_swm4(): 101 | tmpdir = tempfile.mkdtemp() 102 | ff = ForceField.open(cwd + '/../forcefield/files/SWM4-NDP.zff') 103 | mol = Topology.open(cwd + '/files/TIP3P.zmat').molecules[0] 104 | mol.generate_virtual_sites(ff) 105 | mol.generate_drude_particles(ff) 106 | ff.assign_charge(mol) 107 | 108 | top = Topology([mol], numbers=[10]) 109 | tmp = os.path.join(tmpdir, 'swm4-out.psf') 110 | top.write(tmp) 111 | 112 | assert filecmp.cmp(tmp, cwd + '/files/baselines/swm4-out.psf') 113 | shutil.rmtree(tmpdir) 114 | -------------------------------------------------------------------------------- /tests/topology/test_sdf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pytest 5 | import tempfile 6 | import filecmp 7 | from mstk.topology import Topology 8 | from mstk.chem.constant import RAD2DEG 9 | 10 | cwd = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | 13 | def test_read(): 14 | top = Topology.open(cwd + '/files/CH3NH3+.sdf') 15 | assert top.n_molecule == 1 16 | 17 | mol = top.molecules[0] 18 | assert mol.name == 'CH3NH3+' 19 | assert mol.n_atom == 8 20 | 21 | atom = top.atoms[1] 22 | assert atom.name == 'N2' 23 | assert atom.type == 'UNK' 24 | assert atom.symbol == 'N' 25 | assert pytest.approx(atom.position, abs=1E-6) == [0.07355, 0.00131, -0.00126] 26 | assert atom.has_position 27 | assert atom.formal_charge == 1 28 | 29 | 30 | def test_read_V3000(): 31 | top = Topology.open(cwd + '/files/Structures.sdf') 32 | assert top.n_atom == 473 33 | assert top.n_molecule == 3 34 | assert top.n_residue == 3 35 | assert top.n_bond == 476 36 | assert top.n_improper == 80 37 | assert pytest.approx(top.cell.lengths, abs=1E-6) == [0, 0, 0] 38 | assert pytest.approx(top.cell.angles * RAD2DEG, abs=1E-6) == [90, 90, 90] 39 | 40 | mol = top.molecules[0] 41 | assert mol.name == 'C4OH' 42 | assert mol.n_atom == 15 43 | assert mol.n_residue == 1 44 | 45 | atom = mol.atoms[0] 46 | assert atom.name == 'O1' 47 | assert atom.type == 'UNK' 48 | assert atom.symbol == 'O' 49 | assert atom.charge == 0.0 50 | assert pytest.approx(atom.position, abs=1E-6) == [-0.0460000, 0.1301076, 0.0000000] 51 | assert atom.has_position 52 | assert atom.molecule.name == 'C4OH' 53 | assert atom.residue.name == 'C4OH' 54 | 55 | mol = top.molecules[1] 56 | atom = mol.atoms[-4] 57 | assert atom.name == 'O5' 58 | assert atom.type == 'UNK' 59 | assert atom.symbol == 'O' 60 | assert atom.formal_charge == -1.0 61 | assert atom.charge == 0.0 62 | assert atom.molecule.name == 'CSO3-' 63 | assert atom.residue.name == 'CSO3-' 64 | 65 | mol = top.molecules[2] 66 | assert mol.name == 'amorphous poly(AA-HDO)' 67 | assert mol.n_atom == 90 * 5 68 | assert mol.n_bond == 91 * 5 69 | assert mol.n_residue == 1 70 | 71 | atom = mol.atoms[89] 72 | assert atom.name == 'H90' 73 | assert atom.type == 'UNK' 74 | assert atom.symbol == 'H' 75 | assert atom.charge == 0.0 76 | assert pytest.approx(atom.position, abs=1E-6) == [0.6584924, 1.3906516, 1.9601327] 77 | assert atom.has_position 78 | assert atom.molecule.name == 'amorphous poly(AA-HDO)' 79 | assert atom.residue.name == 'amorphous_poly(AA-HDO)' 80 | -------------------------------------------------------------------------------- /tests/topology/test_topology.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pytest 5 | import tempfile 6 | import filecmp 7 | import shutil 8 | from mstk.topology import Topology, UnitCell 9 | from mstk.forcefield import ForceField 10 | 11 | cwd = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | def test_assign_charge(): 15 | top = Topology.open(cwd + '/files/c_3oh.psf') 16 | ff = ForceField.open(cwd + '/files/c_3oh.ppf') 17 | ff.assign_charge(top) 18 | charges = [atom.charge for atom in top.atoms] 19 | assert pytest.approx(charges, abs=1E-6) == [-0.32, -0.16, 0.4791, -0.45, 0.16, 0.16, 0.16, -0.0291] 20 | 21 | 22 | def test_compress(): 23 | top = Topology.open(cwd + '/files/10-H2O-5-C3H6.lmp', improper_center=3) 24 | molecules = top.molecules 25 | for mol in molecules[4:]: 26 | for atom in mol.atoms: 27 | atom.charge *= 2 28 | 29 | top = Topology.open(cwd + '/files/Im11.zmat') 30 | molecules += top.molecules 31 | 32 | top = Topology.open(cwd + '/files/10-H2O-5-C3H6.lmp', improper_center=3) 33 | molecules += top.molecules 34 | 35 | top = Topology.open(cwd + '/files/Im11.zmat') 36 | molecules += top.molecules 37 | 38 | top = Topology() 39 | top.update_molecules(molecules) 40 | mols_unique = top.get_unique_molecules() 41 | for mol, count in mols_unique.items(): 42 | print(str(mol), count) 43 | 44 | assert list(mols_unique.values()) == [4, 6, 5, 1, 10, 5, 1] 45 | 46 | 47 | def test_packmol(): 48 | # use fixed path so that _pack.inp has fixed content 49 | tmpdir = os.path.join(tempfile.gettempdir(), '_mstk_' + test_packmol.__name__) 50 | os.makedirs(tmpdir, exist_ok=True) 51 | top = Topology.open(cwd + '/files/Im11.zmat') 52 | top.cell.set_box([3, 3, 3]) 53 | top.scale_with_packmol(10, tempdir=tmpdir) 54 | assert filecmp.cmp(tmpdir + '/_pack.inp', cwd + '/files/baselines/_pack.inp') 55 | assert filecmp.cmp(tmpdir + '/_MO_0.xyz', cwd + '/files/baselines/_MO_0.xyz') 56 | shutil.rmtree(tmpdir) 57 | 58 | 59 | def test_guess_bonds(): 60 | top = Topology.open(cwd + '/files/MoS2-13x8-layer1.xyz') 61 | top.cell.set_box([4.109, 4.380, 1.230]) 62 | ff = ForceField.open(cwd + '/files/MoS2.ff') 63 | top.guess_bonds_from_ff(ff, pbc='xy') 64 | assert top.n_bond == 1248 65 | -------------------------------------------------------------------------------- /tests/topology/test_unitcell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pytest 5 | from mstk.chem.constant import * 6 | from mstk.topology import UnitCell 7 | 8 | cwd = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def test_cell(): 12 | cell = UnitCell() 13 | assert cell.is_rectangular == True 14 | assert cell.volume == 0.0 15 | assert pytest.approx(cell.get_size(), abs=1E-6) == [0, 0, 0] 16 | assert pytest.approx(cell.lengths, abs=1E-6) == [0, 0, 0] 17 | assert pytest.approx(cell.angles, abs=1E-6) == [PI / 2, PI / 2, PI / 2] 18 | 19 | cell = UnitCell([0, 0, 0]) 20 | assert cell.is_rectangular == True 21 | assert cell.volume == 0.0 22 | assert pytest.approx(cell.get_size(), abs=1E-6) == [0, 0, 0] 23 | assert pytest.approx(cell.lengths, abs=1E-6) == [0, 0, 0] 24 | assert pytest.approx(cell.angles, abs=1E-6) == [PI / 2, PI / 2, PI / 2] 25 | 26 | cell = UnitCell([1, 2, 3]) 27 | assert cell.is_rectangular == True 28 | assert cell.volume == 6.0 29 | assert pytest.approx(cell.get_size(), abs=1E-6) == [1, 2, 3] 30 | assert pytest.approx(cell.lengths, abs=1E-6) == [1, 2, 3] 31 | assert pytest.approx(cell.angles, abs=1E-6) == [PI / 2, PI / 2, PI / 2] 32 | 33 | cell = UnitCell([[1, 0, 0], [0, 2, 0], [0, 0, 3]]) 34 | assert cell.is_rectangular == True 35 | assert cell.volume == 6.0 36 | assert pytest.approx(cell.get_size(), abs=1E-6) == [1, 2, 3] 37 | assert pytest.approx(cell.lengths, abs=1E-6) == [1, 2, 3] 38 | assert pytest.approx(cell.angles, abs=1E-6) == [PI / 2, PI / 2, PI / 2] 39 | 40 | cell = UnitCell([[1, 0, 0], [0.3, 2, 0], [0.3, 0.5, 3]]) 41 | assert cell.is_rectangular == False 42 | assert cell.volume == 6.0 43 | assert pytest.approx(cell.get_size(), abs=1E-6) == [1, 2, 3] 44 | assert pytest.approx(cell.lengths, abs=1E-6) == [1, 2.022375, 3.056141] 45 | assert pytest.approx(cell.angles, abs=1E-6) == [79.842392 * DEG2RAD, 84.366600 * DEG2RAD, 81.469230 * DEG2RAD] 46 | -------------------------------------------------------------------------------- /tests/topology/test_virtual_site.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import tempfile 5 | import filecmp 6 | import pytest 7 | from mstk.topology import Topology, Molecule, Atom, TIP4PSite 8 | from mstk.forcefield import ForceField 9 | 10 | cwd = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | 13 | def test_tip4p(): 14 | top = Topology.open(os.path.join(cwd, 'files/TIP3P.zmat')) 15 | mol = top.molecules[0] 16 | 17 | ff = ForceField.open(os.path.join(cwd, '../forcefield/files/TIP4P.zff')) 18 | mol.generate_virtual_sites(ff) 19 | ff.assign_charge(mol) 20 | 21 | avsite = top.atoms[-1] 22 | assert avsite.name == 'VS1' 23 | assert avsite.type == 'MW' 24 | assert pytest.approx(avsite.position, abs=1E-6) == [0.009181, -0.011862, 0.] 25 | 26 | assert pytest.approx([atom.charge for atom in top.atoms], abs=1E-6) == [0, 0.52, 0.52, -1.04] 27 | -------------------------------------------------------------------------------- /tests/topology/test_xyz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import tempfile 5 | import filecmp 6 | import pytest 7 | import shutil 8 | from mstk.topology import Topology, Molecule 9 | 10 | cwd = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | 13 | def test_read(): 14 | xyz = Topology.open(cwd + '/files/urea.xyz') 15 | assert xyz.n_atom == 8 16 | assert xyz.cell.volume == 0 17 | 18 | mol = xyz.molecules[0] 19 | assert mol.name == 'urea' 20 | 21 | atom = xyz.atoms[-1] 22 | assert atom.name == 'H8' 23 | assert atom.type == 'HU' 24 | assert atom.symbol == 'H' 25 | assert pytest.approx(atom.position, abs=1E-6) == [-0.194866, -0.115100, -0.158814] 26 | assert atom.has_position 27 | 28 | 29 | def test_write(): 30 | tmpdir = tempfile.mkdtemp() 31 | zmat = Topology.open(cwd + '/files/Im11.zmat') 32 | tmp = os.path.join(tmpdir, 'zmat-out.xyz') 33 | zmat.write(tmp) 34 | assert filecmp.cmp(tmp, cwd + '/files/baselines/zmat-out.xyz') 35 | -------------------------------------------------------------------------------- /tests/topology/test_zmat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pytest 5 | from mstk.topology import Topology 6 | 7 | cwd = os.path.dirname(os.path.abspath(__file__)) 8 | 9 | 10 | def test_read(): 11 | zmat = Topology.open(cwd + '/files/Im11.zmat') 12 | assert zmat.n_atom == 16 13 | assert zmat.n_bond == 16 14 | assert zmat.n_angle == 27 15 | assert zmat.n_dihedral == 32 16 | assert zmat.n_improper == 5 17 | assert zmat.cell.volume == 0 18 | 19 | atom = zmat.atoms[0] 20 | assert all(atom.position == [0., 0., 0.]) 21 | assert atom.name == 'N1' 22 | assert atom.type == 'NA' 23 | atom = zmat.atoms[-1] 24 | assert pytest.approx(atom.position, abs=1E-4) == [0.3369, 0.2250, -0.0890] 25 | assert atom.name == 'H16' 26 | assert atom.type == 'H1' 27 | -------------------------------------------------------------------------------- /tests/trajectory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/trajectory/__init__.py -------------------------------------------------------------------------------- /tests/trajectory/files/100-SPCE.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/trajectory/files/100-SPCE.dcd -------------------------------------------------------------------------------- /tests/trajectory/files/100-SPCE.xtc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/trajectory/files/100-SPCE.xtc -------------------------------------------------------------------------------- /tests/trajectory/files/baselines/gro-out.dcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/trajectory/files/baselines/gro-out.dcd -------------------------------------------------------------------------------- /tests/trajectory/files/baselines/gro-out.xtc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-gong/mstk/69404710c25b5601b8d784423a3011d6602d556d/tests/trajectory/files/baselines/gro-out.xtc -------------------------------------------------------------------------------- /tests/trajectory/test_combined.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | from mstk.trajectory import Trajectory 5 | 6 | import os 7 | 8 | cwd = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def test_read(): 12 | trj = Trajectory.open([cwd + '/files/100-SPCE.gro', cwd + '/files/100HOH.lammpstrj']) 13 | assert trj.n_atom == 300 14 | assert trj.n_frame == 6 15 | 16 | frame = trj.read_frame(0) 17 | assert frame.has_velocity == True 18 | assert pytest.approx(frame.positions[0], abs=1E-6) == [1.283, 1.791, 1.658] 19 | assert frame.has_charge == False 20 | 21 | frame = trj.read_frame(1) 22 | assert frame.cell.is_rectangular 23 | assert pytest.approx(frame.cell.get_size(), abs=1E-6) == [3.00906, 3.00906, 3.00906] 24 | assert pytest.approx(frame.velocities[-1], abs=1E-6) == [-1.0323, 0.5604, -0.3797] 25 | 26 | 27 | frame = trj.read_frame(2) 28 | assert frame.has_velocity == False 29 | assert pytest.approx(frame.positions[0], abs=1E-6) == [2.2545, 1.45688, 1.98961] 30 | assert frame.has_charge == True 31 | assert pytest.approx(frame.charges[:4], abs=1E-6) == [0.4238, -0.8476, 0.4238, 0.4238] 32 | 33 | frame = trj.read_frame(3) 34 | assert frame.cell.is_rectangular 35 | assert pytest.approx(frame.cell.get_size(), abs=1E-6) == [3.0004316] * 3 36 | assert pytest.approx(frame.positions[-1], abs=1E-6) == [2.11148, 0.241373, 0.664092] 37 | -------------------------------------------------------------------------------- /tests/trajectory/test_dcd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import tempfile 5 | import filecmp 6 | import pytest 7 | import shutil 8 | from mstk.trajectory import Trajectory 9 | 10 | cwd = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | 13 | def test_read(): 14 | dcd = Trajectory.open(cwd + '/files/100-SPCE.dcd') 15 | assert dcd.n_atom == 300 16 | assert dcd.n_frame == 3 17 | 18 | frame = dcd.read_frame(0) 19 | assert frame.has_velocity == False 20 | assert frame.has_charge == False 21 | assert pytest.approx(frame.positions[0], abs=1E-6) == [1.180277, 1.421659, 1.885429] 22 | 23 | frame2, frame1 = dcd.read_frames([2, 1]) 24 | assert pytest.approx(frame1.positions[0], abs=1E-6) == [1.645082, 1.233684, 2.165311] 25 | assert pytest.approx(frame2.positions[-1], abs=1E-6) == [2.545774, 2.661569, 1.707037] 26 | assert pytest.approx(frame2.cell.lengths, abs=1E-6) == [2.983427, 2.983427, 2.983427] 27 | 28 | 29 | def test_write(): 30 | tmpdir = tempfile.mkdtemp() 31 | 32 | gro = Trajectory.open(cwd + '/files/100-SPCE.gro') 33 | tmp = os.path.join(tmpdir, 'gro-out.dcd') 34 | dcd = Trajectory.open(tmp, 'w') 35 | for i in range(gro.n_frame): 36 | frame = gro.read_frame(i) 37 | dcd.write_frame(frame) 38 | dcd.close() 39 | assert filecmp.cmp(tmp, cwd + '/files/baselines/gro-out.dcd') 40 | shutil.rmtree(tmpdir) 41 | -------------------------------------------------------------------------------- /tests/trajectory/test_gro.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import tempfile 4 | import filecmp 5 | import pytest 6 | import shutil 7 | from mstk.trajectory import Trajectory 8 | from mstk.topology import Topology 9 | 10 | import os 11 | 12 | cwd = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | 15 | def test_read(): 16 | gro = Trajectory.open(cwd + '/files/100-SPCE.gro') 17 | assert gro.n_atom == 300 18 | assert gro.n_frame == 2 19 | 20 | frame = gro.read_frame(0) 21 | assert frame.has_velocity == True 22 | assert pytest.approx(frame.positions[0], abs=1E-6) == [1.283, 1.791, 1.658] 23 | 24 | frame = gro.read_frame(1) 25 | assert frame.cell.is_rectangular 26 | assert pytest.approx(frame.cell.get_size(), abs=1E-6) == [3.00906, 3.00906, 3.00906] 27 | assert pytest.approx(frame.velocities[-1], abs=1E-6) == [-1.0323, 0.5604, -0.3797] 28 | 29 | 30 | def test_write(): 31 | tmpdir = tempfile.mkdtemp() 32 | top = Topology.open(cwd + '/files/100-SPCE.psf') 33 | xtc = Trajectory.open(cwd + '/files/100-SPCE.xtc') 34 | 35 | tmp = os.path.join(tmpdir, 'xtc-out.gro') 36 | gro = Trajectory.open(tmp, 'w') 37 | for i in range(xtc.n_frame): 38 | frame = xtc.read_frame(i) 39 | gro.write_frame(frame, top, subset=list(range(150, 300))) 40 | gro.close() 41 | assert filecmp.cmp(tmp, cwd + '/files/baselines/xtc-out.gro') 42 | 43 | shutil.rmtree(tmpdir) 44 | -------------------------------------------------------------------------------- /tests/trajectory/test_lammpstrj.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | from mstk.trajectory import Trajectory 5 | 6 | import os 7 | 8 | cwd = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def test_read(): 12 | trj = Trajectory.open(cwd + '/files/100HOH.lammpstrj') 13 | assert trj.n_atom == 300 14 | assert trj.n_frame == 4 15 | 16 | frame = trj.read_frame(0) 17 | assert frame.has_velocity == False 18 | assert pytest.approx(frame.positions[0], abs=1E-6) == [2.2545, 1.45688, 1.98961] 19 | assert frame.has_charge == True 20 | assert pytest.approx(frame.charges[:4], abs=1E-6) == [0.4238, -0.8476, 0.4238, 0.4238] 21 | 22 | frame = trj.read_frame(1) 23 | assert frame.cell.is_rectangular 24 | assert pytest.approx(frame.cell.get_size(), abs=1E-6) == [3.0004316] * 3 25 | assert pytest.approx(frame.positions[-1], abs=1E-6) == [2.11148, 0.241373, 0.664092] 26 | -------------------------------------------------------------------------------- /tests/trajectory/test_xtc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import tempfile 5 | import filecmp 6 | import pytest 7 | import shutil 8 | from mstk.topology import Topology 9 | from mstk.trajectory import Trajectory 10 | 11 | cwd = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | def test_read(): 15 | xtc = Trajectory.open(cwd + '/files/100-SPCE.xtc') 16 | assert xtc.n_atom == 300 17 | assert xtc.n_frame == 4 18 | 19 | frame = xtc.read_frame(0) 20 | assert frame.has_velocity == False 21 | assert frame.has_charge == False 22 | assert pytest.approx(frame.positions[0], abs=1E-4) == [1.283, 1.791, 1.658] 23 | assert pytest.approx(frame.positions[-1], abs=1E-4) == [2.726, 2.750, 1.876] 24 | 25 | frame2, frame1 = xtc.read_frames([2, 1]) 26 | assert pytest.approx(frame1.positions[0], abs=1E-4) == [1.335, 1.775, 1.818] 27 | assert pytest.approx(frame2.positions[-1], abs=1E-4) == [2.529, 0.136, 1.780] 28 | 29 | 30 | def test_write(): 31 | tmpdir = tempfile.mkdtemp() 32 | gro = Trajectory.open(cwd + '/files/100-SPCE.gro') 33 | tmp = os.path.join(tmpdir, 'gro-out.xtc') 34 | xtc = Trajectory.open(tmp, 'w') 35 | for i in range(gro.n_frame): 36 | frame = gro.read_frame(i) 37 | xtc.write_frame(frame) 38 | xtc.close() 39 | assert filecmp.cmp(tmp, cwd + '/files/baselines/gro-out.xtc') 40 | shutil.rmtree(tmpdir) 41 | -------------------------------------------------------------------------------- /tests/trajectory/test_xyz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import tempfile 5 | import filecmp 6 | import pytest 7 | import shutil 8 | from mstk.trajectory import Trajectory 9 | from mstk.topology import Topology 10 | 11 | cwd = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | def test_read(): 15 | xyz = Trajectory.open(cwd + '/files/100-SPCE.xyz') 16 | assert xyz.n_frame == 4 17 | assert xyz.n_atom == 300 18 | frame1 = xyz.read_frame(1) 19 | assert pytest.approx(frame1.positions[150], abs=1E-6) == [1.227000, 1.109000, 2.458000] 20 | frame2, frame0 = xyz.read_frames([2, 0]) 21 | assert pytest.approx(frame0.positions[150], abs=1E-6) == [1.410000, 1.315000, 2.851000] 22 | assert pytest.approx(frame2.positions[-1], abs=1E-6) == [2.529000, 0.136000, 1.780000] 23 | 24 | 25 | def test_write(): 26 | tmpdir = tempfile.mkdtemp() 27 | 28 | top = Topology.open(cwd + '/files/100-SPCE.psf') 29 | xtc = Trajectory.open(cwd + '/files/100-SPCE.xtc') 30 | tmp = os.path.join(tmpdir, 'xtc-out.xyz') 31 | xyz = Trajectory.open(tmp, 'w') 32 | for i in range(xtc.n_frame): 33 | frame = xtc.read_frame(i) 34 | xyz.write_frame(frame, top, subset=list(range(150, 300))) 35 | xyz.close() 36 | assert filecmp.cmp(tmp, cwd + '/files/baselines/xtc-out.xyz') 37 | 38 | shutil.rmtree(tmpdir) 39 | --------------------------------------------------------------------------------